Raxx · internal docs

internal · gated

RBAC V2 — Migration Plan

Status: Design
Date: 2026-05-09 UTC
Owner: software-architect
Refs: design.md, api-contract.md, ADR-0054 through ADR-0057


Current state (as of 2026-05-09)

Migration 0012 landed and is on main. It created the seven RBAC v2 tables and seeded 24 roles, 4 groups, 18 permissions, plus inheritance edges and group-role assignments.

The require_rbac_role decorator is live in Console middleware and used on billing routes (console-billing-read).

The require_role (flat admin_roles) decorator is still the primary gate on all other Console blueprints (17 blueprints).

Raptor has no RBAC code. Product-user access is governed by users.role ('user'|'admin').


Phase 0 — Schema additions (migration 0021)

Status to deliver: FLAG_RBAC_V2 present in console_flag_promotions table (required by I-11).

Migration file: console/migrations/versions/0021_rbac_v2_additions.py

Contents: 1. Create rbac_grants_audit (append-only, DDL REVOKE applied). 2. Create rbac_ticket_grants. 3. Create rbac_break_glass_sessions. 4. Seed four audit roles (antlers-audit-self, raptor-audit-support, raptor-audit-admin, raptor-audit-compliance) and four audit permissions. 5. Assign raptor-audit-support to raxx-support-team group. 6. Assign raptor-audit-admin to raxx-platform-admins group. 7. Insert FLAG_RBAC_V2 into console_flag_promotions with risk=high, current_env=staging, promoted_by=migration.

Rollback: Drop the three new tables. The seeded roles and permissions have no side effects if left; the audit role group assignments can be removed in the down() migration.

Dev-days estimate: 1.5 (schema + seed + migration test)

Prerequisite cards: None. This is the root dependency.


Phase 1 — Grant/revoke API (RV-1, RV-2)

Gate: Migration 0021 merged and migrated on staging.

Scope: - Implement POST /api/rbac/grants, DELETE /api/rbac/grants/<grant_id> (RV-2) - Implement POST /api/rbac/grants/ticket-scoped (RV-4 dependency) - The pre-write audit pattern: audit INSERT before grant INSERT; transaction-scoped - Self-grant prohibition check - Break-glass passkey re-auth challenge - Session cache invalidation on grant/revoke

Flag gate: FLAG_RBAC_V2 — the grant endpoints are only reachable when the flag is on. They return 404 when the flag is off.

Dev-days estimate: 3 (service layer + endpoint + unit tests + integration test)


Phase 2 — Reader endpoints (RV-3)

Gate: Phase 1 merged on staging.

Scope: - GET /api/rbac/me — effective roles + permissions + ticket grants - GET /api/rbac/permissions/check — single-permission check (used by SC-A8) - GET /api/rbac/grants/audit — paginated grant/revoke history - GET /api/rbac/roles, GET /api/rbac/groups — read-only registry views

Session cache design: On login, if FLAG_RBAC_V2 is on, resolve effective permissions and embed as JSONB column on the session record. Cache is invalidated on any grant/revoke for this user.

Dev-days estimate: 2


Phase 3 — Ticket-scoped grants + auto-revoke (RV-4)

Gate: Phase 1 merged. FreeScout integration operational.

Scope: - POST /api/rbac/grants/ticket-scoped endpoint - Background job (Heroku Scheduler, 2-minute interval): poll rbac_ticket_grants WHERE revoked_at_utc IS NULL and check FreeScout ticket status. On RESOLVED/CLOSED: soft-revoke + audit row + session invalidation. - Alternative (per OQ-2 in design.md): FreeScout webhook receiver. Decision by operator before this card is claimed. - Per-request ticket re-validation in the dim-3 check path (fail-closed on FreeScout unavailability).

Dev-days estimate: 3 (includes FreeScout API client work; more if webhook receiver is chosen)


Phase 4 — Dual-mode middleware (RV-5)

Gate: Phases 1–3 merged on staging.

Scope: - FLAG_RBAC_V2 on staging: Console middleware calls both require_role (old) AND require_rbac_role (new) for every request. - Drift reporter: if the two systems disagree on allow/deny, log a structured warning to Sentry. Never block based on disagreement — old path is the enforced path during dual-mode. - Purpose: surface any seeding gaps or inheritance errors before cutover. - Run 7-day soak on staging with drift reporter active before Phase 5.

