0045. The group model, dynamic membership, and the groups claim
Context
Groups are load-bearing everywhere and decided nowhere. SCIM provisions them (ADR-0018), directory sync imports them (proposal 0017), migration carries their memberships (proposal 0014), claims customization wants to emit them (proposal 0008), and every workforce authorization pattern leans on them — yet no record says what a group is in Vord, how membership is stored, whether membership can be rule-based, or how groups reach a token. The last question is the sharpest: unbounded group claims are the incumbents' notorious failure (Entra's groups-overage dance, bloated Kerberos PACs), and Vord's token has a stated size budget (specs/vord-token-v0 §4) that makes "just list them" impossible to leave undecided.
Decision
-
A group is a directory object; membership is Forseti relations. The group object (immutable internal id, realm-scoped per ADR-0011's uniqueness discipline, display name, description, external ids, schema-versioned) lives in the directory so SCIM, sync, and the admin surface have a real resource to address. Membership and nesting live only in the Forseti relation store (ADR-0006):
memberandownerrelations, with nested groups as Zanzibar userset indirection — which the engine resolves natively. There is deliberately no second membership table: "who is in this group" and "is this principal in that group" are relation queries, so group-based authorization is ordinary authorization, and the one engine already picked at M3 carries it. -
Dynamic groups are materialized rules, never hot-path evaluation. A dynamic group carries a declarative membership rule over principal attributes (ADR-0011's eventually-consistent profile data). A background evaluator — a consumer of the identity event stream (ADR-0022) — converges the rule into ordinary membership relations as attribute-change events arrive; a rule change triggers a re-evaluation job. Membership convergence is a stated budget (registered in ADR-0039), not an implication of "dynamic": by the time a decision is made, a dynamic group is indistinguishable from a static one, and nothing evaluates rules at issuance or verification time.
-
The groups claim is bounded by construction. Per client registration (ADR-0038), one of three modes — and unbounded listing is not one of them:
- omit (the default): tokens carry no group claim; relying parties that
need membership ask the
Authorizerthrough the SDK (ADR-0006, ADR-0004) — the honest answer for principals with thousands of groups. - filtered: the client registers the (small) set of groups or app-roles it cares about; the claim carries only the intersection — the app-roles pattern, bounded by registration.
- overage pointer: the claim carries memberships up to a registered
cap; past it, a marker claim says "truncated — query instead." The
Entra-overage shape, adopted deliberately and documented, instead of
discovered in production.
The claim names (
groups,roles, the overage marker) are reserved seats in the v0 token register (ADR-0003 decision 6 pattern; recorded in the spec) so verifiers learn them before any issuer emits them.
- omit (the default): tokens carry no group claim; relying parties that
need membership ask the
-
App roles are client-scoped relations surfaced as small claims. A client may define named roles; assignment (principal-or-group → role) is a relation; the
rolesclaim carries only the assigned roles for that client — always small by construction, the recommended pattern over raw group lists for authorization-bearing tokens. -
SCIM and sync map to the split. The SCIM
Groupsresource (ADR-0018) reads/writes the group object and its membership relations behind one endpoint; inbound sync (proposal 0017) and bulk import (proposal 0014) create group objects and emit membership-relation writes — group hierarchy imports are relation imports.
Consequences
- One membership truth: authorization, SCIM, sync, IGA reviews (ADR-0030 — group-membership certification is just relation certification), and the groups claim all derive from the same relations; drift between "the directory's groups" and "the authz engine's groups" is unrepresentable.
- The token-bloat failure mode is closed at the format level: no mode emits an unbounded list, and the size budget survives group-heavy workforce tenants.
- Group writes are relation-store writes and count against the second consistency domain's write path (ADR-0026, V3's issuance+grant storm) — imports and dynamic-group re-evaluations are exactly the bursty producers the M4 write-rate scenario must include.
- Dynamic membership inherits an honest freshness bound instead of an implied instantaneity; the budget is visible where all budgets live (ADR-0039).
- Refines ADR-0011 (a second directory object class), ADR-0018 (Groups mapping), ADR-0038 (per-client claim mode), and the v0 token spec (reserved claim seats); depends on ADR-0006/ADR-0026.
Build-order note
The group object schema and the claim-mode registration land with the directory/tenancy work at M4 (imports need them); dynamic-group evaluation needs the event stream and attaches at M5. The claim seats cost nothing now and are registered immediately while the format is v0/unstable.