Repository navigation
fix(plugin-security): a by-id write's row re-read propagates a store fault, and judges the platform's own predicates without the caller-predicate guard - #30307
Conversation
…fault, and judges the platform's own predicates without the caller-predicate guard The re-read classifies its outcome through readUnlessRefused: a declared 4xx refusal keeps the record-level 403, anything else propagates as raised. The write predicates the platform composes ride into the re-read under one fresh subtree marked 'policy' with the spec's filter-subtree provenance mark, and the field-level predicate guard does not judge a subtree so marked. The controlled-by-parent master read carries its master write predicates the same way. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ates from the re-read's platform subtree Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…s hide the organization column removes an in-boundary assignment by id Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 17 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 6c78fb227a6fc1843c7b76a9fbdc85c0ab306fc7 && git checkout 6c78fb227a6fc1843c7b76a9fbdc85c0ab306fc7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c1240a38ac2c23c8d526e476a20edeabbbefd6f6 cc1fbac302c6bc1f9b1a4929b28512285b8619ab && git checkout -B drift-repro c1240a38ac2c23c8d526e476a20edeabbbefd6f6 && git merge --no-ff cc1fbac302c6bc1f9b1a4929b28512285b8619ab
node scripts/docs-audit/affected-docs.mjs --json c1240a38ac2c23c8d526e476a20edeabbbefd6f6
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs read: PR #30307 body and file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
…les only, and name every platform scope the predicate guard does not judge The changeset states the two rules without narrating earlier answers, and names where the provenance rule reaches: the by-id write re-read, the controlled-by-parent master read, the analytics read-scope merge handed to an aggregate under the caller's context, and a sharing read scope composed before the security middleware. A REST-level pin covers the analytics face. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
security-plugin-authz-middleware.ts(step 2.7 复读处)、predicate-guard.ts(collectConditionFields跳过 policy 子树那一行)、security-plugin-cbp-write-gate.ts(主记录复读)。pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/by-id-write-row-reread.posture.test.ts,预期 19 passed。What changes
Before an update or delete by id, the security middleware re-reads the addressed row under the caller's context together with the platform's write predicates for that row (the tenancy wall and the row-level write predicates). Two rules now hold for that re-read (step 2.7), and the second also for the controlled-by-parent master read a detail write asks (step 2.8):
readUnlessRefused, the one classification the write doors already use for a caller-context read: a row or no row as before; a declared 4xx keeps the record-level403 PERMISSION_DENIED, one answer whatever the id names; anything else is the write's answer, unchanged (forERR_DATASOURCE_UNAVAILABLEthe transports answer503). The step 2.8 master read keeps its403for every failure (its own comment records why); this PR does not change that catch.$andstamped'policy'with the spec's filter-subtree provenance mark (markFilterSubtreeProvenance, the mark step 3 already stamps on every read scope it injects), and the field-level predicate guard (collectConditionFields) does not judge a subtree so marked. Unmarked or any other mark is judged as before, and the mark does not survive a copy, a serialization or the wire. No second guard and no second read: the one re-read is asked once, and the one guard reads the existing mark.Unchanged: a caller's own filter on a field it cannot read is refused (read and predicate write); a row the caller cannot read and an id naming no row answer as they do for every other caller; a row the caller reads but may not write keeps its
403; a system write is not the gate's; the predicate write's read question still guards the caller's verbatim predicate.Tests
by-id-write-row-reread.posture.test.ts(new; realObjectQLover SQLite, realSecurityPlugin, posturesgroupandisolated,singleas control):403, identical for a row and for no row; with the store answering, the same writes land.404 RECORD_NOT_FOUND), nothing written; the caller's own predicate on the column is still refused on a read and on a predicate write; a system write lands.sys_user_position,sys_user_permission_set), admitted as for an administrator who reads the column.predicate-guard.test.ts: a'policy'subtree is not collected nor anything beneath it; the caller's own predicate beside it is still refused; an unmarked subtree, an'author'subtree, a spread copy and a JSON copy of a'policy'subtree are all judged.authz-matrix-gate.test.ts: the Layer 0 matrix helper reads the write predicates from the re-read's platform subtree (the re-read is now{ $and: [{ id }, PLATFORM] }); every matrix expectation is unchanged.Acceptance notes
main); the delegated-administration gate itself is not touched here.403(its own comment records the ruling it follows), so it now differs from step 2.7 for a store fault only; whether it should follow step 2.7 is left to the seat.Verification
All readings below are on head
b6b340a3db(after mergingmain) unless named otherwise.Full plugin-security suite
pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2under the verify lock:Test Files 243 passed (243),Tests 5349 passed | 45 skipped (5394), VERDICT command-exit 0.pnpm --filter @objectstack/plugin-security typecheck: exit 0 (tscboth configs andcheck:test-typecheckOK).One-shot ablations (each restored, blob equal to HEAD, `git diff HEAD` empty)
groupandisolated: the write answered403instead of raising the fault).403).main): 2 failed (group,isolated: the removal answered403).Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 commands; all 60 run onb6b340a3db, every exit code captured before any pipe, every one0;--ranreconciliation: 60 derived, 60 run, 0 NOT-MEASURED (a derived zero). An earlier sweep on the pre-merge head first readcheck:i18nandcheck:dual-build-cjs-loadsas exit 3 (no built output yet) andcheck:query-options-erasureas exit 1 (the new test's option casts); the options were typed in this PR and all three read 0.Dogfood
No
!in the title and no narrowing arm, so not the full battery: the ten dogfood files that drive by-id and predicate writes (write-door-unreadable-is-not-found,update-by-id-refusal-order,occ-stale-token-organization-wall,owner-anchor-and-bulk-writes,showcase-invoice-cbp,authored-row-write-scope,showcase-fls-read-mask-strip,delegated-admin-assignment-write-unreadable-row,predicate-write-unreadable-not-matched,cbp-parent-attachment-comment-gates), closure built, under the verify lock:Test Files 10 passed (10),Tests 107 passed (107), VERDICT command-exit 0.Generated by Claude Code