Skip to content
State: Approved

ADR#4761776210

multi-tenancyhierarchyauthorizationplatform-design

Resource placement via untyped recursive containers

Context

Platforms that host resources for organizations (agents, schedules, skills, policies) must answer "where does this resource live?" before they can answer who sees it, who may use it, and which rules apply to it.

Three concerns get conflated and must stay separate:

ConcernQuestion
TenantWhich isolation wall is this inside?
PlacementWhere does it live within the tenant?
OwnershipWhich human answers for it?

Ownership is never derived from placement: a tenant-wide resource still needs exactly one accountable human.

Considered options

Enumerated levels. An enum such as org | project | user on every resource. Rejected: it encodes our guess about the tenant's org chart. One company organizes by teams inside departments, another by client accounts, another is a solo operator. Every mismatch becomes a schema migration or a workaround.

Flat namespaces plus labels (the Kubernetes shape). Rejected: no native shadowing or policy inheritance, so hierarchy gets reinvented badly on top. Kubernetes itself needed the Hierarchical Namespace Controller bolted on later.

Pure relations, no canonical parent (the raw Zanzibar shape). Rejected for placement: resources lose a single unambiguous home, and "which pr-reviewer do I get here?" has no defined answer. Relations remain the right substrate for membership.

Structure encoded in the string (paths or prefixes). Placement as a path such as acme/backend/ml: walk-up is segment-stripping, flow-down is prefix matching, and no tree needs to exist. Rejected: a rename or move invalidates every pointer (unrepairable wherever history is immutable; with opaque ids a single recorded move rearranges everything and no pointer changes); there is no authoritative structure to validate against, so a typo silently mints a phantom branch and restructures leave no audit trail; and policy bound to a prefix detaches silently on rename. The chosen design keeps the pointer an opaque string and moves the structure into audited tree operations.

Multiple placements per resource. Rejected: placement exists to answer which policy chain governs a resource and where name resolution starts; two parents give two ceilings and two shadowing orders with no defined winner. All three clouds are single-parent for the same reason (AIP-124: at most one canonical parent). The need to appear in several groupings is served by labels (unlimited), and cross-place access by shares; one placement for governance, many labels for queries.

A container entity with fields. Two drafts died under review: { id, tenant, parentId?, kind, name } (tenant is derivable, kind is an uninterpreted label, name is an API-surface slug) and then { id, parentId? } (the parent is not a node property: commands that change the tree validate tree-wide invariants, so the tree is the consistency unit, not the node).

What the industry converged on

All three major clouds faced unknown tenant structure and independently built the same shape:

PlatformWallUntyped recursive middlePromoted leafRules flow down as
Google CloudOrganizationFoldersProject (billing, quotas)IAM policy (additive)
AWSOrganization rootOrganizational UnitsAccount (billing, isolation)SCPs (ceilings)
AzureTenant rootManagement GroupsSubscription (billing)Azure Policy + RBAC

Recurring properties: the middle is untyped and recursive (customers model themselves), exactly one container kind is promoted (where billing and isolation anchor), policy attaches to nodes and inherits downward, and every vendor caps nesting depth. The clouds disagree on inheritance mood: GCP IAM is additive-only (children gain, never lose), AWS SCPs are ceilings (ancestors bound children). Permissions and limits inherit in opposite moods, and a design needs both.

Resolution

Chosen option: one hierarchy of untyped places per tenant, because it is the only pattern that survived at scale across three independent cloud vendors, and it removes every platform guess about tenant org charts.

A container is a named place in the tenant's tree, not a thing. Nothing is stored inside one; resources point at it.

The decision, five rules:

  1. Each tenant has one tree of places. A place is an opaque id. The tree changes only through three audited operations (add, move, remove), each validated against the whole tree (parent exists, no cycles, depth cap of 10). There is no container record; the current tree is derived from the history of those operations.
  2. Every resource carries exactly one container id (where it lives) and one owner principal (which human answers for it). Neither derives from the other.
  3. The platform never interprets what a place means. Words like "project" or "team" are labels on the id; display names are API-surface slugs. No code path is conditioned on them.
  4. Finding walks up. Looking up a resource by name searches the starting place, then its ancestors toward the root; the nearest match wins.
  5. Rules flow down. A policy attached to a place governs everything at or below it. Permissions accumulate downward (a child can gain, never lose); limits bound downward (a parent caps everything below).

Consequences

  • "What is in this place?" is a query over resource pointers; the tree itself stores nothing.
  • Who belongs to a place (membership) is authorization data, kept in the authorization system, not in the tree.
  • Moving a place is one audited operation; ids never change, so nothing that references a place breaks. Policies apply from the new position going forward.
  • If the platform ever needs to treat one kind of place specially (for example, billing per "project"), that kind is promoted to a real concept at that moment, by a new decision. Until then, everything stays a label.
  • How each service stores the tree history (event stream, audited table) is that service's implementation choice.

Except as otherwise noted, the content of this repository is licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT