ADR-0027: RLS Boundary — Content-Plane vs Control-Plane¶
- Status: accepted
- Date: 2026-06-30
- Deciders: Nicolas Burri
- Refines: ADR-0002, ADR-0009, ADR-0012, ADR-0026
- Refined by: ADR-0029 (content-model realization of the content-plane RLS move), ADR-0048 (folder rows classified content-plane)
Context¶
scope and mandator are dual-purpose: they are the separation boundary (what RLS
protects) and also control-plane objects that administrators create and manage. Once
lqms_authorized_scopes is gated on the VIEW content-read permission (to enforce
admin ≠ content, REQ-ADM-004), RLS on scope/mandator breaks administration:
- an admin whose role lacks
VIEWcannot read the scopes they administer (RLS hides them); - the
WITH CHECKpolicy blocks creating a scope/mandator (the new row's id cannot be in any authorized set — it does not exist yet).
Normal users are unaffected (their VIEW-authorized set already shows the scopes they work in).
Decision¶
Draw the RLS boundary between content-plane and control-plane:
- RLS (the hard DB backstop) guards content-bearing tables — documents, versions,
content parts, audit events, search index, and any table holding mandator/project data —
keyed to
lqms.authorized_scope_ids(VIEW-gated, ADR-0009/0026). scope,mandator, and the catalog / identity / authorization tables are control-plane, governed by the permission model (ADR-0012) at the application layer: reads filtered by role/permission, writes gated by the relevant admin permissions (MANAGE_SCOPES,ASSIGN_ROLES,ACTIVATE_CATALOG_ENTRIES,MANAGE_CATALOG,MANAGE_USERS). They are not under per-request content RLS. (Amended 2026-07-29, V073: the never-enforcedCREATE_SCOPE/CREATE_MANDATORthis list named were retired;MANAGE_SCOPES— ADR-0097 — is the permission that actually gates mandator/scope creation and the per-scope catalog activations.)- This clarifies ADR-0002 ("RLS on content-bearing tables"):
scope/mandatorare structural/control-plane, not content-bearing. - Bootstrap and admin scope/mandator creation are control-plane operations
(privileged/permission-checked), not constrained by content-RLS
WITH CHECK.
Trade-off (accepted): cross-tenant visibility of scope/mandator metadata (names, codes) is prevented by application + permission filtering, not by the DB backstop. The backstop guards customer data (content); structural metadata is control-plane.
Implementation sequencing¶
Implemented when the first content table lands (Phase 3 of the MVP plan), because there is no content table today to carry the RLS proof and the separation suite must not be orphaned. At that point:
- a migration removes
FORCE/ENABLERLS and the policies fromscope/mandator; - the RLS pattern (scope key + policy keyed to
lqms.authorized_scope_ids) is applied to the new content table; - the separation suite (
RlsSeparationTest,RlsConnectionReuseTest,RlsScopeContextproof) is re-targeted onto the content table.
Until then the early scope-RLS spike (V002) remains as a placeholder backstop. The
SECURITY DEFINER requirement on the resolution functions relaxes once scope is no longer
RLS-forced (they may become SECURITY INVOKER), but they remain the canonical resolution path.
Consequences¶
- The RISK-001/002 separation proof migrates from
scope/mandatorto content tables in Phase 3 — a stronger, more meaningful proof (it guards actual data). - Admin/control-plane operations get a clean, permission-gated path free of RLS
WITH CHECKfriction. - Refines REQ-SEP-003 (RLS applies to content-bearing tables; control-plane tables are permission-governed).
Requirements¶
Introduces REQ-SEP-012 (pending requirements baseline v1.4).