Skip to content

feat(BA-6859): bind app_config fragments to their RBAC scope on write (repository layer)#12801

Draft
jopemachine wants to merge 1 commit into
mainfrom
feat/app-config-fragment-rbac-write-base
Draft

feat(BA-6859): bind app_config fragments to their RBAC scope on write (repository layer)#12801
jopemachine wants to merge 1 commit into
mainfrom
feat/app-config-fragment-rbac-write-base

Conversation

@jopemachine

@jopemachine jopemachine commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

Runs the app_config_fragment repository on RBACOpsProvider and binds each fragment to its RBAC scope on write — the data-layer foundation for RBAC-gating fragment writes. This is the base of the AppConfig-fragment-write-authz stack; the service-layer processor validators + DI registration land in a follow-up PR on top.

What it does

  • Global-scoped entities become first-class in the shared RBAC machinery: RBACEntityCreator.scope_ref and RBACScopeEntityUnbinder.scope_ref now accept None, meaning the entity lives outside the RBAC scope hierarchy (e.g. a public app-config fragment) and carries no scope association — the executors simply skip the association rows. Behavior for every existing caller (always non-None) is unchanged.
  • AppConfigScopeType.to_rbac_element_type() maps a fragment scope to its RBAC scope element (userUSER, domainDOMAIN, publicNone), living on the enum next to its to_rbac_scope_type sibling — no ad-hoc mapping tables at call sites.
  • create — every fragment goes through one path: RBACEntityCreator with scope_ref = fragment_rbac_scope_ref(...). A user/domain fragment is bound to its scope via an association_scopes_entities row in the same transaction; a public fragment gets scope_ref=None (no association). No scope branching in the db_source.
  • purge — one path as well: the scope unbinder spec (AppConfigFragmentScopeUnbinder) is executed by RBACWriteOps.unbind_scope_entities, implemented directly on the ops provider (the standalone execute_rbac_* helpers are transitional and their logic is being folded into the ops providers), deleting the row and its scope association atomically; a public fragment's scope_ref resolves to None, so only the row is deleted. (The generic execute_rbac_entity_purger does not apply — it also clears entity-as-scope rows and requires a scope-convertible element type, which a leaf resource like APP_CONFIG_FRAGMENT is not.)
  • Registers APP_CONFIG_FRAGMENT as an owner-accessible RBAC resource type (_resource_types()), so a user's system role covers their own user-scope fragment.
  • repository.create now takes AppConfigFragmentCreatorSpec directly (the RBAC creator is assembled inside the repository layer).

Why the split

The allow-list stays a read allowlist (existence registers a readable layer + rank); write authorization moves to RBAC. This PR is only the repository/association plumbing so it can sit at the base. The follow-up wires the RBAC validators into the fragment write processors and drops the interim _may_write check.

Notes

  • Read paths and the allow-list are unchanged.
  • bulk_create / bulk_purge do not yet write/remove associations (no API-reachable bulk-write path today); handled with the service wiring if/when needed.
  • Regression-tested the domains sharing the touched RBAC machinery (group, scaling_group, container_registry) alongside the fragment repository tests.
  • Verified: real-DB repository tests assert a user-scope create writes a USER:<id> binding, a public create writes none, and purge removes it.

📚 Stacked PRs

Part of the AppConfigFragment / AppConfig stack under BEP-1052 (epic BA-5781). Merge in order:

  1. feat(BA-6552): add app_config_fragments DB model and Alembic migration #12306feat(BA-6552): app_config_fragments DB model and Alembic migration
  2. feat(BA-6553): add app_config_fragments repository layer #12307feat(BA-6553): repository layer
  3. refactor(BA-6619): consolidate AppConfigScopeType into common.data (single definition) #12403refactor(BA-6619): consolidate AppConfigScopeType into common.data
  4. refactor(BA-6620): ExistsQuerier ops primitive + AppConfigAllowList.exists #12405refactor(BA-6620): ExistsQuerier + AppConfigAllowList.exists
  5. feat(BA-6554): add app_config_fragment service layer #12358feat(BA-6554): AppConfigFragment service layer
  6. feat(BA-6702): move fragment rank to the allow list with scope defaults #12516feat(BA-6702): move fragment rank to the allow list with scope defaults
  7. feat(BA-6701): expose allow-list rank on the v2 API surface #12517feat(BA-6701): expose allow-list rank on the v2 API surface
  8. feat(BA-6704): cascade app config subtree deletion from the definition #12518feat(BA-6704): cascade app config subtree deletion from the definition
  9. feat(BA-6626): app_config_fragment bulk repository layer #12426feat(BA-6626): app_config_fragment bulk repository layer
  10. feat(BA-6618): app_config_fragment bulk CRUD service layer #12401feat(BA-6618): app_config_fragment bulk CRUD service layer
  11. feat(BA-6810): AppConfigFragment visible-fragments query (repository layer) #12706feat(BA-6810): AppConfigFragment visible-fragments query (repository layer)
  12. 👉 feat(BA-6859): bind app_config fragments to their RBAC scope on write (repository layer) #12801feat(BA-6859): bind fragments to their RBAC scope on write (repository layer) ← you are here
  13. feat(BA-6555): AppConfig merge engine + resolve service (service layer) #12359feat(BA-6555): AppConfig merge engine + resolve service (service layer)
  14. feat(BA-6556): AppConfig REST v2 API (raw fragments and merged read/update) #12377feat(BA-6556): AppConfig REST v2 API
  15. feat(BA-6860): RBAC-gate app_config fragment writes at the processors #12759feat(BA-6860): RBAC-gate fragment writes at the processors

