ADR-0052: scope: all on tenant-kind entities compiles to the partition-root subtree¶
Status: Accepted (2026-07-07) Issue: #1541
Context¶
scope: <verb>: all compiles to Tautology — no filter. For ordinary
entities that is correct: the tenant fence (RLS tenant_id partition)
bounds the rows before the scope rule is consulted.
Tenant-KIND entities (those declaring tenant_host: — the ADR-0036
hierarchy anchors, e.g. Region ▸ Trust ▸ School) are different: they are
usually in tenancy.entities_excluded, carry no tenant_id column, and
have no fence at all. all on such an entity therefore meant "every
row of every tenant": a trust admin listing Schools saw every school of
every trust on the platform (reproduced live against a two-trust
dataset while investigating #1541).
The session's authority boundary already exists: since #1463 every
membership carries partition_root_id — the archetype:tenant root the
RLS rows are partitioned at — and the scope layer resolves
current_user.tenant_id to it.
Decision¶
When a scope: rule's condition is all and the entity itself is a
tenant kind (declares tenant_host:), the linker compiles the rule to
the partition-root subtree instead of Tautology:
id = current_user.tenant_id -- the row IS the session root
OR <parent> = current_user.tenant_id -- the row's parent is the root
OR <parent>.<parent> = current_user.tenant_id -- deeper ancestors
walking the entity's own tenant_host.parent FK chain (the inverse of
the ADR-0036 self-or-ancestor expansion, which walks an FK to a kind).
A root kind (no parent) compiles to the single id check. Entities
that are not tenant kinds keep Tautology — no behaviour change.
Like the ADR-0036 expansion, this applies where the linker provides the
entity map: READ and LIST scopes. Write-verb all rules keep
Tautology (writes on tenant kinds should use explicit conditions; a
subtree write story needs the #1455 payload-probe machinery and is a
follow-up).
Fail-closed properties¶
- A membership-less/anonymous session resolves
current_user.tenant_idto the deny sentinel → impossible filter → zero rows. - A broken or cyclic
parent:chain stops the walk and keeps the legs built so far — a narrower predicate, never a broader one. Depth is bounded by the same_MAX_TENANT_HIERARCHY_DEPTHas ADR-0036. dazzle db explain-scope <Kind> listshows the compiled subtree.
RLS¶
Tenant kinds in entities_excluded have no RLS policies; the subtree
predicate applies on the app-layer and in-process page paths (both run
the scope filters). A fenced tenant kind (one that does carry
tenant_id) compiles the same predicate through the Phase C scope-policy
path unchanged. Extending fence coverage to excluded anchors is out of
scope here.
Consequences¶
- Behaviour change: apps relying on
allto mean "platform-wide" on a tenant kind now get subtree rows. Platform-wide listings belong to admin personas (which bypass scopes) or an explicit non-kind reporting entity. AegisMark's workaround filter (trust = current_user.trust) remains valid and now redundant. - The #1541 zero-rows report did not reproduce and is not this defect;
the silent-empty observability fixes shipped alongside make any
recurrence diagnosable (scope/permit denials and swallowed fetch
errors are now logged, and an errored fetch renders distinct copy
instead of the surface's
empty:message).