Repository navigation
Commit 7c7741d
fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets (#24641)
Fixes #24549
Clause-②: no (narrowing)
## The invariant
The `sys_attachment` parent gate (`@objectstack/service-storage`) and
the `sys_comment` moderation 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 `update` of that parent: `ISecurityService.explain`
with `{ object, operation: 'update', recordId }` under the caller's
envelope (the operation-private keys removed, as for the sharing call),
admitting only when `allowed` and `record.visible` are both `true`. 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 same
`mayEditAttachmentParent`), and the comment kit's
author-or-parent-editor rule on update and delete.
- The step runs last and only for a caller the earlier steps admit, so
it adds refusals and never admits one. Each limb keeps its envelope:
`ATTACHMENT_PARENT_ACCESS`, `ATTACHMENT_DELETE_DENIED`,
`RECORD_NOT_ACCESSIBLE` (all 403), and the not-visible
`PERMISSION_DENIED` / 403 for a caller who cannot read the parent.
- Record sharing's `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 on `ISharingService` ("Write
DEPTH is an input the CALLER supplies") requires.
- A kernel with no security service asks nothing new (the parent's own
update meets no such judgement there). A security service that serves no
`explain` is not asked it, the same feature detection `assertMayAttach`
already applies to its own `explain` question.
- Posting a comment is unchanged: it stays a read rule on the parent.
- `@objectstack/plugin-security` is not changed. Measured on a real
stack, its `explain` update 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`:
`mayEditAttachmentParent` is two steps, record sharing and the master
check, then `parentUpdateAdmits`. `AttachmentSecurityLike` adds an
optional `explain`.
- `packages/plugins/plugin-audit/src/comment-access-hooks.ts`:
`canEditParent` takes the same two steps. `CommentSecurityLike` adds an
optional `explain`.
-
`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`: `minor` for both packages,
as a declared narrowing (`Clause-②: no (narrowing)`) in the launch
window, with its ADR-0087 disposition `not-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.
- **Pins first, red on the unfixed gates.** Each row names the shape of
the answers the gate is handed (sharing verdict, master outcome, the
update judgement) and asserts the gate's answer, with `code` and
`status` on every refusal. Before the fix, at commit `04ed98cf8`:
attachment rows 38 failed / 29 passed, comment rows 20 failed / 12
passed. With the fix, all of them pass.
- **Ablation**, on the final head `f4f4615e0`. With the fix committed,
`scripts/ablation-replace.mjs` put 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 to `HEAD`, `git
diff HEAD` empty, marker count 0.
- **Real stack, differential.** ObjectQL with the SQL driver,
`SecurityPlugin`, `SharingServicePlugin`, `StorageServicePlugin` and
`AuditPlugin`, 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 with `security.explain` for the same pair. Cells where
the gate admitted and the parent's update refused: 38 before, 0 after.
The parent update and `explain` agree on every row but one.
- **Package suites and typecheck** on the final head `f4f4615e0`:
`@objectstack/service-storage` 61 files / 1118 tests passed;
`@objectstack/plugin-audit` 57 files / 930 tests passed; `typecheck` for
both, test layer included (`check:test-typecheck: OK`, 0 errors).
- **Derived gates** (`node scripts/pm/dispatch-gates.mjs --commands`) on
the final head `7deb8468f`: 99 commands (the docs page added 30
families), every command exit 0. `--ran` reconciliation: 99 derived, 99
run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. The
attachment parity rows live in `attachment-access-hooks.test.ts` so that
they drive that file's ledgered engine double
(`check:engine-double-contract`, `check:objectql-double-limit`).
## Acceptance notes
- **Restrictive disagreements that remain**, where the gate refuses and
the parent's update admits. Both were refusals before this change and
stay refusals:
1. A caller whose edit depth on the parent object is wider than `own`.
The sharing step is asked at `own` by the 2026-08-10 ruling recorded on
`ISharingService`; this PR does not reopen it. `explain` agrees with the
update here, so on these rows the gates and `explain` disagree. Raised
to the seat as an open question.
2. One row where `explain`'s update verdict itself disagrees with the
by-id update (`explain` refuses, the update admits). Reported to the
seat as a finding of the explain-versus-enforce family; not fixed here.
- **Changed answer:** an attach naming a parent object the registry does
not hold now answers `404 OBJECT_NOT_FOUND`, what a by-id update of that
object answers, in place of `403 FILES_DISABLED`.
- **Real-stack pins inside the gate packages** would need
`@objectstack/plugin-security` and `@objectstack/plugin-sharing` as
devDependencies of both packages (two `package.json` files and the
lockfile), outside this claim's file surface, so they are not added. The
real-stack half (the `explain` update verdict equals the by-id update)
is held by `plugin-security`'s explain parity suites.
- **Docs:** `content/docs/permissions/attachments-access.mdx` states
this invariant (commit `e1fdd691e`). The seat amended the claim’s file
surface to cover the page at the review of record.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7879391 commit 7c7741d
6 files changed
Lines changed: 792 additions & 26 deletions
File tree
- .changeset
- content/docs/permissions
- packages
- plugins/plugin-audit/src
- services/service-storage/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | | - | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
49 | 70 | | |
50 | 71 | | |
51 | 72 | | |
52 | | - | |
| 73 | + | |
| 74 | + | |
53 | 75 | | |
54 | 76 | | |
55 | 77 | | |
| |||
65 | 87 | | |
66 | 88 | | |
67 | 89 | | |
68 | | - | |
69 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
70 | 93 | | |
71 | 94 | | |
72 | 95 | | |
| |||
81 | 104 | | |
82 | 105 | | |
83 | 106 | | |
84 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
85 | 116 | | |
86 | 117 | | |
87 | 118 | | |
| |||
181 | 212 | | |
182 | 213 | | |
183 | 214 | | |
184 | | - | |
| 215 | + | |
185 | 216 | | |
186 | 217 | | |
187 | 218 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
68 | 83 | | |
69 | 84 | | |
70 | 85 | | |
| |||
177 | 192 | | |
178 | 193 | | |
179 | 194 | | |
180 | | - | |
181 | | - | |
182 | | - | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
183 | 202 | | |
184 | | - | |
| 203 | + | |
| 204 | + | |
185 | 205 | | |
186 | 206 | | |
187 | 207 | | |
| |||
682 | 702 | | |
683 | 703 | | |
684 | 704 | | |
685 | | - | |
686 | | - | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
687 | 709 | | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
688 | 752 | | |
689 | 753 | | |
690 | 754 | | |
| |||
0 commit comments