ADR-0106: The Client Reflection Follows Assignments, Not Only Content Reach¶
- Status: accepted
- Date: 2026-08-06
- Deciders: Nicolas Burri ("yes, do the interim psql and go ahead with the ADR"), on the finding he reported live from the first real deployment: the fresh lqms.ch install's bootstrap administrator sees no user administration — no Admin entry at all.
- Relates to: ADR-0012 (admin ≠ content; the UI reflects, the server enforces), ADR-0007 (the
reflection pattern itself), ADR-0098 (authority disclosure to scope members — the reasoning this
ADR extends to the assignee), ADR-0097 (the provisioning surface whose reachability this
restores), V005 (
lqms_authorized_scopes, the VIEW predicate that stays untouched).
Context¶
On a fresh installation — no demo seed, exactly what SystemBootstrap produces — the bootstrap
administrator holds the System Administrator role at GLOBAL: MANAGE_USERS, MANAGE_CATALOG,
MANAGE_SCOPES and the rest, deliberately without any content permission (admin ≠ content,
ADR-0012 / REQ-ADM-004). Every server-side admin surface works for this caller: their gates resolve
lqms_effective_permissions(user, GLOBAL), which needs no VIEW.
But the client's affordance gating — the /admin toolbar entry, the admin shell's sub-navigation,
"My access" — is computed over ctx.scopes(), which is GET /api/scopes →
DirectoryService.listScopes → lqms_authorized_scopes(user): the V005 function that enumerates
only scopes where some held role grants VIEW. For the pure administrator that set is empty by
design. Result: an account with full installation authority is shown no doors whatsoever — a
process dead-end of exactly the class the setup-status discipline exists to prevent.
Why it was never caught: every environment this product had ever run in — dev, mocked e2e, the demo
stack, the validation journey — is seeded, and the seeds staff the admin subject into content roles
too. lqms.ch (2026-08-06) was the first genuinely fresh install in the product's life, and it
surfaced the gap within hours. The backend even half-knew: RoleAdminService.listScopes refuses the
VIEW-gated directory read with a comment saying the directory "would show them nothing" — the lesson
was learned server-side and never propagated to the client's gating.
Decision¶
- The content boundary does not move.
lqms_authorized_scopes, every RLS policy, every content read stays exactly as it is: VIEW remains the visibility boundary for scope content, and the document/context picker keeps offering only VIEW-reachable scopes. Nothing in this ADR widens what any caller can read. - A second reflection leg: assignment-held scopes. A new authenticated read returns the caller's assigned scopes — every scope where they hold at least one role assignment — each with the caller's effective permissions there. Being staffed into a role in a scope is being told the scope exists: the same ISO 13485 §5.5.1 communication reasoning ADR-0098 used to disclose authorities to scope members applies a fortiori to the assignee themselves. The read discloses the scope's identity (code, name, type) and the caller's own permissions — never other people's staffing, never content.
- Affordance gates consult the union. The
/admintoolbar entry, the admin shell's sub-pages, and "My access" compute over content-reflection ∪ assignment-reflection. Content affordances (documents, dossiers, inbox…) keep computing over the content reflection alone. - The derived dead-end check ships with the fix (the house pattern): a test that boots bootstrap-only — no seed — and asserts the administrator's reflection is non-empty and the admin affordances appear. Fresh-install is now a tested state, not an assumption; the gate would have caught this ADR's finding the day the nav gating was written.
Consequences¶
- No schema migration:
role_assignmentalready answers the enumeration; the read composes the existingSECURITY DEFINERresolution functions underrunAslike every directory read. - "My access" becomes truthful for admin-only accounts (today it renders empty for exactly the people most likely to ask what they may do).
- Interim on lqms.ch, recorded and to be removed: until this ADR's realization is deployed, the
live instance carries a hand-made
QMS Operations Memberrole (VIEW on the internal QMS-OPS scope) assigned to the bootstrap admin via SQL on 2026-08-06 — unaudited (raw SQL predates the fix it papers over), marked INTERIM in its description. The realization's deployment removes the assignment and the role; the fresh-install test then proves it was never needed. - The instance-init wizard (registered backlog 2026-08-06) inherits a cleaner story: its "first admin" station can rely on the admin seeing their affordances without any content grant.