Access model: fragment create/update/purge are gated by RBAC (a user writes their own user-scope fragment, a domain admin their domain's, a superadmin any — public is superadmin-only); reads are gated by the allow list (existence = readable layer, rank = merge order) plus scope visibility, with no RBAC on the read path; app_config_definitions / app_config_allow_list themselves are superadmin-only (REST + GQL). Closed / superseded: #12794 · #12756 · #12764.

"""Database source for app config fragment operations."""

_ops: DBOpsProvider
_ops: RBACOpsProvider

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_rbac_ops_provider

@jopemachine jopemachine force-pushed the feat/app-config-fragment-rbac-write-base branch from 1b707ed to 19f19b7 Compare July 13, 2026 08:30
@jopemachine jopemachine force-pushed the feat/app-config-fragment-rbac-write-base branch from 19f19b7 to f9ec19e Compare July 13, 2026 08:31
@jopemachine jopemachine changed the title feat: bind app_config fragments to their RBAC scope on write (repository layer) feat(BA-6859): bind app_config fragments to their RBAC scope on write (repository layer) Jul 13, 2026
@jopemachine jopemachine force-pushed the feat/app-config-fragment-rbac-write-base branch 3 times, most recently from bbd0e93 to 269eb29 Compare July 13, 2026 09:09
Comment thread changes/12801.feature.md
@@ -0,0 +1 @@
Bind AppConfig fragments to their RBAC scope on write (repository layer): a `user` / `domain` fragment is associated to its RBAC scope on create and unbound on purge (`public` maps to GLOBAL and carries none), so fragment write authorization can be enforced by RBAC. The allow list becomes a read allowlist (registration + `rank`) while writes move to RBAC (BEP-1052).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too verbose

… (repository layer)

Run the app_config_fragment repository on RBACOpsProvider and make every
fragment write flow through the RBAC scope machinery, branch-free:

- The shared RBAC creator/unbinder specs now model *global-scoped* entities:
  RBACEntityCreator.scope_ref and RBACScopeEntityUnbinder.scope_ref accept
  None, meaning the entity lives outside the RBAC scope hierarchy and carries
  no scope association.
- AppConfigScopeType.to_rbac_element_type() maps a fragment scope to its RBAC
  scope element (public -> None), next to its to_rbac_scope_type sibling.
- create binds a user/domain fragment to its scope via
  association_scopes_entities in the same tx; purge unbinds it through
  RBACWriteOps.unbind_scope_entities (implemented directly on the ops
  provider — the standalone execute_rbac_* helpers are transitional),
  deleting the row and its association atomically. A public fragment takes
  the same single code path with scope_ref=None.
- Registers APP_CONFIG_FRAGMENT as an owner-accessible RBAC resource type.
- Repository/db_source create now takes AppConfigFragmentCreatorSpec directly.

Data-layer foundation for RBAC-gating fragment writes; service-layer
validators + DI registration follow (BA-6860).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jopemachine jopemachine force-pushed the feat/app-config-fragment-rbac-write-base branch from 269eb29 to d0a0c05 Compare July 13, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds repository-layer RBAC scope bindings for AppConfig fragments.

Changes:

  • Binds user/domain fragments to RBAC scopes; public fragments remain global.
  • Atomically removes fragment bindings during purge.
  • Extends RBAC resource registration and repository tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
changes/12801.feature.md Adds the feature changelog.
src/ai/backend/common/data/app_config/types.py Maps AppConfig scopes to RBAC elements.
src/ai/backend/common/data/permission/types.py Registers fragments as RBAC resources.
src/ai/backend/manager/repositories/app_config_fragment/db_source/db_source.py Adds scoped creation and unbinding.
src/ai/backend/manager/repositories/app_config_fragment/repositories.py Instantiates the RBAC ops provider.
src/ai/backend/manager/repositories/app_config_fragment/repository.py Updates provider and create interfaces.
src/ai/backend/manager/repositories/app_config_fragment/scope_binders.py Defines fragment scope-binding helpers.
src/ai/backend/manager/repositories/base/rbac/entity_creator.py Supports global entities without associations.
src/ai/backend/manager/repositories/base/rbac/scope_unbinder.py Supports global entity unbinding.
src/ai/backend/manager/repositories/ops/rbac/provider.py Adds transactional scope unbinding.
src/ai/backend/manager/services/app_config_fragment/service.py Passes creator specs directly.
tests/unit/manager/repositories/app_config_fragment/test_repository.py Tests fragment RBAC associations.
tests/unit/manager/services/app_config_fragment/test_service.py Updates creation delegation assertions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +114
all_scope_refs = [
ref for ref in (creator.scope_ref, *creator.additional_scope_refs) if ref is not None
]
Comment on lines +269 to +271
all_scope_refs = [
ref for ref in (creator.scope_ref, *creator.additional_scope_refs) if ref is not None
]
cls.ARTIFACT,
cls.ARTIFACT_REGISTRY,
cls.APP_CONFIG,
cls.APP_CONFIG_FRAGMENT,
Comment on lines +127 to +133
await w.unbind_scope_entities(
AppConfigFragmentScopeUnbinder(
fragment_id=data.id,
fragment_scope_type=data.scope_type,
fragment_scope_id=data.scope_id,
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:common Related to Common component comp:manager Related to Manager component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants