Groups and role bindings
Phase A introduces group-mediated role bindings. Before Phase A, every operator had to be bound directly to a role; for tenants with dozens of operators this is unmanageable.
The principal types
A role_bindings row points at exactly one of three principal columns:
| Column | Subject type | What it means |
|---|---|---|
user_id | user | This specific user holds this role |
group_id | group | Every member of this group holds this role |
service_account_id | service_account | This service identity holds this role |
The check_principal constraint enforces "exactly one" at the database level. A row with two non-null principals (or zero) is rejected.
Effective scope resolution
When AuthSec issues a token for a user, the scope resolver unions three sources of grants:
effective_bindings(user) =
bindings WHERE user_id = user
∪ bindings WHERE group_id IN (SELECT group_id FROM user_groups WHERE user_id = user)
∪ bindings WHERE service_account_id = user -- only when subject is a service account
Each binding then expands to:
role → role_permissions → permissions → oauth_scope_permissions → oauth_scopes
The final token contains the intersection of:
- The scopes the user has effectively (from the union above).
- The scopes the resource server supports.
- The scopes the OAuth client requested.
(See Scope design for MCP tools for the third intersection.)
Membership precheck
Before any of the above runs, AuthSec checks the user's tenant-level status:
- If they have a
tenant_membershipsrow, it must bestatus='active'. Suspended/invited/left → fail closed. - If they have a
tenant_end_user_statesrow, it must bestatus='active'. Suspended → fail closed. - Service accounts skip this check (they have neither row).
This precheck is the cheapest possible "kick everyone out" lever during an incident — set a member's status to suspended and every binding they hold is ignored immediately, without rewriting role bindings.
Worked example
User: alice@example.com → user_groups: [engineering, on-call]
Bindings on those groups:
engineering → GitHub PR Writer on github-mcp
on-call → Deploy Operator on deploy-mcp (conditions: requires_mfa=true)
Direct bindings:
alice → Auditor tenant-wide
Effective access:
GitHub PR Writer on github-mcp
Deploy Operator on deploy-mcp (subject to MFA precheck at PDP — Phase B)
Auditor tenant-wide
Token request for github-mcp returns:
mcp:tools:write, github.pr:write (GitHub PR Writer's scopes)
audit_log.read claims (Auditor's scopes for the same audience)
When to use a group binding instead of a direct user binding
Use a group binding when:
- More than one person should hold the role.
- The set of holders changes over time and you'd rather edit the group than rewrite role bindings.
- A directory sync (SCIM, OIDC group claims) populates the group.
Use a direct user binding when:
- The grant is genuinely individual (a temporary elevation, a one-off audit role).
- The binding has a tight
expires_at. - The binding has user-specific conditions (
allowed_ip_cidrsrestricted to that user's office IP, say).
API reference
POST /uflow/v2/groups/:group_id/role-bindings— create a group-subject binding.GET /uflow/v2/users/:user_id/effective-access— list every binding affecting a user, direct + via group.
See Effective Access in the admin UI for the operator-side view.