Skip to main content

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:

ColumnSubject typeWhat it means
user_iduserThis specific user holds this role
group_idgroupEvery member of this group holds this role
service_account_idservice_accountThis 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:

  1. The scopes the user has effectively (from the union above).
  2. The scopes the resource server supports.
  3. 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_memberships row, it must be status='active'. Suspended/invited/left → fail closed.
  • If they have a tenant_end_user_states row, it must be status='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_cidrs restricted 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.