Repository navigation
fix(plugin-audit): a member removes their own comment reaction through an own-row unreact door - #26345
Conversation
…h an own-row unreact door `DELETE /api/v1/comments/reactions/:id` removes the caller's own sys_comment_reaction row and only that: the row is read as the caller (so an unreadable reaction answers 404 like an unknown id), its reactor (`created_by`) must be the caller (else 403 PERMISSION_DENIED, row kept), and only then is it removed under a system context attributed to the caller. No object-level delete grant is read or widened; the generic data-door delete is unchanged. Claude-Session: https://claude.ai/code/session_01TZQBvhyKHoq1oKrgnVx35Y Co-authored-by: Claude <noreply@anthropic.com>
…changeset The route-envelope gate audits every `*-routes.ts` module; the unreact door writes every body through the shared sendOk/sendError pair, so it is declared with zero local response builders. Claude-Session: https://claude.ai/code/session_01TZQBvhyKHoq1oKrgnVx35Y Co-authored-by: Claude <noreply@anthropic.com>
…ant-audit census The door's removal is one more write call site threading a decidably elevated context; the census and the page's quoted totals move 243 -> 244. Claude-Session: https://claude.ai/code/session_01TZQBvhyKHoq1oKrgnVx35Y Co-authored-by: Claude <noreply@anthropic.com>
… instead of casting them Claude-Session: https://claude.ai/code/session_01TZQBvhyKHoq1oKrgnVx35Y Co-authored-by: Claude <noreply@anthropic.com>
…olveRequestExecutionContext The door hand-composed resolveAuthzContext and assembleExecutionContext. It now calls the platform's one request-to-context composition with the settings door's choices (fail-closed, gate resolved, API key and session, optional session API, nothing localized). check-single-authz-resolver lists it as a delegator. Claude-Session: https://claude.ai/code/session_01TZQBvhyKHoq1oKrgnVx35Y Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 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 75f40ffd86e7dd30263affa9b5560d120406d689 && git checkout 75f40ffd86e7dd30263affa9b5560d120406d689
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a8e7393dda4837823a88e906dd96eac4a175e554 0f28b4760718fb93247c8b0095c83246e8196b38 && git checkout -B drift-repro a8e7393dda4837823a88e906dd96eac4a175e554 && git merge --no-ff 0f28b4760718fb93247c8b0095c83246e8196b38
node scripts/docs-audit/affected-docs.mjs --json a8e7393dda4837823a88e906dd96eac4a175e554
|
Contract reviewServed-tier: The review is owed by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26161, patch changeset for @objectstack/plugin-audit, mergeable clean. Not governed (0 of 12 paths), 1,075 changed lines. |
| Fix | Ruling 6422861164 letter A. DELETE /api/v1/comments/reactions/:id reads the row as the caller (unreadable or missing gives 404, so nothing leaks). It refuses a foreign row with 403 PERMISSION_DENIED, and only then deletes under a system context attributed to the caller and kept in their organization. The grant model, member_default and security-anchor-high-privilege are unchanged, and the generic data-door delete still answers 403 (pinned). |
| Evidence | 38 check runs succeeded and 10 were expected skips on this head, 0 failed. Red first: the own-row case fails on main. The ablation of the reactor check turns the foreign-row pin red. Derived gates: 122 of 122 exit 0. |
| Carried | The UI half is filed as domain:ui sub-card #26346 (Blocked-by: #26161), with the final route shape. The @objectstack/client method gap is recorded in the route ledger and named on #26346. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. Every check on this head is green or an expected skip, and the review PASS is on record. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26161
This is the server half of ruling 6422861164 (letter A). The console half (the reaction chip calls the new route, and
feedReactionRemovestops reading adeletegrant) is a separatedomain:uicard that the seat files withBlocked-by: #26161.Clause-②: yes (widening)
Metadata-protocol: none
What changed
AuditPluginmounts an own-row unreact door on the kernel's HTTP server:DELETE /api/v1/comments/reactions/:id(:idis thesys_comment_reactionrow id)200{ success: true, data: { id, deleted: true } }400 VALIDATION_ERROR401 UNAUTHENTICATED403held-sign-in code403 PERMISSION_DENIED404 RECORD_NOT_FOUND503 SERVICE_UNAVAILABLEHow it decides, in order:
resolveRequestExecutionContext, with the settings door's choices: fail-closed for anonymous, the sign-in gate resolved onto the context, API key and session both admitted, nothing localized, no bearer carried.404 RECORD_NOT_FOUNDas an unknown id. That is the answer the data door gives both.created_bymust be the caller. This is the column the platform's own-record floor (owner_only_deletes) reads, stamped by the engine with the reactor. Otherwise the door answers403 PERMISSION_DENIED.attributedUserIdset to the caller and the caller'stenantId. Step 4 is the only way in to that elevated write. If the driver reports that nothing matched (a race), the door answers404.What does not move: no permission set gains
delete,member_default(default-permission-sets.ts) is untouched, andsecurity-anchor-high-privilege(ADR-0090 D5) is untouched. The genericDELETE /api/v1/data/sys_comment_reaction/:idanswers exactly what it did before.Files:
packages/plugins/plugin-audit/src/comment-reaction-routes.ts: the door,removeOwnCommentReaction, andplatformCallerResolver.packages/plugins/plugin-audit/src/audit-plugin.ts:start()mounts the door onhttp.server(falling back to thehttp-serveralias). With no HTTP server it warns once and mounts nothing.packages/plugins/plugin-audit/src/comment-reaction-route-ledger.tsplus its.conformance.test.ts: the package-local route ledger, using the print-door model. The row is agapbecause@objectstack/clienthas no method yet.packages/plugins/plugin-audit/src/objects/sys-comment-reaction.object.ts: the header's Delete section now names the door.scripts/check-single-authz-resolver.mjs: lists the door as a delegator ofresolveRequestExecutionContext, so a later re-inline of the resolver goes red.content/docs/permissions/tenant-audit-census.mdxanddocs/audits/2026-08-tenant-audit-write-call-sites.counts.md: the census regenerated for the door's one system-context write (243→244 sites).scripts/check-route-envelope.mjs: declares the new*-routes.tsmodule with{ responses: 0, ok: 0, err: 0 }. Every body goes through the sharedsendOk/sendError/sendServerFault..changeset/26161-own-row-unreact-door.md:patchon@objectstack/plugin-audit.Tests
packages/plugins/plugin-security/src/comment-reaction-unreact-door.integration.test.tsruns on the real kernel, the realSecurityPluginwith the shipped sets, and the realAuditPlugin(object, read gate and mounted door). Caller resolution is real too: the platform resolver runs over seededsys_user/sys_memberrows and an auth service whose session read names the user. The member is a plain organization member with no position. Their baseline ismember_defaultcomposed with an app every-member set that grants read and create (never delete) on comments and reactions. Cases:200, and the row is gone on read-back;403 PERMISSION_DENIED, and the row stays;404 RECORD_NOT_FOUND, the same status and code as an unknown id, and the row stays;404 RECORD_NOT_FOUND;401; a query parameter:400. In both cases the row stays;403 PERMISSION_DENIED, via the engine delete under the caller plusmapDataError, the data door's own path. The row stays.The test lives in plugin-security because plugin-security already has a devDependency on plugin-audit. The reverse edge would be a workspace cycle.
packages/plugins/plugin-audit/src/comment-reaction-routes.test.ts: the door's shell before the engine is asked (permission store outage503, nobody401, held sign-in403, no engine503, engine fault500 INTERNAL_ERROR), plus theAuditPluginmount with and without an HTTP server.packages/plugins/plugin-audit/src/comment-reaction-route-ledger.conformance.test.ts: checks ledger and registrar in both directions.Red first, on base
c4968860with the door not mounted: 6 of 8 integration cases red, each with "no DELETE /api/v1/comments/reactions/:id is mounted". The fixture case and the generic-delete403case were green, which is the defect itself.Ablation of the reactor check, on
0f28b47607(if (row.created_by !== callerId) throw …deleted throughscripts/ablation-replace.mjs): plugin-audit was rebuilt andablation-dist-preflight.mjs --absentreported the marker absent from all 6 dist files. Then 1 failed and 7 passed, and the failure was the foreign-row case alone. Restore: HEAD blob hash matched, the rebuild had the marker present indist/index.jsanddist/index.mjs, the tree was clean, and the run was 8 of 8.Package runs on
0f28b47607:pnpm --filter @objectstack/plugin-audit testpassed 64 files and 1023 tests.pnpm --filter @objectstack/plugin-security testpassed 229 files, with 4942 passing and 45 skipped. The--project reposuites of core, objectql, rest, runtime, spec and types all passed.typecheckexited 0 for both. eslint--no-inline-configon both packages exited 0.Gates on
0f28b47607: all 122 commandsdispatch-gates.mjs --commandsderives for this diff exited 0, and--ranreconciled 122 derived, 122 run, 0 NOT-MEASURED, 0 UNRUN.Acceptance notes
gapon the SDK:@objectstack/clienthas no method for it. The console reaches the server throughObjectStackClient(ui/packages/data-objectstack), so the console half will want a client method or a direct call. That half is the seat'sdomain:uicard.route-ledger-live-mount-parity.dogfood.test.ts) boots the showcase withoutAuditPlugin, so this ledger is not in its union, and this route is neither probed nor flagged there. The package-local conformance test is this route's guard. AddingAuditPluginto that boot is outside this card.Generated by Claude Code