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