Architecture Decision Record

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

  1. 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): member and owner relations, 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.

  2. 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.

  3. 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 Authorizer through 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.
  4. 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 roles claim carries only the assigned roles for that client — always small by construction, the recommended pattern over raw group lists for authorization-bearing tokens.

  5. SCIM and sync map to the split. The SCIM Groups resource (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.