Skip to content

RBAC Admin Scopes

Status: ✅ Shipped — the full scope-based RBAC chain from ADR-0058 A-F2 (supersedes ADR-0027's flat is_admin gate) is live: 28 scopes, require_scope/require_all_scopes FastAPI dependencies wired across 32 route files, AdminScopeGrant/AdminActionLog schema, 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 of require_scope, same fail-closed contract.
  • user_has_active_scope(db, user_id, scope) — the underlying check: an AdminScopeGrant row for that (user, scope) with revoked_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 calls db.commit() itself, committing the mutation and the audit log together in one transaction. A route wrapped in admin_action_attempt must not call db.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-effort failed log 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 a blocked or failed log 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.