You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit dce1c8e
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-security): `security.explain` answers an on-behalf-of caller's by-id update and delete the way the write itself does
6
+
7
+
For a principal acting on behalf of a user (an OAuth agent), explain's record-grained `update` / `delete` verdict now composes what the by-id PATCH / DELETE composes (ADR-0090 D10):
8
+
9
+
- record sharing's per-record gate is asked twice, as the write asks it: as the delegated principal at its agent-leg write depth, then as the delegator at the delegator's own write depth;
10
+
- the delegator's own row-level write filter is ANDed beside the principal's, as the write's pre-image check ANDs it.
11
+
12
+
Before, explain asked the gate once at owner-only depth and left the delegator's write filter out, so it could refuse a row the write admits and admit a row the write refuses. Ordinary (non-delegated) callers are unaffected, and so is the delegated read verdict.
feat(service-storage, plugin-audit): the attachment and comment gates judge "may edit the parent record" by the parent's own by-id update alone
8
+
9
+
Clause-②: yes (widening)
10
+
11
+
A caller who can update the parent record by id can also attach files to it and moderate its comments; a caller who cannot, still cannot. The gates only stop refusing what the CRUD path already admits.
12
+
13
+
-**What widens.** The `sys_attachment` parent gate (`@objectstack/service-storage`: attaching, detaching or editing another user's attachment, re-pointing one, and `assertMayAttach`) and the `sys_comment` moderation gate (`@objectstack/plugin-audit`: editing or removing another person's comment) now ask one question about the parent record: the security service's record-grained judgement of a by-id `update` of it (`security.explain` for `update` on the parent, admitting only `allowed === true` and `record.visible === true`). They no longer ask record sharing at owner-only depth first, nor the master-detail check: both are part of that judgement already. So a caller whose write depth on the parent object is `unit`, `unit_and_below` or `org` may now attach and moderate wherever that depth lets them update the parent, and so may a caller an app-authored row-level update policy admits.
14
+
-**What does not move.** A caller the parent's by-id update refuses is refused, with the same codes as before (`ATTACHMENT_PARENT_ACCESS`, `ATTACHMENT_DELETE_DENIED`, `RECORD_NOT_ACCESSIBLE`, all 403), and a caller who cannot read the parent still gets the not-visible refusal. A `controlled_by_parent` parent is still judged through its master. Uploaders keep their own attachments and authors their own comments. Posting a comment stays a read rule on the parent. A store fault keeps its own status.
15
+
-**Kernels that judge no by-id update.** Where the security service serves no `explain` (or there is none), both gates keep exactly the composition they had: record sharing's tri-state edit verdict at owner-only depth, then the master-detail check. With no sharing service, a caller-scoped read of the parent is still asked first.
16
+
-**`@objectstack/spec`.** The `ISharingService` contract prose ("Write DEPTH is an input the CALLER supplies") now records that the parent-record gates ask the security service's by-id update judgement instead of this service, and why they never resolve the parent's depth themselves. No type, member or export changes.
fix(showcase): a paid invoice's lines can no longer be added, removed or moved, so its total stays what was paid
6
+
7
+
Clause-②: no (narrowing) (the showcase refuses two writes it accepted; if the measurement shows the platform half would relax a refusal a faulting rule makes today, the dev stops and reports before building it, and the seat re-declares)
8
+
9
+
<!-- adr-0087: not-required (unpublished) @objectstack/example-showcase is private: no npm release carries the refused writes, and no author-written key was removed or renamed -->
10
+
11
+
`readonlyWhen: parent.status == 'paid'` on a line's `product`, `quantity` and `unit_price` locks EDITS of a line under a paid invoice, but it is insert-exempt by design and judges field values, not whether a line exists. So `POST /api/v1/data/showcase_invoice_line` under the paid INV-1003 answered `201`, a `DELETE` of its line `200`, and a `PATCH` moving a line's `invoice` onto or off it `200`, and the paid `total` moved every time.
12
+
13
+
Three hooks on `showcase_invoice_line` (`src/data/hooks/index.ts`) now refuse each of those writes with a sentence (`400`): `showcase_guard_paid_invoice_line_add` (beforeInsert), `showcase_guard_paid_invoice_line_move` (beforeUpdate) and `showcase_guard_paid_invoice_line_remove` (beforeDelete). Each reads the header invoice under `runAs: 'system'`. A system write passes (the seed loads INV-1003's line under a header that is already paid), and so does the cascade delete of a deleted invoice's lines. A draft or sent invoice's lines add, edit, move and remove as before.
Copy file name to clipboardExpand all lines: docs/adr/0047-object-ui-run-modes.md
+24-2Lines changed: 24 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,6 +24,26 @@
24
24
> (need named presets on an object → use `listViews`). Enforced by a new
25
25
> `ObjectUserFiltersSchema` (element narrowed to `dropdown`/`toggle`, `tabs`/
26
26
> `showAllRecords` omitted), which `ObjectListViewSchema` now `.extend`s back in.
27
+
>
28
+
> **Amendment (2026-10-20) — `filterableFields` is retired in 18.0, with no
29
+
> replacement key and no rewrite (#24640: the maintainer ruled 「C」, record
30
+
> 6278975049, and pulled it into 18.0 with 「提前」, record 6279033298;
31
+
> executed by PR #24721, `4bc3becd5`).** §3.3 item 1 below said
32
+
> `filterableFields` "stays as the legacy shorthand; loaders auto-upgrade it to
33
+
> `userFilters.fields` entries". That loader was never built: on the tree the
34
+
> retirement replaced, the key was declared, offered as a Studio form input and
35
+
> checked for field references, and nothing rewrote it into
36
+
> `userFilters.fields`; its one renderer, objectui's `ListView`, read it as the
37
+
> Filter panel's field whitelist instead. The ruling retires the key with no
38
+
> replacement key: the list-view shapes (`ListViewSchema`,
39
+
> `ObjectListViewSchema`) refuse it at parse with a `retiredKey()` tombstone
40
+
> that names the fix, and the conversion `view-list-filterable-fields-removed`
41
+
> drops it from stored views. No conversion rewrites it to
42
+
> `userFilters.fields`: that rewrite keeps nothing an author saw — the
43
+
> whitelist would vanish and quick-filter chips would appear (record
44
+
> 6278907207). A curated end-user filter bar is authored as `userFilters`.
45
+
> §3.3 item 1's sentence is struck in place, not deleted; §4 already called
46
+
> the alias a one-release courtesy, not a commitment.
27
47
**Deciders**: ObjectStack Protocol Architects
28
48
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0017](./0017-object-has-many-view.md) (independent view entities, `viewKind`), [ADR-0019](./0019-app-as-consumer-unit.md) (App is the consumer-facing unit — navigation decides what users see), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (draft · publish lifecycle), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (**AI is the long-term author of metadata — the design center this ADR inherits**)
0 commit comments