feat(BA-5681): auto-sync user-scope entries on role assign/unassign#10990
Open
feat(BA-5681): auto-sync user-scope entries on role assign/unassign#10990
Conversation
fregataa
added a commit
that referenced
this pull request
Apr 12, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds automatic synchronization of scope→user membership edges in association_scopes_entities when roles are assigned or revoked, so RBAC scope membership stays consistent with role assignments.
Changes:
- Add
sync_user_scope_on_assign()/sync_user_scope_on_revoke()helpers to maintain scope→user edges based on role-scope bindings. - Integrate the sync logic into single and bulk role assign/revoke flows.
- Add unit tests validating scope entry creation/removal behavior for assign/revoke scenarios.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py |
Adds scope-sync helpers and wires them into assign/revoke (including bulk) operations. |
tests/unit/manager/repositories/permission_controller/test_user_scope_sync.py |
New unit tests covering user-scope sync behavior for role assign/revoke cases. |
Comments suppressed due to low confidence (1)
src/ai/backend/manager/repositories/permission_controller/db_source/db_source.py:1202
- The new user-scope sync behavior is integrated into
bulk_assign_role()/bulk_revoke_role(), but the added unit tests only cover the single-userassign_role()/revoke_role()paths. Adding coverage for the bulk paths would help prevent regressions (e.g., partial-success batches, idempotency, and ensuring scope entries are synced for each successful row).
async def bulk_assign_role(
self, bulk_creator: BulkCreator[UserRoleRow]
) -> BulkCreatorResultWithFailures[UserRoleRow]:
async with self._db.begin_session() as db_session:
result = await execute_bulk_creator_partial(db_session, bulk_creator)
for row in result.successes:
await sync_user_scope_on_assign(
db_session, user_id=row.user_id, role_id=row.role_id
)
return result
async def bulk_revoke_role(
self, data: BulkUserRoleRevocationInput
) -> BulkRoleRevocationResultData:
successes: list[UserRoleRevocationData] = []
failures: list[BulkRoleRevocationFailure] = []
async with self._db.begin_session() as db_session:
for user_id in data.user_ids:
try:
async with db_session.begin_nested():
stmt = (
sa.select(UserRoleRow)
.where(UserRoleRow.user_id == user_id)
.where(UserRoleRow.role_id == data.role_id)
)
user_role_row = await db_session.scalar(stmt)
if user_role_row is None:
raise RoleNotAssigned(
f"Role {data.role_id} is not assigned to user {user_id}."
)
user_role_id = user_role_row.id
await db_session.delete(user_role_row)
await db_session.flush()
await sync_user_scope_on_revoke(
db_session, user_id=user_id, role_id=data.role_id
)
successes.append(
UserRoleRevocationData(
user_role_id=user_role_id,
user_id=user_id,
role_id=data.role_id,
)
)
except Exception as e:
log.warning(
"Failed to revoke role {} from user {}: {}",
data.role_id,
user_id,
str(e),
)
failures.append(BulkRoleRevocationFailure(user_id=user_id, message=str(e)))
return BulkRoleRevocationResultData(successes=successes, failures=failures)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
fregataa
added a commit
that referenced
this pull request
Apr 14, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
95e54db to
636fffc
Compare
fregataa
added a commit
that referenced
this pull request
Apr 14, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
636fffc to
2a0026b
Compare
fregataa
added a commit
that referenced
this pull request
Apr 17, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2a0026b to
547552c
Compare
fregataa
added a commit
that referenced
this pull request
Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fregataa
added a commit
that referenced
this pull request
Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fregataa
added a commit
that referenced
this pull request
Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fregataa
added a commit
that referenced
this pull request
Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fregataa
added a commit
that referenced
this pull request
Apr 18, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d4450c5 to
e3b9bd1
Compare
When a role is assigned or revoked, automatically sync the user's membership entries in association_scopes_entities based on the role's bound scopes. This ensures the scope chain CTE can discover user-scope paths for permission checks. - assign: INSERT ON CONFLICT DO NOTHING (idempotent) - revoke: DELETE only when no other role covers the same scope - Works for all scope types (project, domain, etc.) - Supports single and bulk assign/revoke operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e3b9bd1 to
3fa81e0
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
entity_id is a String(64) column that could theoretically hold non-UUID values. Log a warning and skip the row instead of crashing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t queries - Remove UserScopeBinding, _get_role_bound_scopes, _build_bindings - _sync_user_scopes_on_assign: single INSERT...SELECT joining user_roles to resolve scopes at execution time (no pre-fetch TOCTOU gap) - _sync_user_scopes_on_revoke: single DELETE with NOT EXISTS, no longer scoped to a specific role — cleans up any orphaned user-scope entries - Both methods accept user_ids only (role_id removed) - bulk_revoke_role calls sync once after loop instead of per-user - Add TODO comments for association_groups_users migration deprecation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssign Filter user_ids directly on user_roles JOIN instead of using a separate unnest CROSS JOIN. The user_roles table already provides the user_id column, making the subquery unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
association_scopes_entitieswhen roles are assigned or revokedON CONFLICT DO NOTHING)NOT EXISTScheck)assign_role(),revoke_role(),bulk_assign_role(), andbulk_revoke_role()Test plan
Resolves BA-5681