Skip to content

0058 — Monetization, admin authorization, and webhook security (Group M)

Status

Folded into OPERATIONS/monetization.md (A-D3 webhook validation incl. code-wins env-var correction, A-F3 takeover-race serialization), OPERATIONS/rbac-admin-scopes.md + DATA_MODELS/admin.md (A-F2 + Amendment A: 28-scope catalog, is_admin hybrid_property, admin.audit.review, grant-only create-admin — verified against auth/admin_scopes.py), and SYSTEMS/aria-dialogue.md (A-F1 auth_snapshot) — defer to those pages for the canonical description (2026-08-04, re-verified). No genuine code gap found. Supersedes ADR-0027 on the admin authorization model. (re-verified 2026-08-06)

Context

Five audit findings clustered on the auth/billing/admin perimeter (R-V3 closed separately under Group J's worldgen cleanup). The four remaining sat on three orthogonal surfaces: webhook security — PayPal webhook signature validation had an env-var bypass and a log-only failure path, either of which alone lets a forged webhook commit subscription mutations (A-D3); admin authorization — the flat User.is_admin boolean gave a compromised admin account unbounded blast radius (A-F2); and consistency/serialization — subscription-tier upgrades mid-ARIA-dialogue produced inconsistent multiplier values (A-F1), and concurrent multi-regional takeover claims had no documented serialization, risking escrow held without a corresponding ownership state (A-F3). This ADR made webhook validation mandatory and binding with zero production bypass code (A-D3), replaced the flat boolean with a fine-grained scope-based authorization model backed by an audit log + review queue instead of a two-person rule (A-F2), snapshotted subscription tier at ARIA dialogue start (A-F1), and serialized takeover claims through a SELECT FOR UPDATE lock on the Region row with escrow refund inside the same transaction as the loss decision (A-F3).

Folded into: - OPERATIONS/monetization.md — A-D3, A-F3 - OPERATIONS/rbac-admin-scopes.md + DATA_MODELS/admin.md — A-F2 + Amendment A - SYSTEMS/aria-dialogue.md — A-F1 auth_snapshot

  • ADR-0017 — consciousness-level scale (snapshot rule applies at dialogue start).
  • ADR-0050TakeoverIntent row lifecycle.
  • ADR-0053 — periodic-service surface used by audit-log retention sweep.
  • ADR-0056MultiAccountCluster review queue (consumes admin.multi_account.review).
  • ADR-0057 — ARIA security log (consumes admin.aria.audit).
  • ../OPERATIONS/admin-ui.md — admin app, scope-management UI, audit/review pages.
  • ../DATA_MODELS/gameplay.mdAdminScopeGrant, AdminActionLog, processed_webhook_events, TakeoverIntent state machine extension.

Amendment A — scope-catalog expansion, create-admin ruling, and the is_admin authoritative flip (2026-07-17)

Status: Ratified by Max (2026-07-17), building out A-F2 above. Additive; A-D3/A-F1/A-F3 and the original 19-scope launch-list table are unchanged and stand as the historical launch record.

Change — scope catalog 19 → 27. The RBAC build-out's route-coverage sweep found the original 19 platform scopes cover players/subscriptions/webhooks/regions/aria/multi-account/bang/scopes/audit, but have no home for the operational admin surface — galaxy, ship, combat, economy, and security mutation routes were falling back to the read-only admin.players.view. Seven fine-grained capability scopes were added (admin.galaxy.manage, admin.players.adjust_credits, admin.ships.manage, admin.combat.intervene, admin.economy.intervene, admin.security.act, admin.disputes.resolve), plus an eighth building out the existing review-queue mechanic (admin.audit.review). Fine-grained-not-roles is preserved: read-only operational GETs stay on admin.players.view; only mutating/destructive operational routes require one of the new capability scopes.

Change — the review-queue's high-impact set grows to also include admin.galaxy.manage, admin.players.adjust_credits, admin.ships.manage, and admin.disputes.resolve — the four new scopes carrying unbounded-mint or structural-destruction blast radius. admin.combat.intervene, admin.economy.intervene, and admin.security.act are not high-impact. admin.audit.review (the meta-scope gating the queue) is also not high-impact.

Change — review-queue acknowledgement requires admin.audit.review, not admin.audit.view. admin.audit.view grants read access to the audit trail; clearing an item off the daily review queue requires the dedicated admin.audit.review scope.

Change — create-admin is grant-only; no 20th scope. Admin-hood is granted, not minted. POST /users/admin (a raw is_admin = True mint with zero grant row and zero audit-log entry) is retired, returning 410 Gone and pointing callers at POST /admin/scopes/grant. The boot-time bootstrap grants the 3 META_SCOPES as AdminScopeGrant rows in the same transaction as the bootstrap user, rather than only setting the flat flag.

Change — is_admin is the SQL-derived-authoritative gate. User.is_admin is now a SQLAlchemy hybrid_property whose authoritative read — both the Python getter and the SQL .expression used at every filter site — is the correlated EXISTS(AdminScopeGrant WHERE revoked_at IS NULL) predicate. The flat users.is_admin column persists only as a denormalized cache for detached-instance reads; no route or filter site reads it as authoritative.

This ADR's Accepted status and the A-D3/A-F1/A-F3 decisions are unchanged; Amendment A extends only the A-F2 scope model.