RBAC V2 — SAML Group-to-Group Wiring Design
Status: Design — documented for post-launch; no v1 implementation
Date: 2026-05-12 UTC
Owner: software-architect
Refs: #1477 (this card), #1464 (RBAC V2 design PR), design.md, saml-claims-to-groups.md (full implementation spec), migration-plan.md §Phase 12
Milestone: Post-launch (not v1)
Purpose
This document satisfies the acceptance criteria for #1477: it describes the design for wiring SAML identity-provider group claims to Raxx RBAC V2 groups. It covers the assertion flow, the idp_group_name column, fail-closed behavior, and operator provisioning.
For the full implementation specification — including the sequence diagram, audit trail requirements, security checklist, SAML library candidates, and open questions — see saml-claims-to-groups.md in this directory.
1. Scope and Non-Goals
In scope for this document:
- How SAML assertion
groupsclaims map torbac_groupsrows - The
idp_group_namecolumn: which migration adds it, what it contains - Operator provisioning steps to wire an IdP group to an RBAC group
- Fail-closed behavior for unmapped group claims
- Precedence when a user has both a SAML-sourced group membership and a direct RBAC grant
- Default-member behavior when no group claim matches
Not in scope (all post-launch):
- Any SAML ACS endpoint code or SP metadata endpoint
- Choice of SAML library (deferred to implementation time)
- Customer (Antlers) authentication — passkeys only; SAML never touches the product-user auth path
- Any change to the Console WebAuthn login path
2. Invariants
These constraints are non-negotiable. Any implementation that violates one must stop and escalate.
| # | Invariant |
|---|---|
| I-1 | No stored assertions. The SAML response document and assertion XML are validated and discarded. Only the derived group memberships are persisted. Nothing in storage can be used to replay authentication. |
| I-2 | Console operators only. SAML applies exclusively to Console operator and partner logins. It never applies to product-user (Antlers) authentication, which is WebAuthn-only. |
| I-3 | Fail-closed. If no IdP group name in the assertion matches any rbac_groups.idp_group_name row, the session receives zero RBAC permissions. No group is auto-created from the assertion. |
| I-4 | Audit trail pre-write. Every SAML-sourced group membership grant or revoke writes a row to rbac_grants_audit (event_type='saml_grant' or 'saml_revoke') before the rbac_user_groups change is committed. If the audit write fails, the membership change is rejected. |
| I-5 | No role names in assertions. The IdP emits group names only. Role resolution happens entirely inside Raxx using the rbac_groups → rbac_group_roles → rbac_roles path. The IdP has no knowledge of the <app>-<resource>-<level> taxonomy. |
| I-6 | All timestamps UTC. |
| I-7 | Secrets in infra. SAML SP private key, IdP metadata URL, and entity IDs live in Infisical (/saml/ path) or SSM. Never in files that ship. |
3. The idp_group_name Column
3.1 Current state
Migration 0021 (0021_rbac_v2_additions.py, PR #1500) adds the three RBAC V2 audit tables and audit role seeds. It does not add idp_group_name to rbac_groups. The RBAC V2 design (design.md §7) always described this as a future migration addition, added only when SAML is actively scoped.
Confirmation: the idp_group_name column is not present in rbac_groups as of v1. This is expected and correct — the column provides no value until an IdP integration exists.
3.2 Migration to add the column (post-launch)
When SAML is scoped, the implementing developer adds a migration (next available version number at that point) containing:
ALTER TABLE rbac_groups
ADD COLUMN idp_group_name TEXT UNIQUE;
CREATE UNIQUE INDEX uq_rbac_groups_idp_group_name
ON rbac_groups (idp_group_name)
WHERE idp_group_name IS NOT NULL;
- Column is nullable.
NULLmeans the group has no IdP mapping. - Partial unique index prevents two Raxx groups from claiming the same IdP group name.
- All existing rows get
idp_group_name = NULL. No data changes. - Rollback:
ALTER TABLE rbac_groups DROP COLUMN idp_group_name.
The same migration must also add 'saml_grant' and 'saml_revoke' to the ck_rga_event_type check constraint on rbac_grants_audit. On Postgres this requires DROP CONSTRAINT + ADD CONSTRAINT (no in-place ALTER VALUE).
3.3 Decoupling principle
The idp_group_name value is what the IdP emits. The rbac_groups.name is Raxx's internal name following the <app>-<resource>-<level> or group-bundle convention. These two values may differ. Decoupling them means:
- The IdP administrator configures their side using IdP-native naming conventions.
- The Raxx operator maps the IdP name to the Raxx group by setting
idp_group_nameon therbac_groupsrow. - Neither side needs to know the other's naming scheme.
4. SAML Claim Attributes
Raxx trusts two attributes from a validated SAML 2.0 assertion. All other attributes are ignored.
| Attribute | Trusted | Purpose |
|---|---|---|
email |
Yes, after signature validation | Look up or JIT-provision the admin record |
groups |
Yes, after signature validation | Resolve RBAC group memberships for the session |
| Any role-level attribute | Never | Role names never appear in SAML assertions (I-5) |
The groups attribute is multi-value. Its XML attribute name varies by IdP:
- Okta:
groups - Azure AD / Entra ID:
http://schemas.microsoft.com/ws/2008/06/identity/claims/groups - Google Workspace SAML:
groups(must be configured as a custom attribute in the SAML app)
Configure the attribute name as the Queue service environment variable SAML_GROUPS_ATTRIBUTE_NAME (default: groups). This accommodates IdP variation without a code change.
5. Claim-to-Group Mapping
5.1 Matching rule
idp_group_name matching is exact string, case-sensitive. The group name emitted by the IdP must byte-for-byte match rbac_groups.idp_group_name. No regex, no glob, no case-folding.
Rationale: glob or regex matching against IdP group names creates a privilege escalation surface if group names can be influenced by end users or IdP administrators. Literal matching is unambiguous.
5.2 Lookup
SELECT g.id, g.name
FROM rbac_groups g
WHERE g.idp_group_name = ANY(:assertion_groups)
AND g.idp_group_name IS NOT NULL;
Where :assertion_groups is the list of group names from the SAML assertion. This returns the complete set of Raxx RBAC groups the session will hold.
5.3 Multiple matching groups
If the assertion carries multiple group names that each match a different rbac_groups row, the session receives membership in all matched groups. Group memberships are additive; the effective permission set is the union of all roles from all matched groups per the resolution algorithm in design.md §3.4.
6. Default-Member Behavior (No-Match / Unmapped Claims)
When SAML authentication succeeds but no group claim matches:
- The session is issued. The user is authenticated.
- The session carries zero RBAC group memberships. No fallback group is assigned.
- The Console renders an "insufficient access" page on first load.
- A Sentry alert fires:
SAML_UNMAPPED_GROUPwith the unmapped group names redacted to a count (not the actual names, to avoid PII leakage in Sentry). - The full unmapped names are written to
customer_audit_eventsatdimension=operator_interaction,action=sso.saml.unmapped_group, accessible only toraptor-audit-admin. - No RBAC group is auto-created.
The same behavior applies when:
- The
groupsattribute is present but empty (zero values). - The
groupsattribute is absent entirely from the assertion.
There is no "default member" group for SAML users. Access requires an explicit idp_group_name mapping.
7. Precedence: SAML Claims vs. Direct RBAC Grants
A user's session may have both SAML-sourced group memberships and direct RBAC grants made by a human operator. The precedence rule:
SAML-sourced memberships and manually granted memberships are independent and additive.
- SAML provisioning only creates and revokes group memberships where
rbac_user_groups.granted_by LIKE 'saml:%'. - Manually granted memberships (any
granted_byvalue not starting with'saml:') are never touched by SAML assertion evaluation. - The effective permission set is the union of both sources via the standard resolution algorithm (
design.md §3.4).
Consequence: if a human operator grants a user group membership directly, that membership persists across SAML re-logins. It is not revoked when the SAML assertion no longer contains a matching group claim. To remove it, a human operator must explicitly revoke it via the Console grants UI or grant API.
This design is intentional: it allows operators to grant elevated access to specific users beyond what their IdP group membership provides, without coupling those grants to IdP state.
8. Operator Provisioning Steps
To wire an IdP group to a Raxx RBAC group (post-launch, after the idp_group_name column migration):
Step 1: Identify the target Raxx group.
All Raxx RBAC groups follow the naming conventions from design.md. Groups are bundles of roles; see design.md §3.1 for the group table. The four seeded groups as of migration 0012 are:
rbac_groups.name |
Roles included | Typical IdP mapping |
|---|---|---|
raxx-platform-admins |
All platform admin roles; includes raptor-audit-admin |
IdP group for engineering/platform team |
raxx-platform-owners |
Owner-level billing and configuration roles | IdP group for founders / C-level |
raxx-support-team |
raptor-audit-support and ticket-scoped access roles |
IdP group for support staff |
raxx-compliance-auditors |
raptor-audit-compliance (read-only, no notification) |
IdP group for SOC-2 auditors (provisioned empty at v1) |
Step 2: Set the idp_group_name value.
Via the Console RBAC group edit form (/console/rbac/groups, requires console-invite-admin):
Edit Group: raxx-platform-admins
IdP group name: raxx-engineering
Or directly via an audited SQL update (for bootstrap before the UI lands in RV-8):
UPDATE rbac_groups
SET idp_group_name = '<idp-group-name>'
WHERE name = '<raxx-group-name>';
Step 3: Coordinate with the IdP administrator.
Provide the IdP administrator with the exact group name you set in idp_group_name. They must configure their IdP to include that group name in the groups attribute of assertions issued for the SAML application. The IdP administrator does not need to know rbac_groups.name — only the idp_group_name value.
Step 4: Verify with a test login.
After mapping, a test user with the IdP group assigned should log in. The Queue service logs will show the group lookup. Confirm the user's session shows the expected group membership via GET /api/rbac/me.
9. Audit Trail
Every SAML-sourced group membership event produces two records (per I-4 and design.md §I-9):
-
rbac_grants_auditrow — pre-write, append-only: -event_type:'saml_grant'or'saml_revoke'-target_user_id: the admin record being affected -group_id: therbac_groups.idbeing granted or revoked -granted_by:'saml:jit'(first login) or'saml:reauth'(re-login reconciliation) -created_at_utc: UTC -
customer_audit_eventsrow —dimension=operator_interaction,action=sso.saml.group_grantorsso.saml.group_revoke. Visible in the unified audit trail toraptor-audit-admin.
Unmapped group alerts write only to customer_audit_events (action=sso.saml.unmapped_group). No rbac_grants_audit row is written because no grant occurred.
Retention: rbac_grants_audit rows are retained for 7 years. customer_audit_events rows follow the schedule in docs/architecture/customer-audit-unified/.
10. Re-Login Reconciliation
On every SAML login by a user already in admins:
- Queue re-evaluates the assertion group claims against
rbac_groups.idp_group_name. - Groups present in the assertion but not yet in
rbac_user_groups(SAML-sourced): grant — insertrbac_user_groupsrow +rbac_grants_auditpre-write. - Groups absent from the assertion but present in
rbac_user_groupswheregranted_by LIKE 'saml:%': revoke — soft-deleterbac_user_groupsrow +rbac_grants_auditpre-write (event_type='saml_revoke'). - Groups unchanged: no-op.
- Session is issued with the updated group set.
Implication: removing a user from an IdP group takes effect at their next login, not immediately. For immediate revocation, the operator manually removes the SAML-sourced rbac_user_groups row via the Console grants UI (/console/rbac/grants).
11. Cross-References
- Full implementation spec:
saml-claims-to-groups.md— sequence diagram, JIT provisioning algorithm, GDPR checklist, SAML library candidates, replay prevention, open questions. - Data model:
design.md §3— table schemas forrbac_groups,rbac_user_groups,rbac_grants_audit. - Permission resolution:
design.md §3.4— how SAML-sourced group memberships combine with ticket grants and break-glass grants. - Phase 12 scope:
migration-plan.md §Phase 12— confirms this is post-launch; dev-days estimate when scoped. - Compliance auditors group:
migration-plan.md §Phase 9—raxx-compliance-auditorsgroup provisioned empty; ready for IdP mapping when SOC-2 is in scope. - FreeScout webhook auto-revoke:
design.md §5— ticket-scoped grants auto-revoke on ticket close; SAML-sourced group memberships do not (they revoke on next login).