Skip to content

Admin & Audit

Status: ✅ Shipped — Admin model is fully implemented: User.is_admin, AdminCredentials, AuditLog with all columns, and per-resource inline admin flags all match the spec exactly … (impl audit 2026-06-16)

Admin authentication is a side table on User (AdminCredentials). Authorization is a fine-grained scope model per ADR-0058 A-F2: each admin capability is one of 19 canonical scopes, granted per-admin via the AdminScopeGrant join table, and every admin action is recorded in AdminActionLog. The User.is_admin boolean is a derived view (is_admin = EXISTS (active scope grant)), not the authorization gate. Beyond scope-gated capabilities, granular moderation state lives inline on the resource being administered (e.g., MarketTransaction.flagged_suspicious / reviewed_by, CombatLog.admin_resolved / admin_reviewed, Message.flagged / moderated_by, PriceAlert.acknowledged_by).


User (admin flag)

Source: services/gameserver/src/models/user.py

The is_admin: Boolean column on User is a derived convenience view — is_admin = EXISTS an active AdminScopeGrant for that user — so a plain is_admin check resolves to "holds any admin scope." It is not the authorization gate: capability checks read scopes (see AdminScopeGrant). See ./player.md for the full User schema. An admin user is one holding at least one active scope grant, with a populated AdminCredentials row for the admin login path.


AdminCredentials

Source: services/gameserver/src/models/admin_credentials.py

Purpose: Separate password hash for the admin login path, isolated from the OAuth/MFA flow used by ordinary users.

Fields:

name type constraints notes
user_id UUID FK users.id PK, CASCADE 1:1 with User
password_hash String(255) not null bcrypt/argon2
last_password_change DateTime server default now for rotation policy

Relationships: user (1:1, back-pop User.admin_credentials).


AdminScopeGrant

Source: services/gameserver/src/models/admin_scope_grant.py

Purpose: The authorization gate. One row per (admin user, scope) grant. An admin's capabilities are exactly the set of scopes they currently hold (revoked_at IS NULL). Capabilities are drawn from the canonical 19-scope set defined in ADR-0058 A-F2 — families admin.players.*, admin.subscriptions.*, admin.webhooks.*, admin.regions.*, admin.aria.audit, admin.multi_account.review, admin.bang.regenerate, admin.scopes.*, admin.audit.view.

Full field schema (id, user_id, scope, granted_by, granted_at, revoked_at, revoked_by) and indexes are in ./gameplay.md#adminscopegrant. The bootstrap superadmin is granted admin.scopes.grant + admin.scopes.revoke + admin.audit.view via a one-time migration.

User.is_admin is the derived view EXISTS (SELECT 1 FROM admin_scope_grants WHERE user_id = User.id AND revoked_at IS NULL).


AdminActionLog

Source: services/gameserver/src/models/admin_action_log.py

Purpose: Append-only record of every scope-authorized admin action, carrying the scope_used that authorized it. Drives the daily review queue that surfaces high-impact actions (admin.subscriptions.*, admin.webhooks.replay, admin.regions.terminate, admin.scopes.*) for retrospective acknowledgement by another admin holding admin.audit.view. Distinct from the catch-all HTTP AuditLog below: AuditLog is the request-trail emitted by middleware, while AdminActionLog is the scope-keyed action record.

Full field schema (admin_user_id, scope_used, action, target_type, target_id, payload_snapshot, result, failure_reason, reviewed_by, reviewed_at, at) and indexes are in ./gameplay.md#adminactionlog.


AuditLog

Source: services/gameserver/src/models/audit_log.py

Purpose: Catch-all request/audit trail emitted by middleware. Used for security/compliance review and admin search.

Fields:

name type constraints notes
id UUID PK
timestamp DateTime default utcnow, indexed
method String(10) not null HTTP verb
path String(255) not null, indexed
status_code Integer nullable
duration_ms Integer nullable
user_id UUID nullable, indexed no FK constraint (logs survive user deletes)
user_type String(20) nullable admin/player/anonymous
client_ip String(45) not null IPv6-safe
user_agent Text nullable
action String(100) indexed login/logout/economy_intervention/…
resource_type String(50) nullable auth/ship/economy/message/…
resource_id UUID nullable affected resource id
query_params, request_body, security_flags JSON nullable request body should be sanitized of secrets
response_summary Text nullable
violation_detected String(100) nullable violation classifier

Relationships: none (deliberately decoupled — no FK).


Per-resource admin flags

Several entities carry inline admin / moderation columns rather than living in a centralized permissions table. These are the surfaces an admin UI hits:

Entity Admin columns Source
MarketTransaction (enhanced_market_transactions) admin_notes, flagged_suspicious, reviewed_by (FK users.id) market_transaction.py
CombatLog admin_notes, admin_resolved, admin_resolved_at, disputed, resolved, admin_reviewed combat_log.py
Message flagged, flagged_reason, moderated_at, moderated_by (FK users.id) message.py
PriceAlert acknowledged_by (FK users.id), acknowledged_at, resolved_at market_transaction.py
Reputation is_locked, lock_reason, lock_expires, special_status reputation.py

Indexes on audit_logs.timestamp, audit_logs.path, audit_logs.user_id, and audit_logs.action cover the common admin-search queries. Composite indexes for user-activity searches are defined inline.


Authorization surface

Authorization is fine-grained scopes, not roles. Middleware/routes gate each admin endpoint on a specific scope (e.g., admin.regions.terminate); a request lacking the scope returns 403 with the missing scope name in the body. Scope membership is read from active AdminScopeGrant rows. There is no roles enum and no role hierarchy — operators dial each admin's capabilities up or down one scope at a time. This is canonical per ADR-0058 A-F2, which supersedes ADR-0027 (whose flat-is_admin model scoped the pre-0058 admin surface). Per-resource moderation flags (the inline columns above) sit alongside the scope set: scopes gate who may invoke a capability, while the inline flags carry per-record moderation state for the entity being administered.