RBAC Admin Scopes¶
Status: ✅ Shipped — the full scope-based RBAC chain from ADR-0058 A-F2 (supersedes ADR-0027's flat
is_admingate) is live: 28 scopes,require_scope/require_all_scopesFastAPI dependencies wired across 32 route files,AdminScopeGrant/AdminActionLogschema, grant/revoke UI, and a daily HIGH_IMPACT review queue. This page is the feature-status home for the RBAC rollout — the schema itself is documented at../DATA_MODELS/admin.md.
RBAC replaced the flat User.is_admin == true gate (ADR-0027) with a per-admin scope grant model, greenlit as the RBAC foundation chain by ADR-0093 item 19. User.is_admin is now itself grant-derived — a correlated EXISTS against active AdminScopeGrant rows — rather than a plain settable column.
Scope catalog¶
28 scopes total: the 19 canonical scopes from ADR-0058 A-F2 verbatim, plus an 8-scope operational expansion Max-ruled 19→26 (audit/design-briefs/rbac-scope-expansion-2026-07-17.md), plus admin.audit.review (26→27, Phase E), plus admin.system.health_view (27→28). Source: services/gameserver/src/auth/admin_scopes.py (verified via grep -oE '"admin\.[a-z_.]+"' | sort -u, 2026-08-04).
Families: admin.players.* (view/suspend/adjust_rep/transfer_assets/adjust_credits), admin.subscriptions.* (view/modify/refund), admin.webhooks.* (view/replay), admin.regions.* (view/create/terminate/transfer_ownership), admin.aria.audit, admin.multi_account.review, admin.bang.regenerate, admin.scopes.* (grant/revoke), admin.audit.* (view/review), admin.galaxy.manage, admin.ships.manage, admin.combat.intervene, admin.economy.intervene, admin.security.act, admin.disputes.resolve, admin.system.health_view.
HIGH_IMPACT subset — scopes carrying material financial, access, or structural risk are flagged and their invocations surface to a daily review queue with a two-eyes retrospective acknowledgment requirement (admin.audit.review is itself not high-impact — reviewing isn't a high-impact action).
Enforcement¶
require_scope(scope)(auth/dependencies.py) — FastAPI dependency factory. Unknown catalog scopes raise at dependency-construction time (deploy-time signal against typos, never a silent always-deny). Fail-closed: any exception during the grant lookup returns 403 naming the missing scope — never 200, never a 500 leak.require_all_scopes(*scopes)— AND-composition ofrequire_scope, same fail-closed contract.user_has_active_scope(db, user_id, scope)— the underlying check: anAdminScopeGrantrow for that (user, scope) withrevoked_at IS NULL.- Wired across 32 route files as of this writing.
Rollout phases (all shipped)¶
| Phase | Scope | Status |
|---|---|---|
| A1 | AdminScopeGrant/AdminActionLog schema + migrations |
✅ Shipped |
| A2 | require_admin additive alongside the new scope checks (no sweep yet) |
✅ Shipped |
| B | Route-by-route sweep to require_scope; existence-check keyed on username, not raw is_admin SQL |
✅ Shipped |
| C0 | Shared AdminActionLog helper + first writes (planet/multi-account) |
✅ Shipped |
| C1/C2 | HIGH_IMPACT AdminActionLog wiring |
✅ Shipped |
| C2b | AdminActionLog on deferred-mutation routes |
✅ Shipped |
| C3 | User.is_admin derived from active scope grants (correlated EXISTS) |
✅ Shipped |
| D | Admin scopes + action log UI | ✅ Shipped |
| D1 | AdminActionLog list, scope-catalog descriptions, scope-holder lookup |
✅ Shipped |
| E | admin.audit.review + review-queue GET + retention policy + review UI |
✅ Shipped |
| E5 | HIGH_IMPACT first-cut wraps (money-path, GALAXY_MANAGE mutators, faction update/delete) |
✅ Shipped |
Mutation-attempt audit pattern (admin_action_attempt)¶
admin_action_attempt (services/gameserver/src/services/admin_action_attempt.py, RBAC Phase E-5) is a context manager that logs the success/blocked/failed outcome of an admin mutation without scattering ad-hoc log_admin_action calls through each route. A HIGH_IMPACT wrapped route (the E5_WRAPPED_ROUTES set in that file — scope grant/revoke, contract dispute resolution, player-credit/create routes, ship management, galaxy/sector/planet/port/warp-tunnel/faction mutators) should use it like:
with admin_action_attempt(db, actor=admin, scope_used=SCOPE, action="...",
target_type="...", target_id=str(id)) as attempt:
# perform the mutation (do NOT db.commit() yourself)
attempt.succeed()
Commit-boundary ownership — the helper owns it, not the route:
succeed()writes the success log row and callsdb.commit()itself, committing the mutation and the audit log together in one transaction. A route wrapped inadmin_action_attemptmust not calldb.commit()on its own path — finalizing to success only happens after that commit lands. If the commit itself fails,succeed()rolls back, writes a best-effortfailedlog row on a separate commit, and re-raises the original commit exception.- On an uncaught exception,
__exit__rolls back any uncommitted mutation side effects, then writes ablockedorfailedlog row (HTTP 400/403/404/409/422 →blocked, anything else →failed) and commits the log alone, best-effort — a failure to persist the log never masks or replaces the original business exception, which still propagates. - Failure reasons are sanitized (
sanitize_failure_reason) before persisting: secrets/tokens/passwords/API keys are redacted and the text is capped at 500 chars.
A future admin-route author adding a new HIGH_IMPACT mutation should add its route string to E5_WRAPPED_ROUTES and follow the succeed()-not-commit() pattern above rather than hand-rolling a commit + separate audit call.
Admin UI¶
Grant/revoke and the review queue are surfaced in the admin UI — see ./admin-ui.md for the frontend surfaces and ../DATA_MODELS/admin.md for the full AdminScopeGrant/AdminActionLog field schema.
Related¶
- ADR-0027 — the flat gate this replaced.
- ADR-0058 — the 19 canonical scopes + A-F2 rollout plan.
- ADR-0093 item 19 — greenlit the foundation chain.
../DATA_MODELS/admin.md— schema../admin-ui.md— admin-facing UI surfaces.