Repository navigation
fix(plugin-security)!: an update or delete of a controlled_by_parent detail asks for edit access to its master on every write shape (ADR-0055 §3) - #25042
Conversation
…ster-edit question on every write shape (ADR-0055 §3) The master-detail write gate decided one write shape of a controlled_by_parent detail without asking whether the caller may edit the masters involved. It now judges that shape per master, through the same chain walk a by-id write runs, and refuses with the by-id leg's envelope. The chain walk is extracted verbatim so both legs run one copy. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…d_by_parent detail write shape (ADR-0055 §3) Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…olled_by_parent detail write shape Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…p-write-path-master-edit
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 75a8900a7560356b7586619b6c247578c3e46322 && git checkout 75a8900a7560356b7586619b6c247578c3e46322
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 50169dcecd571b63317a584448a9b2d1ba5bf751 e71bf1e5764a9e4c0715bb773551ad0cda59dff1 && git checkout -B drift-repro 50169dcecd571b63317a584448a9b2d1ba5bf751 && git merge --no-ff e71bf1e5764a9e4c0715bb773551ad0cda59dff1
node scripts/docs-audit/affected-docs.mjs --json 50169dcecd571b63317a584448a9b2d1ba5bf751
|
…p-write-path-master-edit Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…t gate as it now answers every write shape Comment-only: the RlsFilterOptions coverage-vouch bullet and step 2.7's superset parenthetical no longer say the gate returns early when no single row is addressed. No code token changes. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #25012 (body and every comment through the patch-round report ① Derived judgmentsGoverning text and the ruling it must not cross. ADR-0055 §3 (
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…e to update and delete Changeset text only; no code, no test. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT ·
|
Fixes #25012
Clause-②: no (narrowing)
What this changes
ADR-0055 §3 (
docs/adr/0055-master-detail-controlled-by-parent.md): "editing/deleting/creating a detail requires edit access to its master". Thecontrolled_by_parentdetail write gate (assertControlledByParentWrite, step 2.8 of the security middleware) decided one write shape the engine routes without asking that question. It now asks it on every shape.PERMISSION_DENIED, 403, the same message), and nothing is written. On an on-behalf-of write both principals are asked, as on a by-id write.computeControlledByParentFilter),packages/objectql/**andpackages/spec/**are untouched, and plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932's ruling B (6303721609) on the master-scope mechanism gets no second spelling here.assertControlledByParentChainEditable, so the insert, by-id and new legs run one copy. Git shows the walk's lines as unchanged context; one comment line in it is reworded.INVALID_FILTERrefusal answers.Unchanged: inserts and by-id writes (same path, same walk), system contexts, the read scope, the platform ownership floor (still applied on this shape; nothing here vouches it away), and every object that does not declare
controlled_by_parent.Reach, stated abstractly under the disclosure ruling on #9387 (
5323310017): it was re-measured before the change on a realObjectQL+SqlDriver+SecurityPlugin+ sharing stack, across every refusal leg, three principal classes and both tenancy postures. The measurement covers what bounded the rows. The readings are held privately by the seat.Tests
New file:
packages/plugins/plugin-security/src/controlled-by-parent-write-shape-master-edit.test.ts, 15 tests on a real stack (ObjectQLover better-sqlite3, the realSecurityPlugin, the real sharing service and middleware, contexts fromresolveAuthzContext+assembleExecutionContext,isolatedtenancy). Pinned behaviours:Ablation, one leg per pinned behaviour. Each leg was mutated through
scripts/ablation-replace.mjs(anchor hit 1 to 0, blob changed on disk) and restored withgit checkout HEAD -- PATH. Each restore was proven by blob equality with HEAD (cdc1192e52) and an emptygit diff HEAD. No build step was needed: the suite imports the subject fromsrc/relatively.Verification (head
2f80e0db95, baseorigin/main3b3830ca22)pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build: lockVERDICT command-exit 0.pnpm --filter @objectstack/plugin-security test: 200/200 files, 4129 passed, 45 skipped (taken at44eef53692; the merge from main touches none of this package's sources or dependencies).pnpm --filter @objectstack/plugin-security typecheck: exit 0. This includescheck:test-typecheck, which reports 0 files and 0 errors held in the debt ledger, so the new test compiles.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 69 commands (identical at44eef53692and2f80e0db95). All 69 ran under the lock at2f80e0db95, each exit captured before any pipe: 69 exit 0.--ranreconciliation: 69 derived, 69 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN. At44eef53692, before the dogfood closure existed,check:dual-build-cjs-loadsandcheck:i18nhad exited 3 (PREREQUISITE NOT MET). They are green here.(narrowing)and the title's!): dogfood closure built, thenpnpm --filter @objectstack/dogfood testunder the lock with no shard and no path.VERDICT command-exit 0: Test Files 284 passed and 3 skipped (287); Tests 2361 passed and 18 skipped (2379); no failures.node scripts/check-nul-bytes.mjs: OK.Acceptance notes
security-plugin.ts). The seat can approve a comment-only rider, or name a carrier.RlsFilterOptions.masterGateCoversThisWritedocblock says the gate "returns early and nothing replaces the floor" when no single id is addressed. Proposed: "does not vouch: the master gate judges this shape, but the floor stays, because dropping it here would widen the rows a member writes (a separate decision)".extractSingleId, or it returns early)". Proposed: "(extractSingleId; with no single id step 2.8 judges every master the write reaches instead)".controlled_by_parentdetail adds one read under the caller's context, one system read of the master references, and one chain walk per distinct master, each hop about three reads. The bound is the existing 10,000-row cap.security/explain(no record) for update or delete on acontrolled_by_parentdetail answersallowedwith no master leg. It is a record-agnostic question; noted, not filed.Open question for the seat
Seat's append (
domain:servicesseat 1, from the dev's patch-round report6307856898on #25012; the dev writes its PR body once):Patch round 1 (head
033fc75a09)git diff HEAD~1 -- packages/plugins/plugin-security/src/security-plugin.tsshows 7 lines in and 3 out, and every changed line is a comment line (0 non-comment lines). No code token changed.origin/main(dfe60e3a0c) was merged first, because it moved files the suite depends on (packages/objectql,packages/drivers/driver-sql,packages/core).033fc75a09under the lock:pnpm --filter @objectstack/plugin-security test: 200/200 files, 4129 passed, 45 skipped;typecheckexit 0;VERDICT command-exit 0, Test Files 284 passed and 3 skipped (287), Tests 2361 passed and 18 skipped (2379), no failures.Seat's append (from the dev's patch-round-2 report
6308106727on #25012):Patch round 2 (head
e71bf1e576)033fc75a09is that one changeset file, with no code change, so the033fc75a09dogfood battery stands.Generated by Claude Code