Skip to content

Commit 7cae49c

Browse files
committed
docs(adr): ADR-0055 — permission model evolves by gap-closure, not rewrite (Proposed)
Records the architecture decision that the authorization model (already Salesforce-shaped: permission sets + FLS + predicate RLS + ownership + sharing rules + hierarchy) should close enumerable gaps via ADR-0049 enforce-or-remove and ADR-0054 prove-it-runs, against a mainstream conformance checklist — NOT a rewrite. Headline gap: master-detail `controlled_by_parent` is declared (OWDModel) yet has zero runtime consumers, isn't reachable through the object's `sharingModel` enum, and the RLS compiler is relationship-blind. Includes a conformance matrix and per-gap implement/remove/not-do verdicts. Status: Proposed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
1 parent dbc749e commit 7cae49c

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# ADR-0055: The permission model evolves by gap-closure against a mainstream baseline, not a rewrite
2+
3+
**Status**: Proposed (2026-06-19)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove gate), [ADR-0054](./0054-runtime-proof-for-authorable-surface.md) (prove-it-runs gate)
6+
**Surfaced by**: an audit of master-detail permission semantics — `OWDModel.controlled_by_parent` is declared in the spec yet has **zero runtime consumers**, is **not reachable** through the object's actual `sharingModel` field, and the RLS compiler is relationship-blind. The question it raised: *is the authorization model wrong at the foundation (rewrite), or is it the right shape with enumerable gaps (close them)?*
7+
8+
---
9+
10+
## TL;DR
11+
12+
ObjectStack's authorization model is **already structurally the mainstream (Salesforce-shaped) model**: permission sets + field-level security + predicate row-level security + record ownership + sharing rules + a role/department hierarchy. It is not primitive and its core abstraction is not wrong.
13+
14+
What it has is **enumerable gaps and unenforced declared surface** — most visibly **master-detail "controlled by parent"**: the spec declares `OWDModel.controlled_by_parent` ("Access derived from parent record (Master-Detail)") but nothing reads it, it is not even authorable through the object's real `sharingModel` enum, and the RLS compiler has no relationship traversal. That is exactly the *false compliance* ADR-0049 exists to kill and the *unproven liveness* ADR-0054 exists to expose.
15+
16+
**Decision.** Do **not** rewrite the permission model. Treat a mainstream platform (Salesforce primary, Microsoft Dataverse secondary) as a **conformance checklist**, not a blueprint to rebuild from. Close each gap deliberately through the gates we already built — **ADR-0049 enforce-or-remove** and **ADR-0054 prove-it-runs** — recording every capability's verdict in a living conformance matrix. The first concrete verdict is master-detail `controlled_by_parent`: **implement it (with a dogfood RLS proof) or remove the misleading enum**, and fix the `OWDModel``object.sharingModel` inconsistency either way.
17+
18+
---
19+
20+
## Context
21+
22+
### The model is already the right shape
23+
24+
| Capability | Salesforce | Dataverse (Power) | ServiceNow | **ObjectStack today** |
25+
|---|---|---|---|---|
26+
| Object-level CRUD | Profile / PermSet | Security Role | table ACL |`permission.objects.allow{Create,Read,Edit,Delete}` |
27+
| Field-level security | FLS | Field Security Profile | field ACL |`permission.fields.{readable,editable}` |
28+
| Row-level security (predicate) | — (via sharing) | BU/owner depth | query ACL |`permission.rowLevelSecurity.{using,check}` (incl. the #1994 by-id-write pre-image check) |
29+
| Record ownership | owner | user/team-owned | created_by | ✅ owner policy on `created_by` |
30+
| Hierarchy access | role hierarchy | BU hierarchy | groups | ◑ department/role hierarchy (`plugin-sharing` department-graph) |
31+
| Sharing rules (owner / criteria) || sharing ||`sys_sharing_rule` (owner + criteria) |
32+
| Manual / programmatic grants | manual + Apex share | access team || ◑ share-links / grants |
33+
| OWD baseline per object | private…public | org default ||`object.sharingModel` = private/read/read_write/full |
34+
| **Master-detail "controlled by parent"** | Controlled by Parent | parent-child BU ||**declared-only, unenforced** |
35+
| Teams / groups as principals | account/case teams | owner/access teams | groups | ◑ team principal |
36+
37+
The deltas are specific capability points (``/``), not a wrong foundation (predicate-RLS + permission-sets + sharing-rules + hierarchy *is* the mainstream pattern).
38+
39+
### Verified facts behind the headline gap
40+
41+
- `packages/spec/src/security/sharing.zod.ts``OWDModel` enum includes `'controlled_by_parent' // Access derived from parent record (Master-Detail)`.
42+
- `packages/spec/src/data/object.zod.ts` — the object's actual `sharingModel` is a **different, narrower** enum `z.enum(['private','read','read_write','full'])` that **does not include** `controlled_by_parent`. The two are out of sync; the value is not authorable on a real object.
43+
- `controlled_by_parent` has **zero non-spec, non-test runtime consumers** (2 total occurrences, both in spec).
44+
- `packages/plugins/plugin-security/src/rls-compiler.ts` — the compiler recognizes **exactly four forms** (`field = current_user.prop`, `field = 'literal'`, `field IN (current_user.array)`, `1 = 1`); intentionally **no subqueries / joins / relationship traversal**. There is no "resolve the detail's access through its master" path.
45+
- `plugin-sharing`'s only "parent" logic is `sys_department.parent_department_id` (the org/department hierarchy for principal expansion) — unrelated to master-detail record inheritance.
46+
47+
So master-detail today carries **lifecycle only** (cascade delete via `deleteBehavior: cascade`, `$expand`, inline editing) — **not** permission inheritance. A child object's RLS/sharing is evaluated entirely on the child's own fields/owner, independently of the master.
48+
49+
### Why this is gap-closure, not a rewrite
50+
51+
- **Security is the worst subsystem to rewrite for cleanliness.** Every rewrite re-opens holes like #1994 (the by-id-write bypass). The current engine has accumulated hard-won, tested invariants: the by-id-write pre-image check, the org-scoping policy-stripping logic, the fail-closed RLS compiler. A rewrite resets that to zero.
52+
- **The gaps are enumerable** (the matrix above), not systemic. They are debt items, each independently closable.
53+
- **We already built the right tools to close them safely**: ADR-0049 (enforce-or-remove) + ADR-0054 (prove-it-runs) + the liveness ledger. They are designed for exactly this — classify each authorable property honestly, then implement-with-a-proof or remove. That is a ratchet, not a rewrite.
54+
55+
## Decision
56+
57+
1. **No rewrite.** Keep the predicate-RLS + permission-set + sharing-rule + hierarchy engine and its invariants.
58+
2. **Mainstream platform as a conformance checklist, not a blueprint.** Salesforce is the primary reference (closest model + the mental model AI authors arrive with); Dataverse secondary. Use it to decide *which* gaps to close and *which to explicitly decline* — not to rebuild.
59+
3. **Every capability gets a verdict**, recorded in a living conformance matrix and driven through the existing gates:
60+
- **implement-with-proof** — build it, and (ADR-0054) carry a dogfood RLS proof;
61+
- **remove** — (ADR-0049) delete the misleading declared surface;
62+
- **not-do** — record an explicit non-goal with rationale.
63+
4. **First verdicts** (recommended; this ADR's acceptance settles them):
64+
65+
| Gap | Verdict | Rationale |
66+
|---|---|---|
67+
| Master-detail `controlled_by_parent` | **implement-with-proof OR remove** (decide on acceptance) | Table stakes for a relational low-code platform; today it is false compliance. Implementing = detail read/write access resolved through the master + a "can't-read-master ⇒ can't-read/write-detail" dogfood proof. If declined, remove the enum value. |
68+
| `OWDModel``object.sharingModel` inconsistency | **fix either way** | The two enums must converge: either `controlled_by_parent` becomes authorable on `sharingModel`, or the dead value is dropped from `OWDModel`. |
69+
| Role/department hierarchy access depth | **audit → enforce-or-remove** | Confirm "grant access via hierarchy" actually applies at read/write; classify in the ledger. |
70+
| `permission.contextVariables` | **remove** | Already ledgered `dead` (rls-compiler never reads it). |
71+
| ServiceNow-style scripted/per-row ACL scripts | **not-do** | Over-engineering for an AI-authored platform; unpredictable AI output. The four-form fail-closed compiler is the deliberate ceiling. |
72+
73+
5. **Master-detail is the headline.** Its concrete semantic decision (how deep does inheritance go; read-only vs read-write穿透; interaction with the child's own owner policy) may, if it needs more depth than this ADR settles, spin into a child ADR that *builds on* this one — exactly as ADR-0054 builds on ADR-0049.
74+
6. **Enabling harness.** Relationship-derived-access proofs need the verifier to synthesize parent+child record graphs. The related-record **topological synthesis** capability in `@objectstack/verify` (currently it skips objects with required relations) is the prerequisite for those proofs and is sequenced ahead of the master-detail proof.
75+
76+
## Consequences
77+
78+
- **Positive.** The model converges toward mainstream conformance **without** a high-risk security rewrite; every closed gap leaves behind a runtime proof (ADR-0054) or a removed dead surface (ADR-0049); the AI-authoring audience gets predictable, Salesforce-aligned behavior. The conformance matrix becomes the single, honest source of "what authorization actually does."
79+
- **Negative / cost.** Gap-closure is incremental and visible debt sits in the matrix until burned down. Master-detail inheritance is a non-trivial feature (relationship-aware access resolution) whose proof depends on the verifier's topological-synthesis work landing first.
80+
81+
## Non-goals
82+
83+
- **A permission-model rewrite.** Explicitly rejected.
84+
- **Implementing every mainstream feature.** The matrix decides per-capability; some are `not-do` on purpose (e.g. scripted ACLs).
85+
- **Settling master-detail's full semantics here.** This ADR settles the *strategy* and the implement-or-remove *verdict*; deep semantics may move to a child ADR.
86+
- **Client-side enforcement.** Authorization is server-enforced; UI affordances are presentation, out of scope for this gate (consistent with ADR-0054 §Non-goals).

0 commit comments

Comments
 (0)