Dev-days estimate: 1.5


Phase 5 — Raptor blueprint cutover (RV-6)

Gate: Phase 4 soak complete with zero drift events for 48h on staging; FLAG_RBAC_V2 on prod staging soak.

Scope: - Replace @require_role("admin") or users.role checks in Raptor blueprints with @require_permission("raptor:audit:read-*") or @require_rbac_role("raptor-audit-*") where applicable. - Raptor's audit reader endpoint (SC-A8 from audit v2) is the primary new endpoint needing RBAC; all existing Raptor admin endpoints continue to use the Console JWT pattern unchanged. - Add Raptor service call to GET /api/rbac/permissions/check before serving audit data (or inline equivalent if Raptor and Console are on the same Postgres — see OQ-1 in design.md).

Dev-days estimate: 2


Phase 6 — Console blueprint cutover (RV-7)

Gate: Phase 5 merged and soaked.

Scope: - Replace all @require_role(...) decorators in Console blueprints with @require_rbac_role(...) or @require_permission(...). - 17 blueprints; see current blueprint table in auth-unification-rbac-reconciliation.md §3.1 for the role-by-role mapping. - Legacy admin_roles table is NOT dropped in this phase — only the middleware callsites change. - Remove the dual-mode drift reporter (was for transition only).

Dev-days estimate: 3 (17 blueprints, each requires mapping old role → new permission name; many are straightforward substitutions)


Phase 7 — Console UI (RV-8, RV-9, RV-10)

Gate: Phase 2 (reader API) merged.

Scope: - RV-8: Roles page + Groups page + Grants page (see design.md §8.1–8.3) - RV-9: Per-customer ticket-scoped grant flow (see design.md §8.4) - RV-10: Grants audit timeline (see design.md §8.5)

These can be built in parallel with Phases 5–6.

Dev-days estimate: 5 (3 pages + modal flows + audit timeline; hand wireframes to ux-designer for polish pass)


Phase 8 — Break-glass role + rotation (RV-12)

Gate: Phase 1 merged.

Scope: - Break-glass session flow: passkey re-auth challenge, justification input, session duration input. - rbac_break_glass_sessions writer + expiry checker. - Nightly Heroku Scheduler job: snapshot break-glass group roles; diff against previous snapshot; alert on changes outside deploy events. - Ops alert to ops@raxx.app before break-glass session is created (Postmark or Slack; Postmark approved out of sandbox 2026-05-09).

Dev-days estimate: 2


Phase 9 — raptor-audit-compliance role (RV-13)

Gate: Phase 6 merged. SOC-2 scoping begun.

Scope: - Create a raxx-compliance-auditors group (empty at creation). - Assign raptor-audit-compliance to the group. - Document the provisioning procedure: add auditor email to Console admins, add to raxx-compliance-auditors group. - No code change required at SOC-2 time — only group membership assignment.

Dev-days estimate: 0.5 (migration row + group creation + runbook)


Phase 10 — CI gate: no old require_role (RV-14)

Gate: Phase 7 complete.

Scope: - Add a CI lint job: grep -r '@require_role' console/app/blueprints/ --include="*.py" exits non-zero if any match found. - The job runs on every PR that touches console/app/blueprints/ or console/app/middleware/. - Equivalent lint for Raptor: grep -r 'users.role ==' backend_v2/api/routes/ --include="*.py" for hardcoded role checks.

Dev-days estimate: 0.5


Phase 11 — Drop legacy admin_roles (post-30d soak)

Gate: Phase 7 soaked for 30 days on prod with zero regressions.

Scope: - Migration: DROP TABLE admin_roles (after backup). - Remove Admin.has_role(), Admin.primary_role(), _ROLE_HIERARCHY from models/admin.py and middleware/rbac.py. - Remove require_role decorator from middleware.

Rollback: Restore from DB backup. This is the only destructive step. Pre-migration backup is mandatory.

Dev-days estimate: 1 (mostly deletion + smoke test confirmation)


Phase 12 — SAML wiring (RV-11 — documented for post-launch)

