Repository navigation
fix(plugin-security): stamp the access depth on the operation's own envelope - #33303
Conversation
…nvelope Step 2.6 of the authz middleware stamped the per-object access depth (`__readScope` and its siblings, plus the external rung's grant stamp) onto the caller's execution context in place. One caller context serves every operation a request runs, concurrent ones included, so a sibling read on another object overwrote the depth before plugin-sharing read it: the record page's related lists filtered a slice at its sibling's depth (a manager's contact slice answered 1 row beside total 26). The stamps are now collected per operation and handed to the rest of the chain on a copy of the envelope built at `next()`, after the steps above have staged the request-wide memos on the caller's envelope. Claude-Session: https://claude.ai/code/session_016WV4d1D3AzXvEhBbz79Pmi Co-authored-by: Claude <noreply@anthropic.com>
…velope Claude-Session: https://claude.ai/code/session_016WV4d1D3AzXvEhBbz79Pmi Co-authored-by: Claude <noreply@anthropic.com>
…eset Claude-Session: https://claude.ai/code/session_016WV4d1D3AzXvEhBbz79Pmi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 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 — 47 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 90aac88085c87d1712e50f76e3cdd5ed7bf9c378 && git checkout 90aac88085c87d1712e50f76e3cdd5ed7bf9c378
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 dc070731cbfedabd32e6dab710aa82686491b2c5 && git checkout -B drift-repro 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 && git merge --no-ff dc070731cbfedabd32e6dab710aa82686491b2c5
node scripts/docs-audit/affected-docs.mjs --json 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6
|
…rivate-object slice A page for a plain member opened by the showcase manager, related = [showcase_private_note, showcase_contact] by owner_id, in both orders: each slice's ids and total equal the same slice read alone. Claude-Session: https://claude.ai/code/session_016WV4d1D3AzXvEhBbz79Pmi Co-authored-by: Claude <noreply@anthropic.com>
|
Contract-review-claim: session_016WV4czuuneAppDnJbrNejw · head dc07073 · 2026-11-14T16:34Z review#2 (server area pilot, A3). |
Review: PASSHead:
Checked:
Before landing: checks on this head were still running when I read it (11 success, 10 skipped, 15 in progress, none failed). This PASS holds for this head once they are all green. No findings. |
|
domain:server#5 · os-elon-musk · session_016WV4d1D3AzXvEhBbz79Pmi ACCEPT · area A3
|
Requested by Elon · project thread
Fixes #33051
Area-lease pilot #33059, area A3.
What changes
packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts: step 2.6 used to write the per-object access depth (__readScope,__writeScope, the delegator halves) and the external grant stamp straight onto the caller's execution context. It now collects them per operation. Atnext()it hands them to the rest of that operation's chain on a copy of the envelope. The caller's context is never written. The copy is made atnext(), not at 2.6, so the request-wide memos that step 3 stages on the caller's context (membership sets, queues) are already on it.security-plugin.test.tsandauthz-matrix-gate.test.tsnow read the stamps fromopCtx.context, and assert that the caller's envelope stays unstamped. The external-stamp fixture's update now reachesnext(), because a refused write hands nothing on.packages/qa/dogfood/test/record-page-related-slices-concurrent-read-scope.dogfood.test.ts.packages/core/src/security/operation-private-keys.ts: doc comment only. It said the depth was "stamped in place", which is no longer true.@objectstack/plugin-securitypatch.Why
The record page route (
packages/rest/src/record-page-routes.ts) reads everyrelated[]slice concurrently, and all slices share one caller context. Each slice'sfindDatapasses the authz middleware, which stamped that object's depth on the shared context, and then reaches plugin-sharing's middleware, which reads the depth after several awaits. A sibling slice on another object overwrote the depth in between.On the showcase,
showcase_managerholdsshowcase_contactatreadScope: 'org'andshowcase_field_zooat the owner default. The contactfindwas therefore filtered atownand returned 1 row. Itscountran later atorgand reported 26. The route itself holds no shared per-slice state; the defect is in the producer, so the fix is there too. The same fix covers the page's other concurrent reads (streams, timeline counts) and any other door that runs reads in parallel on one context.related= contacts + field zoo (top 3 each)Verification
Pin, before and after the fix (dogfood, showcase as shipped)
origin/main e540baf:
Branch:
Tests 3 passed (3). The two CONTROL cases (each slice alone, and the admin in both orders) are green on both trees.Ablation at a41e121 (fix committed first, one line reverted, then restored)
The mutation is
const stamps: Record<string, unknown> = {};changed to= sc;, which puts the stamps back on the shared context.ablation-replacereported anchor 1 to 0, blob 440ec9c3 to e1a0635d.ablation-dist-preflightreporteddist/: marker present in 2 built files.showcase_contact: 1 rows beside total 26: expected 1 to be 3expected { userId: 'user_agent', …(7) } to not have property "__readScope"Restore:
git checkout HEAD -- …leftgit diff HEADempty, the blob hash matched HEAD (440ec9c3), the plugin was rebuilt, and preflight--absentreported the tree clean. The dogfood pin then exited 0.Package suites and gates
@objectstack/plugin-securityvitest: Test Files 267 passed, Tests 6607 passed, 45 skipped.@objectstack/plugin-sharingvitest: Test Files 74 passed, Tests 1511 passed.typecheckpassed for plugin-security (including check:test-typecheck) and for dogfood. eslint--no-inline-configon the touched files: exit 0.check-adr-0087-registration --base origin/main: no declared-breaking changeset.check:nul-bytes: OK.check-plugin-teardown-shape --self-testandcheck:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: shallow clone, and packages outside the build closure have no dist), so they are NOT MEASURED.check:type-check-debtis NOT MEASURED (whole-repo tsc hit the local time limit). CI runs all three.Clause-②: no
Metadata-protocol: none
Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4d1D3AzXvEhBbz79Pmi