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 19c1c3f65f9e1e34fa43f7e50b2a4a190aee7fed && git checkout 19c1c3f65f9e1e34fa43f7e50b2a4a190aee7fed
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f18adce2f65b9de8ce3b7de1c3ba870bebb53420 6c7ff71e5e53ef2fc6f62ad391d033f6eda49d33 && git checkout -B drift-repro f18adce2f65b9de8ce3b7de1c3ba870bebb53420 && git merge --no-ff 6c7ff71e5e53ef2fc6f62ad391d033f6eda49d33
node scripts/docs-audit/affected-docs.mjs --json f18adce2f65b9de8ce3b7de1c3ba870bebb53420
|
|
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>
|
security#1 · os-sales · session_01L6bhgVfjk2i8T2D4RmxsKg Contract reviewServed-tier: Inputs read: PR #30307's body, its three comments (the docs-drift advisory 6650741217, the prior ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
… the by-id re-read with the platform's write predicates under one nested $and
The by-id write re-read carries the platform's write predicates as one
nested `$and` beside the `{ id }` address. The three sys_user self-service
pins that assert the re-read's `where` verbatim now expect that shape, and
the file's row-scope sentence says so; each pin's verdict (the refusing
layer, the error code) is unchanged. Two plugin-security test comments
describe the same shape.
Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
Co-authored-by: Claude <noreply@anthropic.com>
|
security#1 · os-sales · session_01L6bhgVfjk2i8T2D4RmxsKg Contract reviewServed-tier: This is a DELTA review of Inputs read: the delta commit and its message, the replaced PR body, the PR's comments after the claim (the FAIL record itself; nothing newer), the three changed files at the head, the cited step 2.7 code at the head, and the head's check-runs, read at 13:09 and 13:11 UTC, identical: every one of the 37 non-skipped check-runs ① Derived judgments
② Semver levelUnchanged by the delta: ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
security#1 · os-sales · session_01L6bhgVfjk2i8T2D4RmxsKg 维护者速读
席位核对 — ACCEPT · security#1 · head
|
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;分析面pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/analytics-platform-scope-predicate-guard.test.ts,预期 2 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 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; 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. An unmarked subtree, or one with any other mark, is judged as the caller's, 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.Where the rule reaches: every
'policy'-marked subtree that arrives at the guard inside a caller-context read — the step 2.7 re-read and the step 2.8 master read above, the analytics read-scope merge (withReadScopeand its foreign-key expand) handed to an aggregate under the caller's context, and a record-sharing read scope composed before this middleware. Only platform-composed predicates are exempt; nothing the caller writes is.Unchanged: a caller's own filter on a field it cannot read is refused (read, analytics query 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.packages/rest/src/analytics-platform-scope-predicate-guard.test.ts(new; realObjectQL, realSecurityPluginundergroupandisolated, realAnalyticsServicePluginon the engine-aggregate strategy): a caller whose field rules hide the scoped organization column counts the records of its organization, the same count as a caller who reads the column; its own filter on that column is refused403 PERMISSION_DENIED.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{ $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 the two differ for a store fault only; whether it should follow step 2.7 is left to the seat.Verification
Readings are on head
6c7ff71e5eunless named otherwise.6c7ff71e5echanges no source file over8a92cab832(a merge ofmainintocc1fbac302): it brings the threesys_userself-service pins that assert the by-id re-read'swhereverbatim to the re-read's shape (the platform's write predicates as one nested$andbeside{ id }), with that file's row-scope sentence and two plugin-security test comments; each pin's verdict half (the refusing layer, the error code) is unchanged. The PR's source, changeset and earlier test files are byte-identical acrosscc1fbac302,8a92cab832and6c7ff71e5e, so the earlier ablation readings and the dogfood reading onb6b340a3dbstand for that source.Suites and type checks
pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/sys-user-self-service-route.test.ts, under the verify lock: on8a92cab832Tests 3 failed | 9 passed (12), the same three pinsTest Core (8/8)read red on that head, each diff the nested$and; on6c7ff71e5eTest Files 1 passed (1),Tests 12 passed (12), VERDICT command-exit 0.6c7ff71e5e, under the verify lock: the full plugin-auth suite,Test Files 224 passed (224),Tests 3954 passed | 10 skipped (3964), exit 0; the full plugin-security suite,Test Files 244 passed (244),Tests 5378 passed | 45 skipped (5423), exit 0;pnpm --filter @objectstack/plugin-auth typecheckandpnpm --filter @objectstack/plugin-security typecheck, VERDICT command-exit 0 (check:test-typecheckOK on both).6c7ff71e5e:Test Coresuccess, every shard success;Test Core (8/8)reads plugin-authTest Files 224 passed (224),Tests 3954 passed | 10 skipped (3964)and plugin-securityTest Files 244 passed (244),Tests 5379 passed | 45 skipped (5424);TypeScript Type Check,Lint & Repo Gates,Build Core,Dogfood Regression Gate,Live E2E UI,Test UIandGoverned Surface Queue Guardsuccess.cc1fbac302, under the verify lock:pnpm --filter @objectstack/plugin-security typecheckexit 0 (check:test-typecheckOK);by-id-write-row-reread.posture.test.ts,predicate-guard.test.ts,authz-matrix-gate.test.ts:Test Files 3 passed (3),Tests 111 passed (111);packages/restanalytics-platform-scope-predicate-guard.test.tsandanalytics-masked-field-gate.test.ts:Test Files 2 passed (2),Tests 12 passed (12);pnpm --filter @objectstack/rest typecheckexit 0 (check:test-typecheckOK).b6b340a3db: the full plugin-security suite,Test Files 243 passed (243),Tests 5349 passed | 45 skipped (5394), VERDICT command-exit 0.One-shot ablations (each restored, blob equal to HEAD, `git diff HEAD` empty)
dist/byablation-dist-preflight): 2 failed, red, then restored and rebuilt.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon6c7ff71e5ederived 62 commands; all 62 run, every exit code captured before any pipe, every one0;--ranreconciliation: 62 derived, 62 run, 0 NOT-MEASURED (a derived zero).check:dual-build-cjs-loads,check:i18nandcheck:type-check-debtread exit 3 (prerequisite: built output) in the first pass and0once run with the package tree built under the lock. Oncc1fbac302: 61 derived, all 610.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, onb6b340a3db:Test Files 10 passed (10),Tests 107 passed (107), VERDICT command-exit 0.Generated by Claude Code