Repository navigation
fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets - #24641
Conversation
…t's by-id update judgement Each limb of the sys_attachment parent gate and the sys_comment moderation gate that asks whether the caller may edit the parent record is pinned to admit only where the security service's judgement of a by-id update of that parent admits, with every refusal asserted on its code and status. Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
… by-id update of the parent gets The sys_attachment parent gate and the sys_comment moderation gate now ask, after record sharing and the master-detail check, the security service's judgement of a by-id update of the parent record: its record-grained explain verdict for update (allowed and record.visible). Each limb that asks whether the caller may edit the parent admits only where that update admits. The step only adds refusals: record sharing's deny, the master check's refusals, the owner-only edit depth and a rejection keep their answers. Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
…nt gates Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
…gine doubles The attachment parity rows move into attachment-access-hooks.test.ts and drive that file's own engine double, which already routes findOne through the engine's predicate contract and holds the caller's limit; the comment pin's find double holds the caller's limit too. Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s 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 — 15 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 0b28161cfcb5595561d30de1a8de09c6ac99179a && git checkout 0b28161cfcb5595561d30de1a8de09c6ac99179a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b12e16a561cae3da15c09b463b470ce760b11834 7deb8468f840b8b979a5e8ab0fe1b206faf78abd && git checkout -B drift-repro b12e16a561cae3da15c09b463b470ce760b11834 && git merge --no-ff 7deb8468f840b8b979a5e8ab0fe1b206faf78abd
node scripts/docs-audit/affected-docs.mjs --json b12e16a561cae3da15c09b463b470ce760b11834
|
Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
…he gates enforce A caller may attach to, detach from, re-point to or moderate on a parent record only where a by-id update of that parent by the same caller would be admitted: record sharing, then the master-detail check, then the security service's judgement of that update. The page names the order, the degraded modes, the comment moderation gate's use of the same rule, and the 404 an attach naming an unregistered parent object answers. Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
…-0087 disposition Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, as the brief sets out: card #24549 (body and its three comments), PR #24641 (body, six-file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flagsThe os-dev report's
The report's
The report's
Further flags from this review:
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · #24549 ·
Generated by Claude Code |
Fixes #24549
Clause-②: no (narrowing)
The invariant
The
sys_attachmentparent gate (@objectstack/service-storage) and thesys_commentmoderation gate (@objectstack/plugin-audit) now hold one invariant: each gate gives the same answer as a by-id update of the parent record.Every limb that asks whether the caller may edit the parent record asks, after record sharing and the master-detail check, the security service's judgement of a by-id
updateof that parent:ISecurityService.explainwith{ object, operation: 'update', recordId }under the caller's envelope (the operation-private keys removed, as for the sharing call), admitting only whenallowedandrecord.visibleare bothtrue. That verdict is the write path's own composition (the parent object's grant, its row-level security, its record sharing and its master), so the gates keep no second copy of it.The limbs: the attach rule, the uploader-or-parent-editor rule on update and delete, the attach rule on a re-point,
FileAttachmentService.assertMayAttach(it asks the samemayEditAttachmentParent), and the comment kit's author-or-parent-editor rule on update and delete.ATTACHMENT_PARENT_ACCESS,ATTACHMENT_DELETE_DENIED,RECORD_NOT_ACCESSIBLE(all 403), and the not-visiblePERMISSION_DENIED/ 403 for a caller who cannot read the parent.deny, the master check's refusals and a store fault keep their answers. The owner-only edit depth the sharing step is asked at stays, as the 2026-08-10 ruling recorded onISharingService("Write DEPTH is an input the CALLER supplies") requires.explainis not asked it, the same feature detectionassertMayAttachalready applies to its ownexplainquestion.@objectstack/plugin-securityis not changed. Measured on a real stack, itsexplainupdate verdict already agrees with the by-id update on every row this card's invariant needs (one restrictive exception, below).Files
packages/services/service-storage/src/attachment-access-hooks.ts:mayEditAttachmentParentis two steps, record sharing and the master check, thenparentUpdateAdmits.AttachmentSecurityLikeadds an optionalexplain.packages/plugins/plugin-audit/src/comment-access-hooks.ts:canEditParenttakes the same two steps.CommentSecurityLikeadds an optionalexplain.packages/services/service-storage/src/attachment-access-hooks.test.ts: the parity rows, on the file's own pinned engine double.packages/plugins/plugin-audit/src/comment-parent-update-parity.test.ts: the parity rows for the comment gate..changeset/24549-parent-gate-parity.md:minorfor both packages, as a declared narrowing (Clause-②: no (narrowing)) in the launch window, with its ADR-0087 dispositionnot-required (no-migration-prescription).Evidence
The reproduction stays off GitHub under the disclosure ruling (#9387). What is cited here is counts and verdict lines only.
codeandstatuson every refusal. Before the fix, at commit04ed98cf8: attachment rows 38 failed / 29 passed, comment rows 20 failed / 12 passed. With the fix, all of them pass.f4f4615e0. With the fix committed,scripts/ablation-replace.mjsput the old decision back (record sharing and the master check alone) in each gate. The pins went red again on the same rows: attachment file 38 failed / 104 passed (142), comment pins 20 failed / 12 passed. The restore was proven: blob equal toHEAD,git diff HEADempty, marker count 0.SecurityPlugin,SharingServicePlugin,StorageServicePluginandAuditPlugin, one process. For 8 caller shapes and 14 parent records, each gate operation (attach, delete another user's file, delete another user's comment) was compared with a by-id update of the parent by the same caller and withsecurity.explainfor the same pair. Cells where the gate admitted and the parent's update refused: 38 before, 0 after. The parent update andexplainagree on every row but one.f4f4615e0:@objectstack/service-storage61 files / 1118 tests passed;@objectstack/plugin-audit57 files / 930 tests passed;typecheckfor both, test layer included (check:test-typecheck: OK, 0 errors).node scripts/pm/dispatch-gates.mjs --commands) on the final head7deb8468f: 99 commands (the docs page added 30 families), every command exit 0.--ranreconciliation: 99 derived, 99 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. The attachment parity rows live inattachment-access-hooks.test.tsso that they drive that file's ledgered engine double (check:engine-double-contract,check:objectql-double-limit).Acceptance notes
own. The sharing step is asked atownby the 2026-08-10 ruling recorded onISharingService; this PR does not reopen it.explainagrees with the update here, so on these rows the gates andexplaindisagree. Raised to the seat as an open question.explain's update verdict itself disagrees with the by-id update (explainrefuses, the update admits). Reported to the seat as a finding of the explain-versus-enforce family; not fixed here.404 OBJECT_NOT_FOUND, what a by-id update of that object answers, in place of403 FILES_DISABLED.@objectstack/plugin-securityand@objectstack/plugin-sharingas devDependencies of both packages (twopackage.jsonfiles and the lockfile), outside this claim's file surface, so they are not added. The real-stack half (theexplainupdate verdict equals the by-id update) is held byplugin-security's explain parity suites.content/docs/permissions/attachments-access.mdxstates this invariant (commite1fdd691e). The seat amended the claim’s file surface to cover the page at the review of record.Generated by Claude Code