Repository navigation
fix(plugin-security): a controlled_by_parent detail's read scope requires object-level read on its master - #24985
Conversation
…ires object-level read on its master ADR-0055 §3 requires read access to the master to read a detail. The master-derived read scope now asks the master's object-level read admission before it scopes the master's rows, through one predicate extracted from canReadObject (objectReadAdmission), so no second copy of the rule exists. A principal the master's own read door refuses at the object gate gets the empty master set at every door that composes the scope: the caller half, the ADR-0090 D10 delegator half, getReadFilter and every hop of a chain. The engine's ADR-0139 leaf already answered this way. Pins on the real stack name the rule and its controls; a principal that may read the master object keeps its previous answer. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…es; note the insert pin's gate Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…p-master-object-read
…ter door answers Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 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 6ea3d602eae1c5089346817f530e4e7cbfa4a535 && git checkout 6ea3d602eae1c5089346817f530e4e7cbfa4a535
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0603bfa1cfb02a6e787f8ab2e389ce4752565ba9 3dd33f7b781f32525e32af20c9b7d72ab4e343b7 && git checkout -B drift-repro 0603bfa1cfb02a6e787f8ab2e389ce4752565ba9 && git merge --no-ff 3dd33f7b781f32525e32af20c9b7d72ab4e343b7
node scripts/docs-audit/affected-docs.mjs --json 0603bfa1cfb02a6e787f8ab2e389ce4752565ba9
|
席位核对 · ACCEPT ·
|
Fixes #24962
Clause-②: no
What this changes
ADR-0055 §3 says "Reading a detail requires read access to its master". §4 says a member who cannot read master M "can neither read nor by-id-write a detail D under M".
computeControlledByParentFilterbuilds acontrolled_by_parentobject's read scope from the master ids the principal may read. It scoped the master's rows: RLS, the sharing half, and the master's own derivation on a chain. It never asked the master's object-level read admission. Now it asks that first, and a principal the master's own read door refuses at the object gate gets the empty master set.canReadObjectmove into a privateobjectReadAdmission(object, sets, delegatorSets), which works over sets the caller has already resolved. The arms are the posture, the ADR-0066 D3 capabilities, theallowReadgrant, and the ADR-0090 D10 delegator's same questions.canReadObjectresolves the context and asks it, as before. The derivation asks it of the master, for the principal whose sets it was handed. There is no second copy of the rule.getReadFilter(security-plugin.tsnear:4390,:4408and:5943atf32d795ce5). The recursion asks the same question at every hop of a chain.packages/objectql/**andpackages/spec/**.The two spellings now agree
The engine's ADR-0139
ReadableIdsleaf askscanReadObject(parent)before it scopes a parent's rows (engine.ts,readableIdsScopeOf). That call now runs the same predicate the id-list derivation asks. Measured on the real stack with a probe read through the leaf, for a principal without object-level read on the master and for its control:The probe was a one-off measurement and is not committed.
Verification (head
3dd33f7b78)Pins.
src/controlled-by-parent-master-object-read.test.tsruns on the real stack:ObjectQLover better-sqlite3, the realSecurityPlugin, and on-behalf-of contexts assembled byresolveAuthzContextandassembleExecutionContext. It has 23 cases, named by the rule:find,findOneandcountread no detail, thegetReadFilterscope admits none, and a detail two hops down is not read;controlled_by_parentis not read without object-level read on that master;RECORD_NOT_FOUND/ 404 and writes nothing, an insert is refused, and a predicate update matches no detail;Package. At
438ef2dfde,pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2gave 198 files passed, 4100 passed and 45 skipped,VERDICT command-exit 0. The one commit since then renames the new file's labels, and its 23 cases pass at3dd33f7b78.pnpm --filter @objectstack/plugin-security typecheckgaveVERDICT command-exit 0at3dd33f7b78.tsconfig.test.jsonincludessrc/**/*, so the new file is compiled.Ablation. The fix was committed first. Each leg mutated
security-plugin.tsthroughscripts/ablation-replace.mjs(anchor 1 to 0, blob changed) and ran the pin file. Each was then restored withgit checkout HEAD -- PATHand proven by blob equal to HEAD (a774fe4e1ddf) and an emptygit diff HEAD. The subject is imported fromsrc/by a relative path, so nodist/leg applies.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 69 families at3dd33f7b78. All 69 ran one at a time, each exit captured before any pipe.--ranreports "69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)".438ef2dfdehadcheck:dual-build-cjs-loadsandcheck:i18nexit 3, because thedist/they read was missing. The trees were present at the second run, and both exited 0 there.Acceptance notes
RECORD_NOT_FOUND/ 404 where it answeredPERMISSION_DENIED/ 403 from the master-edit leg. The 404 is the write doors' existing rule for a row the caller cannot read, and that row is now unreadable. It is pinned.origin/mainwas merged atb811c73696(3 commits: service-datasource, docs, spec liveness data). This branch changes nothing underpackages/spec. Main has since moved toba69b168be(plugin-approvals labels, disjoint), and that is not merged.Generated by Claude Code