SAML is not in v1 scope. The design is SAML-ready per design.md §7. When scoped: - Add idp_group_name TEXT column to rbac_groups. - On SSO login, read SAML groups assertion, look up each name in rbac_groups.idp_group_name (not rbac_groups.name to avoid collision), add session-scoped memberships. - Alert on unmapped IdP group names; fail-closed (no auto-create).

Dev-days estimate (when scoped): 3


Sub-Cards to File

These card bodies define the implementation-sized work. Operator will issue the file command after design review.

Card Title Phase Est. dev-days Critical path for audit v2?
RV-1 schema(console): rbac_grants_audit + rbac_ticket_grants + rbac_break_glass_sessions + audit role seeds (migration 0021) 0 1.5 Yes — SC-A8 depends on these roles existing
RV-2 feat(console): POST /api/rbac/grants + DELETE /api/rbac/grants/<id> — grant/revoke API with pre-write audit 1 3 No (blocks UI; not audit v2 blocker)
RV-3 feat(console): GET /api/rbac/me + GET /api/rbac/permissions/check — reader endpoints + session-embedded cache 2 2 Yes — SC-A8 calls /permissions/check
RV-4 feat(console): POST /api/rbac/grants/ticket-scoped + auto-revoke on FreeScout ticket close 3 3 Yes — dim-3 ticket gate in SC-A8
RV-5 feat(console): dual-mode RBAC middleware — parallel old+new resolution with drift reporter 4 1.5 No (staging validation)
RV-6 refactor(raptor): cut over audit reader (SC-A8) and admin blueprints to require_rbac_role / require_permission 5 2 Yes — SC-A8 needs raptor-audit-* roles
RV-7 refactor(console): cut over all 17 console blueprints from require_role to require_rbac_role / require_permission 6 3 No (after RV-6)
RV-8 feat(console): RBAC management UI — Roles, Groups, and Grants pages 7 4 No
RV-9 feat(console): per-customer ticket-scoped grant flow on customer detail page 7 1 No
RV-10 feat(console): Grants audit timeline page (/console/rbac/grants/audit) 7 1 No
RV-11 docs(arch): SAML claims-to-groups wiring — documented for post-launch; migration 0021 adds idp_group_name stub 12 0.5 No
RV-12 feat(console): break-glass session flow — passkey re-auth, justification, duration cap, nightly snapshot alert 8 2 No
RV-13 feat(console): raptor-audit-compliance role + raxx-compliance-auditors group + provisioning runbook 9 0.5 No (post-SOC-2 scope)
RV-14 ci(lint): require_role callsite gate — fail build if @require_role found in blueprints post-cutover 10 0.5 No

Total dev-days estimate (v1 scope, excluding SAML, break-glass polish, and compliance): ~23 days

This breaks down as: - Critical path for audit v2 (RV-1, RV-3, RV-4, RV-6): ~8.5 dev-days - Everything else including console cutover and UI: ~14.5 dev-days

v1 cutover minimum (schema → dim-3 gate working): RV-1 + RV-3 + RV-4 + RV-6 = 8.5 dev-days, achievable within the 2026-05-23 milestone.


Dependency graph

graph LR
    RV1["RV-1\nSchema (migration 0021)"]
    RV2["RV-2\nGrant/revoke API"]
    RV3["RV-3\nReader endpoints"]
    RV4["RV-4\nTicket-scoped grants"]
    RV5["RV-5\nDual-mode middleware"]
    RV6["RV-6\nRaptor cutover"]
    RV7["RV-7\nConsole cutover"]
    RV8["RV-8\nConsole UI"]
    RV9["RV-9\nTicket grant UI"]
    RV10["RV-10\nGrants audit timeline"]
    RV12["RV-12\nBreak-glass flow"]
    RV14["RV-14\nCI lint gate"]
    SCA8["SC-A8\n(audit v2)"]

    RV1 --> RV2
    RV1 --> RV3
    RV1 --> RV4
    RV2 --> RV5
    RV3 --> RV5
    RV4 --> RV5
    RV5 --> RV6
    RV5 --> RV7
    RV6 --> SCA8
    RV3 --> SCA8
    RV4 --> SCA8
    RV2 --> RV8
    RV3 --> RV8
    RV4 --> RV9
    RV3 --> RV10
    RV7 --> RV14
    RV1 --> RV12

Critical path for audit v2 SC-A8: RV-1 → RV-3 → RV-4 → RV-6 → SC-A8