Skip to content

[Admin] Foundation: access control gating + admin-action audit log #28

Description

@serge-ivo

[Admin] Foundation: access control gating + admin-action audit log

Why

requireAdmin() (workers/api/src/lib/auth.ts) already checks session.roles.includes("admin") but is wired to no routes. Before any admin API ships we need: (a) a documented, tested way to grant/verify the admin role, (b) a lightweight probe the admin UI can call, and (c) an audit log so every privileged mutation is recorded (a gap PAS explicitly still has).

Scope

  • Add GET /v1/admin/me (or /v1/me/is-admin) → { admin: boolean }, behind requireUser (not requireAdmin, so non-admins get false not 403). The UI uses this to decide whether to mount the portal.
  • Confirm/derive admin role: users.roles JSON column, read at sign-in (routes/auth.ts upsertUser). Provide a safe way to grant admin — a migration seeding the operator account (serge.the.dev@gmail.com / operator GitHub id) as admin, plus a documented ADMIN_ALLOWLIST env fallback (comma-separated user ids or logins) checked inside requireAdmin so the very first admin isn't a chicken-and-egg problem.
  • New admin_audit_log table (migration): id, actor_user_id, action, target_type, target_id, detail_json, created_at. Add a recordAdminAction(env, session, action, target, detail) helper in lib/.
  • Wire the helper into a shared admin route middleware so every mutating /v1/admin/* call is logged.

Acceptance criteria

  • GET /v1/admin/me returns correct {admin} for admin and non-admin sessions.
  • requireAdmin honors both users.roles and the ADMIN_ALLOWLIST env fallback; unit-tested for allow + 403 deny.
  • Migration seeds the operator as admin and is idempotent.
  • admin_audit_log migration + helper; a mutating admin call writes exactly one row with actor + target + action.
  • Non-admin hitting any /v1/admin/* mutating route → 403 and no audit row.

Relevant code

  • workers/api/src/lib/auth.ts (requireAdmin, requireUser)
  • workers/api/src/routes/auth.ts (upsertUser, roles → session)
  • workers/api/migrations/ (next number after 0048)

References

  • FAS requireAdmin via ADMIN_GITHUB_LOGINS/ADMIN_USER_IDS (fas/platform/packages/backend/src/lib/auth.ts)
  • PAS dual model + its missing audit trail (pas/.../submissions-helpers.ts)

Part of the PAGS Admin Portal epic. Blocks all other admin issues.


Best-practice hardening (2026-08-01)

  • Defense-in-depth auth (not role-claim alone). A portal with read access to every tenant must not be guarded only by an HS256 role claim. Add a Cloudflare Access perimeter in front of both the /admin UI and /v1/admin/* API: a cloudflareAccessGate() middleware verifies the Cf-Access-Jwt-Assertion header against the team JWKS (RS256), inert until CF_ACCESS_TEAM_DOMAIN + CF_ACCESS_AUD are set (same pattern as FAS verifyAccessJwt). Role check runs behind it. Document turning CF Access on for admin.*//admin before real onboarding.
  • Live role resolution (no stale-token lockout / no re-login lag). requireAdmin resolves admin from: session role → live users.roles DB readADMIN_ALLOWLIST env (break-glass, by uid). So a freshly-granted admin ([Admin][API] Moderation — suspend user, unpublish/delete agent, cancel instance, roles/keys #34) takes effect immediately and a role revocation isn't defeated by an old token.

Metadata

Metadata

Assignees

No one assigned

    Labels

    admin-portalPAGS admin/operator portalenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions