Repository navigation
feat(service-storage): sys_attachment verifies the caller may open the referenced file - #27424
Conversation
…e referenced file Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
…n registration Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
…existing engine double Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
…ile gate Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
📓 Docs Drift CheckThis PR changes 1 package(s): 46 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 10 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 6152e636830e3a4beb234645ef263dc57c3ddb00 && git checkout 6152e636830e3a4beb234645ef263dc57c3ddb00
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5d46341747746b180626ad2fcb918324341cd22 bc7274ef53fb053cd7ebf5ba6872578ef17a40a7 && git checkout -B drift-repro c5d46341747746b180626ad2fcb918324341cd22 && git merge --no-ff bc7274ef53fb053cd7ebf5ba6872578ef17a40a7
node scripts/docs-audit/affected-docs.mjs --json c5d46341747746b180626ad2fcb918324341cd22
|
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB 席位核对Verdict: ACCEPT at head Context: this PR was dispatched without a card, under the maintainer's 2026-10-29 disclosure ruling for security findings met in testing (dispatch only through pm-dispatch, no issue). So there is no
Accepted deviations:
Out-of-scope findings from the dev: none. Landing: ready plus auto-merge through the relay, then followed to MERGED. Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB Landing held at 2026-10-29T10:19Z: the seat's landing write was refused by this session's permission classifier, so the PR stays draft and nothing was retried.
The ACCEPT record above stands at head Generated by Claude Code |
Clause-②: no
Metadata-protocol: none
维护者速读
sys_attachment的写入闸门现在会核验调用者能否打开file_id指向的文件(插入时,以及更新改动file_id时),与文件字段写入共用同一个判定。packages/services/service-storage/src/attachment-access-hooks.ts(installAttachmentFileReferenceGate)、packages/services/service-storage/src/referenceable-file.ts、packages/services/service-storage/src/storage-service-plugin.ts(接线)。pnpm --filter @objectstack/dogfood exec vitest run test/attachments-permission-matrix.dogfood.test.ts -t 'names a file the caller may open',预期 3 个用例全绿。What changes
sys_attachmentnow verifies that the caller may open the file a row references.findReferenceableFile,systemReadContext, theFileReferenceReadAuthorizertype and the refusal sentence move fromfile-reference-lifecycle.tsinto a new module,referenceable-file.ts. The file-field door keeps using them unchanged (byte-identical refusal text); the attachment gate now uses the same ones.installAttachmentFileReferenceGate(inattachment-access-hooks.ts, not exported from the package entry), adds abeforeInsertand abeforeUpdatehook onsys_attachment. Insert: the row'sfile_idmust name a file the caller may open. Update: asked once when the payload names afile_idthat differs from a matched row's. System-context and context-less writes are not judged, exactly as at the other gates in that module.400 VALIDATION_FAILED, one ADR-0114invalid_valueentry forfile_idlabelled with the field's declared label (File). Nothing is written.installAttachmentAccessHooks(same-priority hooks run in registration order), so the parent rule and the row rule answer first and a caller they refuse keeps their refusal.storage-service-plugin.tsbuilds the download door's verdict (buildFileReferenceReadAuthorizer) once and hands the same binding to both doors.installAttachmentAccessHooks's exported signature is unchanged.attachFileAs, whose fresh upload is the caller's own), attaching a file you can already open through another record, admin attaching their own files, seeds and lifecycle sweeps.Tests
attachment-access-hooks.test.ts, new describe "the file a row names must be one the caller may open", driven through the suite's existing engine double): insert refuses a file the caller cannot open (envelope:code+ thefile_idfield entry, lookup carries the acting organization); an id naming no file gets the same answer; controls: an openable file is admitted, a system insert is not judged, a caller the parent rule refuses keepsATTACHMENT_PARENT_ACCESSwith no file judged, no file gate installed means no file judged; update refuses afile_idchange to an unopenable file, admits an openable one, and judges nothing whenfile_idis unchanged or absent.storage-service-plugin.test.ts: the hook-roster pin now lists the secondbeforeInsert/beforeUpdatepair, with a comment naming it.attachments-permission-matrix.dogfood.test.ts, new describe(h)): insert of a file the member cannot open answers 400VALIDATION_FAILEDwith thefile_identry, no row lands, the member's download of that file still answers 403; control: the member's own upload attaches. Update re-pointingfile_idat that file answers the same 400 and leaves the row unchanged; control: re-pointing at the member's second own upload answers 200. Control: the file's owner attaches it.Acceptance notes
Clause-②: no: a runtime permission behaviour change, no contract surface widened (the new installer is module-level, not reachable from the package entry;installAttachmentAccessHookskeeps its signature).file_idonce per dispatch; the per-row dispatch bindsinput.id, and the unscoped multi-update shape is refused by the row rule before this hook runs.Verification
All readings at head
bc7274ef(objectstack-ai/objectstack).Package tests, typecheck, lint
pnpm --filter @objectstack/service-storage test: Test Files 70 passed (70), Tests 1267 passed (1267).pnpm --filter @objectstack/service-storage typecheck: exit 0.eslint --no-inline-config packages/services/service-storage/src packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts: exit 0.os-verify-lock: VERDICT command-exit 0.Dogfood
test/attachments-permission-matrix.dogfood.test.ts(dependency closure built first): Tests 22 passed, 1 skipped (23).sys_attachmentor the storage file routes (21 files): Test Files 20 passed, 1 skipped (21); Tests 177 passed, 9 skipped (186).Ablation (one-time proof, not kept)
storage-service-plugin.ts, theinstallAttachmentFileReferenceGate(...)call replaced throughscripts/ablation-replace.mjs(anchor 1 to 0, blob00a7d069to50b2de31), package rebuilt,ablation-dist-preflight.mjs --absentconfirmed the call absent from all 4 built files.(h)cases: the two refusal cases failed on their400expectation, the owner control passed.git checkout HEAD -- PATH, rebuild, preflight confirmed the call back indist/, blob hash equals HEAD (00a7d069), working tree clean.Gate families
node scripts/pm/dispatch-gates.mjs --commands(95 families) run locally;--ranreconciliation: 95 derived, 95 run, 0 UNRUN.check-plugin-teardown-shape --self-test(exit 3, its pinned fixture commit is outside this shallow checkout) andcheck:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET: unrelated packages have nodist/here). CI runs both.check:system-context-censusandcheck:engine-double-contractwere red on an earlier head and are green at this head (the register row names the new installer; the new unit cases reuse the suite's existing engine double).node scripts/check-platform-checklist.mjs --changed-from origin/main: exit 0, 0 anchors lost.check:nul-bytes: OK.Dispatched by
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB.Generated by Claude Code