Skip to content

feat(BA-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type#12856

Open
jopemachine wants to merge 4 commits into
mainfrom
feat/BA-6886-register-app-config-fragment-rbac-entity
Open

feat(BA-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type#12856
jopemachine wants to merge 4 commits into
mainfrom
feat/BA-6886-register-app-config-fragment-rbac-entity

Conversation

@jopemachine

@jopemachine jopemachine commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Register APP_CONFIG_FRAGMENT as an RBAC resource type and backfill its write permissions onto existing write-capable roles, so later PRs in the stack can authorize fragment writes through RBAC (BEP-1052).

Split out of #12826 as its base: the entity-type registration and the permission backfill are independent of the per-fragment scope binding, and land together because a backfilled permission is meaningless unless the type is a resource type.

Changes

  • EntityType._resource_types() — add APP_CONFIG_FRAGMENT, so new roles receive its permissions at role creation (10 → 11 types)
  • rbac_models/migration/enums.py — same registration for the migration-local enum
  • b3d9f7a2c184 — backfill the permissions onto roles that predate the entity type

What the migration backfills

Only permissions rows, and only where a fragment can actually live:

Scopes touched user and domain only — a fragment lives at no other scope. public maps to ScopeType.GLOBAL and is granted to nobody: superadmin reaches it through the is_superadmin short-circuit in the RBAC validators, which needs no rows
Roles targeted a user's own user-scope role, and domain admins selected by capability — the role must hold domain_admin_page at that same domain (see below)
Operations the full owner operation set; permission carries the matching bitmask (c6648c039bd4 shape), grant operations store 0
Not migrated the scope associations (association_scopes_entities) — they are per-fragment and written at fragment-creation time (#12826). The feature is unreleased, so no fragments and no associations exist yet

ON CONFLICT DO NOTHING, so re-running grants nothing twice. downgrade is a no-op, following f2b9a4c7e103: a DELETE by entity_type cannot tell backfilled rows from the ones role creation now grants natively, so it would strip permissions that roles created since this revision legitimately hold.

Selecting admins by capability, not by name

The sibling backfills (f1a2b3c4d5e6, 30c8308738ee, …) select owner roles negatively, via NOT (role_name LIKE '%member'). That is unsafe: role names carry no enforced convention — CreateRoleInput.name is a free string and the caller picks the scope — so any domain-scoped role not named %member matches. The domain-viewer preset in test_create_preset_roles.py (read-only: VFOLDER:READ, SESSION:READ) is exactly that shape and would have received hard-delete and grant:all on fragments across the whole domain.

Those grant rows are not inert: the live check matches on permissions.operation (db_source.py), and the bitmask is the half that is currently unread.

So this migration selects admins positively instead:

WHERE rs.scope_type = 'user'
   OR EXISTS (SELECT 1 FROM permissions admin_page
              WHERE admin_page.role_id     = rs.role_id
                AND admin_page.entity_type = 'domain_admin_page'
                AND admin_page.scope_type  = rs.scope_type
                AND admin_page.scope_id    = rs.scope_id)
  • domain — every domain admin role holds domain_admin_page at its own domain by the time this runs, under both naming schemes: 3b6297b1bd75 covers role_domain_%_admin, f2b9a4c7e103/a3c1d8e5b294 cover domain-%-admin, and all three are ancestors of this revision. So no name matching is needed and the roles join drops out entirely. Matching scope_id too keeps an admin of domain A from being granted on domain B.
  • user — kept broad. A user-scope grant only ever reaches that user's own scope, and writing an own-scope fragment is deliberately not admin-only (see AppConfigScopeType.to_rbac_scope_type).
  • role_superadmin holds domain-scoped rows but no domain_admin_page, so it drops out. That is correct and matches 3b6297b1bd75: superadmin authority comes from the is_superadmin short-circuit, never from these rows.

Verified against a local DB (per models/alembic/AGENTS.md, data migrations must be checked against a real DB):

domain-scope role outcome
domain-testdom-admin (runtime naming) full ops ✅
role_domain_default_admin (legacy naming) full ops ✅
domain-auditors-readonly (read-only custom, the escalation) nothing
role_domain_default_member nothing ✅
role_superadmin nothing ✅

Backport

Not needed for 26.4 — the feature does not exist there:

main 26.4
app_config_fragment paths 36 0
create_app_config_fragments_table migration present absent
EntityType.APP_CONFIG_FRAGMENT present absent

26.4's 84d5c6daf8cc says it directly: the replacement tables "are introduced in a follow-up migration on top of this one". Backporting would fail to import EntityType.APP_CONFIG_FRAGMENT, and would grant permissions on a table that does not exist.

📚 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-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type #12856feat(BA-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type + permission backfill ← you are here
  13. feat(BA-6859): bind AppConfig fragments to their RBAC scope on write (repository layer) #12826feat(BA-6859): bind fragments to their RBAC scope on write (repository layer)
  14. fix(BA-6872): seed APP_CONFIG_FRAGMENT permissions into the RBAC role fixture #12837fix(BA-6872): seed APP_CONFIG_FRAGMENT permissions into the RBAC role fixture
  15. chore(BA-6873): drop the dead APP_CONFIG RBAC entity type #12839chore(BA-6873): drop the dead APP_CONFIG RBAC entity type
  16. feat(BA-6555): AppConfig merge engine + resolve service (service layer) #12359feat(BA-6555): AppConfig merge engine + resolve service (service layer)
  17. feat(BA-6556): AppConfig REST v2 API (raw fragments and merged read/update) #12377feat(BA-6556): AppConfig REST v2 API
  18. feat(BA-6860): RBAC-gate app_config fragment writes at the processors #12759feat(BA-6860): fragment write API routes + bulk write authz

Jira: https://lablup.atlassian.net/browse/BA-6886

🤖 Generated with Claude Code

Add APP_CONFIG_FRAGMENT to EntityType._resource_types() and to the RBAC
migration enum, and backfill its write permissions onto existing write-capable
roles (a user's own user-scope role and domain admins; member roles excluded).
New roles get these permissions at role creation, so the migration only covers
roles that predate the entity type (BEP-1052).

Split out of BA-6859 as its base: registration and the backfill are independent
of the per-fragment scope binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jopemachine jopemachine requested a review from a team as a code owner July 15, 2026 02:04
Copilot AI review requested due to automatic review settings July 15, 2026 02:04
@github-actions github-actions Bot added the size:L 100~500 LoC label Jul 15, 2026
@github-actions github-actions Bot added comp:manager Related to Manager component comp:common Related to Common component require:db-migration Automatically set when alembic migrations are added or updated labels Jul 15, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

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

Registers app config fragments as RBAC resources and backfills permissions for existing roles.

Changes:

  • Extends resource type registries and tests.
  • Adds an Alembic permission backfill.
  • Adds a changelog fragment.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/manager/rbac/test_permission_types.py Updates resource-type expectations.
tests/unit/manager/actions/test_action_types.py Updates action type tests.
src/ai/backend/manager/models/rbac_models/migration/enums.py Synchronizes migration enums.
src/ai/backend/manager/models/alembic/versions/b3d9f7a2c184_add_app_config_fragment_permissions_to_rbac.py Backfills RBAC permissions.
src/ai/backend/common/data/permission/types.py Registers the resource type.
changes/12856.feature.md Documents the feature.

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

Comment on lines +91 to +95
db_conn = op.get_bind()
db_conn.execute(
sa.text("DELETE FROM permissions WHERE entity_type = :entity_type"),
{"entity_type": EntityType.APP_CONFIG_FRAGMENT.value},
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is dangerous, let's not delete during downgrade migration

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.

I see in other PRs that added permission records that those records were removed during the downgrade. Are you suggesting that we leave downgrade as pass here instead?

Is there a particular reason for that?

jopemachine and others added 2 commits July 15, 2026 12:39
…role name

The backfill selected owner roles negatively, via `NOT (role_name LIKE
'%member')`. Role names carry no enforced convention — `CreateRoleInput.name`
is a free string and the caller picks the scope — so any domain-scoped role
not named `%member` matched. The `domain-viewer` preset (read-only, VFOLDER
and SESSION READ) is exactly that shape and would have received
`hard-delete` and `grant:all` on fragments across the whole domain. Those
grant rows are not inert: the live check matches on `permissions.operation`,
and the bitmask is the half that is currently unread.

Select admins positively instead, by the capability each domain admin role
provably holds:

* domain — require `domain_admin_page` at the same domain. Every domain admin
  role holds it under both naming schemes by the time this runs
  (3b6297b1bd75, f2b9a4c7e103/a3c1d8e5b294 are all ancestors), so the `roles`
  join drops out entirely.
* user — kept broad. A user-scope grant only reaches that user's own scope,
  and writing an own-scope fragment is deliberately not admin-only.

`public` (ScopeType.GLOBAL) is granted to nobody; superadmin reaches it via
the `is_superadmin` short-circuit in the RBAC validators, which needs no rows.
`role_superadmin` drops out for the same reason, matching 3b6297b1bd75.

Also make `downgrade` a no-op, as f2b9a4c7e103 does: a DELETE by entity_type
cannot tell backfilled rows from those role creation grants natively.

Verified against a local DB per models/alembic/AGENTS.md: domain admin roles
under both naming schemes receive the grant; the read-only custom role,
member roles, and `role_superadmin` receive nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Drop the revision-by-revision argument for why every domain admin role
holds ``domain_admin_page``, the ``permission`` bitmask provenance, and
the bullet structure. Keep only what the SQL cannot show: why the bits
are inlined, why admins are selected by capability instead of role name,
and why ``role_superadmin`` drops out.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
jopemachine added a commit that referenced this pull request Jul 15, 2026
The resource-type registration and the permission backfill migration moved to
BA-6886 (#12856), so this PR's fragment no longer claims them.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
jopemachine added a commit that referenced this pull request Jul 15, 2026
The resource-type registration and the permission backfill migration moved to
BA-6886 (#12856), so this PR's fragment no longer claims them.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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 require:db-migration Automatically set when alembic migrations are added or updated size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants