feat(security): master-detail controlled-by-parent permissions + verify topological synthesis (ADR-0055)#2049
Merged
Merged
Conversation
…write (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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…by-parent design Refocus from a high-level "gap-closure not rewrite" principle to the concrete landing plan for master-detail permission inheritance — the substance an ADR should carry. Grounded in an engine investigation: - Read: auto-derive the user's accessible master-id set and constrain the detail's master FK via the EXISTING pre-resolved-membership mechanism (ExecutionContext.rlsMembership + the compiler's `field IN (current_user.<key>)` form) — zero RLS-compiler changes. Trade-off table vs subquery-join (compiler has no subquery support) and materialized column (dual-write/stale). - Write: extend the #1994 by-id-write pre-image check to require master EDIT access. - Spec: add controlled_by_parent to the authorable sharingModel enum (fixes the OWDModel/sharingModel inconsistency); detail must declare one required master_detail field. - Proof (ADR-0054): a dogfood RLS proof; prerequisite is related-record topological synthesis in @objectstack/verify (P0). Honest limits: id-set size ceiling, single-level chains in v1. Status: Proposed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
deriveCrudCases no longer skips objects with required relations. It builds the object dependency graph from required relational fields, topologically orders it (targets before dependents), and emits `relationalRefs` the runner fills with real ids — so relationship-dense objects (the core of real apps) are verified, not skipped. Both runners (runCrudVerification, runRlsProofs) thread a created-id registry via fillRelationalRefs. Honest `blocked` verdicts remain for the genuinely unsatisfiable: required relation to an external/missing target, required-reference cycle (incl. required self-reference), and cascade (target itself blocked). Optional relations never block — filled best-effort, else left null. This is the prerequisite harness for the master-detail controlled-by-parent RLS proof (ADR-0055 P2), and independently widens `objectstack verify` coverage. Tests: 10 unit cases (ordering, optional/required, external/missing, cascade, cycle, self-ref, id threading). Full dogfood suite green (69 tests); rls cross-owner counts unchanged on example-crm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
…ADR-0055 P1+P2) Implements derived master-detail access — a detail object's read/write scope is derived from its master record. - spec: `controlled_by_parent` added to the authorable `object.sharingModel` enum (converges with OWDModel). - security (read): for a controlled_by_parent object, resolve the master records the user can read (reusing the master's own RLS via computeRlsFilter, run under a system context — no middleware re-entry) and AND-inject `masterFK IN (accessible master ids)` onto the read AST. Empty set ⇒ fail closed. Zero RLS-compiler changes (builds the $in filter directly). - security (write): extend the by-id-write path — a detail insert/update/delete requires CRUD edit on the master AND the master row visible under the master's write RLS. Closes the #1994-class by-id hole for derived access (the detail carries no authored RLS of its own). - proof (ADR-0054): dogfood RLS proof — a member who can't read the master can neither read nor by-id-write the detail, and isn't over-blocked on a master they own. Bound in the liveness ledger on object.sharingModel. Five high-risk classes now CI-enforced: field types, RLS, controlled-by-parent, analytics, flow nodes. Honest v1 limits documented in ADR-0055 (id-set ceiling, single-level chains). Full dogfood suite green (73 tests); liveness gate green; 18 registry tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
…ypecheck - ADR-0055 status → Accepted (implemented in this PR, P0–P2). - changeset for the controlled-by-parent feature (spec/plugin-security/verify minor). - fix(app-showcase): task-triage userFilters tabs were missing the now-required pinned/isDefault/visible fields (a pre-existing main breakage from #2048 that reddened TypeScript Type Check on every PR) — examples typecheck now green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 89 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…n (ADR-0055) Adds the canonical master-detail scenario to the showcase: `showcase_invoice_line` now declares `sharingModel: 'controlled_by_parent'`, so a line's access is derived from its parent `showcase_invoice` (a line is meaningless apart from its invoice). Admin/default single-tenant behavior is unchanged (admin sees all invoices → all lines); the showcase's own 20 tests still pass. Verification (real HTTP stack — the boundary is server-side; no browser needed): a dogfood proof boots the REAL showcase app with a member permission set carrying an owner policy on the invoice, then asserts a member who cannot read an admin-owned invoice can neither read nor by-id-write its lines, but can read/edit lines under an invoice they own. 4/4 green; full dogfood suite 77 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
os-zhuang
marked this pull request as ready for review
June 19, 2026 16:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements ADR-0055 end-to-end (the ADR is included, now Accepted): master-detail "controlled by parent" permissions — a detail object's read/write access derived from its master record — plus the verify harness capability that makes it provable.
Previously
OWDModel.controlled_by_parentwas declared but unenforced (no consumers; not authorable viasharingModel; the RLS compiler is relationship-blind). This closes that gap on the existing engine (no rewrite), guarded by a runtime proof.Phases (all in this PR)
P0 —
@objectstack/verifyrelated-record topological synthesis.deriveCrudCasesno longer skips objects with required relations: it builds the object dependency graph, orders it topologically (targets before dependents), and threads real ids (fillRelationalRefs) through both runners. Honestblockedverdicts remain for required-reference cycles and external/missing targets. Independently widensobjectstack verifycoverage. (10 unit tests.)P1 — spec + read derivation.
controlled_by_parentadded to the authorableobject.sharingModelenum. For such an object, the security middleware resolves the master records the user can read (reusing the master's own RLS viacomputeRlsFilter, run under a system context — no re-entry/recursion) and AND-injectsmasterFK IN (accessible master ids)onto reads. Empty set ⇒ fail closed. Zero RLS-compiler changes.P2 — write enforcement + proof. The by-id write path is extended: a detail insert/update/delete requires CRUD edit on the master AND the master row visible under the master's write RLS (closes the #1994-class by-id hole for derived access). A dogfood RLS proof asserts both directions and is bound in the liveness ledger on
object.sharingModel.Verification (all local, green)
plugin-securityunit tests: 102/102 (cbp path is gated tocontrolled_by_parent, so non-cbp objects are unaffected).mainbreakage from feat(app-showcase): task default list switches across every record visualization #2048 intask-triage.page.tsthat reddened TypeScript Type Check on every PR).Honest v1 limits (in the ADR)
Accessible-master id-set is unbounded (large-tenant scale = documented future limit); master-detail chains are single-level (not transitively traversed).
Builds on ADR-0049 and ADR-0054.
🤖 Generated with Claude Code