Skip to content

Commit 7a0e38b

Browse files
committed
feat(dogfood): ADR-0056 D10 — authorization conformance matrix (CI-checked)
Encode the ADR-0056 audit as a living artifact: one row per authz primitive in exactly one honest state (enforced/experimental/removed); enforced rows name an enforcement site, high-risk ones reference an end-to-end dogfood proof. A test asserts completeness + that every referenced proof file exists on disk — making "the permission model is landed" a green CI gate (a new fail-open or a deleted proof breaks the build). Also records the implementation status + the requireAuth default-flip readiness audit (share-links safe; public forms need a built-in guest_portal) in the ADR. 6/6 conformance tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
1 parent 6595b53 commit 7a0e38b

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

docs/adr/0056-permission-model-landing-verification.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,39 @@ The one-time table above rots. Make it a **living ledger**: extend the ADR-0054
188188
- ADR-0049 (no unenforced security properties), ADR-0054 (runtime proof), ADR-0055 (controlled-by-parent), ADR-0010 (metadata protection), ADR-0029 (kernel object ownership).
189189
- Audit evidence: `plugin-security/src/security-plugin.ts`, `rls-compiler.ts`; `plugin-sharing/src/sharing-service.ts`, `sharing-rule-service.ts`; `rest/src/rest-server.ts`; `runtime/src/security/resolve-execution-context.ts`; `spec/src/security/{permission,rls,sharing}.zod.ts`, `spec/src/data/object.zod.ts`.
190190
- Liveness ledger: `packages/spec/scripts/liveness/proof-registry.mts`. Implementation status: `content/docs/concepts/implementation-status.mdx`, `content/docs/guides/security.mdx`.
191+
192+
---
193+
194+
## Implementation status (2026-06-20)
195+
196+
The decisions landed incrementally, each with a runtime proof:
197+
198+
| Decision | Status | Proof / artifact |
199+
| :-- | :-- | :-- |
200+
| OWD scenarios (private + public-read) | ✅ landed | `showcase-private-owd`, `showcase-public-read-owd` dogfood |
201+
| D1 — canonical OWD vocabulary | ✅ landed | `plugin-sharing` units + OWD dogfood |
202+
| D2 — anonymous deny (warn) | ✅ landed (warn) | `showcase-anonymous-deny` dogfood; **flip release-gated** |
203+
| D4 — RLS compiler no-silent-drop | ✅ landed | `plugin-security` units |
204+
| D6 — configurable role hierarchy | ✅ landed | `RoleGraphService` units (`role_and_subordinates`) |
205+
| D7 — app-declared default profile | ✅ landed | `showcase-default-profile` dogfood |
206+
| D8 — experimental-tag unenforced surface | ✅ landed | spec markers + liveness |
207+
| **D10 — conformance matrix** | ✅ landed | `authz-conformance.matrix.ts` + `.test.ts` (CI-checked) |
208+
| D6/D7 e2e showcase, requireAuth flip | follow-on | see below |
209+
210+
### `requireAuth` default-flip readiness (pre-flip audit)
211+
212+
Flipping the global `requireAuth` default to secure-by-default is **release-gated**. A
213+
pre-flip audit of the legitimate anonymous surfaces found:
214+
215+
- **Share links** — SAFE. `share-link-service.ts` validates the token then reads under a
216+
**system context** (`SYSTEM_CTX`), so it does not depend on the anonymous fail-open and
217+
survives a deny flip.
218+
- **Control-plane** (`/auth`, `/health`, `/discovery`) — exempt via dispatcher skip-paths.
219+
- **Public forms** — AT RISK. `/forms/:slug/submit` bypasses `enforceAuth` but the INSERT
220+
relies on a `guest_portal` profile to scope it; `guest_portal` is **not built-in** (only
221+
the CRM example defines one), so under a deny flip, public forms break unless the
222+
deployment ships a `guest_portal` profile.
223+
224+
**Pre-requisite for the flip:** make public-form submission self-authorizing (grant INSERT
225+
on the form's declared target object) or ship a built-in `guest_portal`, then warn→enforce
226+
with a migration note. Until then, D2's boot WARN is the landed state.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0056 D10 — Authorization Conformance Matrix.
4+
//
5+
// The durable encoding of the ADR-0056 audit: one row per authorization
6+
// primitive, each in EXACTLY ONE honest state (enforced / experimental /
7+
// removed). `enforced` rows name their runtime enforcement site; high-risk
8+
// enforced rows additionally reference an end-to-end dogfood proof. The
9+
// companion test (`authz-conformance.test.ts`) asserts the matrix is complete
10+
// and that every referenced proof file exists — so "the permission model is
11+
// landed" is a CHECKED artifact, not a one-time scan. A new fail-open (a
12+
// declared-but-unenforced primitive) or a deleted proof breaks CI.
13+
14+
export type AuthzState = 'enforced' | 'experimental' | 'removed';
15+
16+
export interface AuthzPrimitive {
17+
id: string;
18+
summary: string;
19+
state: AuthzState;
20+
/** Runtime enforcement site (required when state === 'enforced'). */
21+
enforcement?: string;
22+
/** Dogfood proof filename in this directory (required for high-risk enforced). */
23+
proof?: string;
24+
/** Why it is experimental/removed, or a roadmap pointer. */
25+
note?: string;
26+
}
27+
28+
export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
29+
// ── Enforced + end-to-end proven ───────────────────────────────────────
30+
{ id: 'rls-read', summary: 'RLS `using` read filter', state: 'enforced',
31+
enforcement: 'plugin-security/security-plugin.ts computeRlsFilter (AND-injected)', proof: 'rls-fixture.dogfood.test.ts' },
32+
{ id: 'rls-by-id-write', summary: 'by-id write enforcement (#1994)', state: 'enforced',
33+
enforcement: 'plugin-security/security-plugin.ts pre-image re-read', proof: 'rls-fixture.dogfood.test.ts' },
34+
{ id: 'owd-private', summary: 'OWD private (owner-only)', state: 'enforced',
35+
enforcement: 'plugin-sharing/sharing-service.ts effectiveSharingModel=private', proof: 'showcase-private-owd.dogfood.test.ts' },
36+
{ id: 'owd-public-read', summary: 'OWD public_read (everyone reads, owner writes)', state: 'enforced',
37+
enforcement: 'plugin-sharing/sharing-service.ts (read model + canEdit)', proof: 'showcase-public-read-owd.dogfood.test.ts' },
38+
{ id: 'controlled-by-parent', summary: 'master-detail controlled_by_parent', state: 'enforced',
39+
enforcement: 'plugin-security/security-plugin.ts computeControlledByParentFilter + assertControlledByParentWrite', proof: 'controlled-by-parent.dogfood.test.ts' },
40+
{ id: 'multi-tenant', summary: 'organization isolation', state: 'enforced',
41+
enforcement: 'plugin-org-scoping + wildcard tenant RLS', proof: 'rls-multitenant.dogfood.test.ts' },
42+
{ id: 'anonymous-deny', summary: 'secure-by-default anonymous posture (capability)', state: 'enforced',
43+
enforcement: 'rest/rest-server.ts enforceAuth (requireAuth)', proof: 'showcase-anonymous-deny.dogfood.test.ts' },
44+
{ id: 'default-profile', summary: 'app-declared default profile (isDefault)', state: 'enforced',
45+
enforcement: 'plugin-security/security-plugin.ts fallback resolution', proof: 'showcase-default-profile.dogfood.test.ts' },
46+
47+
// ── Enforced (unit-proven; e2e proof is a follow-on) ───────────────────
48+
{ id: 'object-crud', summary: 'object CRUD permissions', state: 'enforced',
49+
enforcement: 'plugin-security/security-plugin.ts checkObjectPermission (fail-closed 403)' },
50+
{ id: 'fls', summary: 'field-level security (read mask + write deny)', state: 'enforced',
51+
enforcement: 'plugin-security/field-masker.ts + detectForbiddenWrites' },
52+
{ id: 'ownership-stamp', summary: 'owner_id auto-stamp on insert', state: 'enforced',
53+
enforcement: 'plugin-security/security-plugin.ts (insert owner_id inject)' },
54+
{ id: 'record-share', summary: 'manual record shares (sys_record_share)', state: 'enforced',
55+
enforcement: 'plugin-sharing/sharing-service.ts buildReadFilter/canEdit' },
56+
{ id: 'sharing-rules', summary: 'criteria/owner sharing rules', state: 'enforced',
57+
enforcement: 'plugin-sharing/sharing-rule-service.ts (materialized into sys_record_share)' },
58+
{ id: 'role-hierarchy', summary: 'role-hierarchy widening (role_and_subordinates)', state: 'enforced',
59+
enforcement: 'plugin-sharing/role-graph.ts RoleGraphService (sys_role.parent)' },
60+
{ id: 'rls-compiler-fail-closed', summary: 'uncompilable RLS predicate is surfaced/denied, not dropped', state: 'enforced',
61+
enforcement: 'plugin-security/rls-compiler.ts isSupportedRlsExpression + warn' },
62+
{ id: 'system-permissions', summary: 'systemPermissions / tab-app gating', state: 'enforced',
63+
enforcement: 'rest/rest-server.ts filterAppForUser' },
64+
65+
// ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ───────────
66+
{ id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'experimental', note: 'no runtime consumer; marked [EXPERIMENTAL] (D8)' },
67+
{ id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8)' },
68+
{ id: 'data-masking', summary: 'role-based data masking', state: 'experimental', note: 'FLS is the enforced field-visibility path; marked [EXPERIMENTAL] (D8)' },
69+
{ id: 'rls-config-global', summary: 'global RLSConfig / RLSAuditEvent', state: 'experimental', note: 'not read by the RLS path; marked [EXPERIMENTAL] (D8)' },
70+
{ id: 'requireAuth-default-flip', summary: 'flip the global requireAuth default to secure-by-default', state: 'experimental',
71+
note: 'D2 warn landed; the flip is release-gated. Pre-req: built-in guest_portal so public forms survive (share-links already read as SYSTEM after token validation, so they are safe).' },
72+
73+
// ── Removed — by ADR-0049 (roadmap M2) ─────────────────────────────────
74+
{ id: 'allow-transfer-restore-purge', summary: 'transfer/restore/purge ops', state: 'removed', note: 'ADR-0049 → roadmap M2' },
75+
{ id: 'flow-run-as', summary: 'flow runAs', state: 'removed', note: 'ADR-0049 → roadmap M2' },
76+
];
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0056 D10 — the conformance matrix is a CHECKED artifact. These assertions
4+
// make "every authorization primitive is in exactly one honest state, and every
5+
// claimed proof exists" a green CI gate. A new fail-open (enforced row with no
6+
// site/proof) or a deleted proof file breaks the build.
7+
8+
import { describe, it, expect } from 'vitest';
9+
import { existsSync } from 'node:fs';
10+
import { fileURLToPath } from 'node:url';
11+
import { dirname, join } from 'node:path';
12+
import { AUTHZ_CONFORMANCE, type AuthzPrimitive } from './authz-conformance.matrix.js';
13+
14+
const HERE = dirname(fileURLToPath(import.meta.url));
15+
const VALID = new Set(['enforced', 'experimental', 'removed']);
16+
17+
describe('ADR-0056 D10 — authorization conformance matrix', () => {
18+
it('has no duplicate primitive ids', () => {
19+
const ids = AUTHZ_CONFORMANCE.map((p) => p.id);
20+
expect(new Set(ids).size).toBe(ids.length);
21+
});
22+
23+
it('every primitive is in exactly one honest state', () => {
24+
for (const p of AUTHZ_CONFORMANCE) {
25+
expect(VALID.has(p.state), `${p.id} has invalid state '${p.state}'`).toBe(true);
26+
}
27+
});
28+
29+
it('every ENFORCED primitive declares an enforcement site (no silent claims)', () => {
30+
const missing = AUTHZ_CONFORMANCE.filter((p) => p.state === 'enforced' && !p.enforcement).map((p) => p.id);
31+
expect(missing, `enforced primitives missing an enforcement site: ${missing.join(', ')}`).toEqual([]);
32+
});
33+
34+
it('every experimental/removed primitive carries a note (honest rationale)', () => {
35+
const missing = AUTHZ_CONFORMANCE.filter((p) => p.state !== 'enforced' && !p.note).map((p) => p.id);
36+
expect(missing, `non-enforced primitives missing a note: ${missing.join(', ')}`).toEqual([]);
37+
});
38+
39+
it('every referenced dogfood proof FILE EXISTS (the ratchet)', () => {
40+
const broken: string[] = [];
41+
for (const p of AUTHZ_CONFORMANCE as AuthzPrimitive[]) {
42+
if (p.proof && !existsSync(join(HERE, p.proof))) broken.push(`${p.id}${p.proof}`);
43+
}
44+
expect(broken, `conformance proofs missing on disk: ${broken.join(', ')}`).toEqual([]);
45+
});
46+
47+
it('the high-risk owner/derived OWD primitives each carry an end-to-end proof', () => {
48+
const highRisk = ['owd-private', 'owd-public-read', 'controlled-by-parent', 'anonymous-deny', 'default-profile'];
49+
for (const id of highRisk) {
50+
const p = AUTHZ_CONFORMANCE.find((x) => x.id === id);
51+
expect(p, `missing matrix entry: ${id}`).toBeTruthy();
52+
expect(p!.proof, `${id} must carry a dogfood proof`).toBeTruthy();
53+
}
54+
});
55+
});

0 commit comments

Comments
 (0)