Status: Accepted
Date: 2026-05-03
Deciders: Kristerpher (product owner), software-architect
Scope: CF Access group taxonomy + app-layer RBAC integration for all operator surfaces
Design doc: docs/architecture/auth-unification-rbac-reconciliation.md
Does NOT supersede: ADR-0042 (sibling ADR; extends it)
Extends: ADR-0020, ADR-0042
Refs: Epic #146 (console operator admin console); Velvet v2 epic #907; NV10 (#954); DQ-4 resolved 2026-05-03 ~08:00 UTC
ADR-0042 established Google Workspace as the IDP for all operator surfaces via CF Access Zero Trust. It left open DQ-4: the CF Access group taxonomy. The prior proposal in auth-unification.md §5.3 defined four flat CF Access groups (ops, superadmin, developer, auditor). Kristerpher rejected that taxonomy as incompatible with the fine-grained <app>-<resource>-<level> model specified in rbac-design.md and ADR-0020.
The reconciliation question: CF Access JWTs carry group claims at the network edge, but Raxx's canonical authz namespace uses fine-grained role names internal to each app. The two layers must be connected without duplicating policy state or tying Workspace group management to every app's release cycle.
The CF Access group taxonomy uses four team-function groups — raxx-platform-admins, raxx-support, raxx-devops, raxx-break-glass — not fine-grained role names. These groups answer the question "is this person a Raxx operator of a given function?" at the network edge. They do not answer "can this person perform console:secrets:rotate?" That question is answered entirely by the app-layer.
groups claim is advisoryThe CF JWT groups claim is read by the app layer for two advisory purposes only:
1. raxx-break-glass in claims triggers a mandatory audit alert before the passkey step.
2. Coarse surface filtering (UI optimization before role resolution completes).
The authoritative permission decision is always the user_groups → group_roles → role_permissions → role_inheritance DAG walk from rbac-design.md §7. A permission check that relies solely on the JWT groups claim and skips the DAG walk is a security defect.
<app>-<resource>-<level> naming is the canonical role namespaceAll Raxx roles follow the pattern from rbac-design.md §3.1. CF Access group names and Workspace group names never appear in permission checks. The mapping is:
Workspace group → CF Access Group → [advisory JWT claim for edge routing]
Workspace group → Raxx internal group (same name, different system) → roles → permissions
The Workspace group name and the Raxx internal group name may share the same string (e.g., both are raxx-platform-admins) but are independently managed systems.
An operator whose CF JWT carries raxx-platform-admins but who has no row in user_groups for the Console is permitted past the CF edge but receives 403 at the first authenticated route. CF Access group membership is a necessary but not sufficient condition for app-layer access. This upholds ADR-0042 D5 (no auto-provisioning via Google OAuth).
The NV10 yaml-driven revocation auth gate references group (Raxx internal group name), role (Raxx role name), or single-user (admin_id UUID). It never references CF Access group names or Workspace group emails.
single-user gate is an exception to I2, permitted only for break-glass scenariosADR-0020's invariant (no direct permissions on users) is upheld for all routine access. The single-user option in NV10 is permitted exclusively for emergency break-glass gate configurations. Every use must generate a mandatory audit event. Routine use of single-user gates is prohibited.
raxx-platform-admins, raxx-support, raxx-devops, raxx-break-glass) are far fewer than the ~20 groups that full <app>-<resource>-<level> replication in CF Access would require. Workspace admin stays clean.<app>-<resource>-<level> role taxonomy is fully internal to Raxx. Adding a new fine-grained role requires no Workspace group creation and no CF policy change.groups claim drift. If the Workspace group sync to CF Access has delay (up to 1 hour for non-SCIM), a newly added operator may pass the Workspace IDP login but have stale JWT groups. The app-layer role check is the safety net — but for the advisory surface-filtering use of the JWT claim, the UI may show the wrong subset for up to 1 hour. Acceptable at current team size.admin_roles table coexists with RBAC v2 tables during migration. The transition period (Phases 0–3) has two role-resolution paths behind a feature flag. Both paths must be maintained in rbac.py until Phase 4. Tech debt is explicit and time-bounded (30-day soak then drop).user_groups rows that join to admin_id (covered by existing DSR tooling).<app>-<resource>-<level> namespaceRejected. Requires ~20 Workspace groups; ties group management to every role addition or rename; exposes Raxx's internal permission taxonomy to an external system (violates I7 from the reconciliation doc); breaks SAML readiness (IdP would need to know role names, not just team-function group names). See design doc §5 Option 2 for full analysis.
Rejected as suboptimal, not as incorrect. Option 1 (design doc §5) is operationally correct but wastes the information that CF Access can carry. The raxx-break-glass alerting use-case (D2 above) is valuable and requires the JWT groups claim. Without it, break-glass access generates no early warning until after passkey authentication completes.
Not rejected — this is a future upgrade path. At current team size, 1-hour eventual consistency on group membership changes is acceptable. SCIM can be added when the team grows and real-time revocation latency matters. The group taxonomy and policy design are compatible with SCIM; no architectural change is needed to adopt it later.