Skip to content

Commit 81dba25

Browse files
committed
fix(plugin-security): explain's record write verdict uses the by-id write path's inputs
security/explain answered record.visible: false for update/delete on rows the by-id PATCH/DELETE admits, on private-OWD objects: - M1: Layer 1 kept the platform ownership floor the pre-image gate drops when the sharing write verdict is allow. The gate's floor decision moves into resolvePreImageFloorDrop, and explain's computeLayeredRlsFilter binding asks it for update/delete (behaviour of the gate unchanged). - M2: canEditRecord / canDeleteRecord asked plugin-sharing with the bare context. They now stamp __writeScope from resolveWriteScopeForSharing, overwritten as the middleware's step 2.6 overwrites it. The on-behalf-of path keeps its previous inputs on both halves. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c1011b commit 81dba25

3 files changed

Lines changed: 511 additions & 30 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): `security/explain` computes a record's `update` / `delete` verdict from the by-id write path's own inputs, so `record.visible` matches what the by-id PATCH / DELETE does (#19963)
6+
7+
Clause-②: no
8+
9+
`POST /api/v1/security/explain` with `{ object, operation: 'update' | 'delete', recordId }` answered `decision.record.visible: false` (`decidedBy: 'rls'` or `'sharing'`) on rows that the by-id `PATCH` / `DELETE /api/v1/data/{object}/{id}` then admitted for the same caller. It happened on every object whose OWD is private (set explicitly, or left unset). A console that gates Edit on `record.visible` hid Edit and inline edit from users who were allowed to edit. Two inputs differed from the write path:
10+
11+
- **The platform ownership floor.** The by-id write gate drops `owner_only_writes` / `owner_only_deletes` (`created_by == current_user.id`) when the sharing service answers `allow` for the row. Explain kept the floor, so it excluded every row the caller did not create. That covers a row shared to them with `edit` access, a row they own but did not create, and a row an `org`-depth writer may edit.
12+
- **The write depth.** The write path hands the sharing service's per-record gate (`canEdit` / `canDelete`) the caller's effective write depth. Explain asked the same gate without it, so a caller with `org` or unit write depth was judged owner-only.
13+
14+
Explain now asks the same floor decision the write gate asks. It also passes the same write depth to the per-record gate.
15+
16+
Unchanged:
17+
18+
- Enforcement: the by-id write gate admits and refuses exactly what it did before. Its floor decision moved into one method that both paths call.
19+
- Reads (`operation: 'read'`) and object-level explanations (no `recordId`).
20+
- A caller acting on behalf of another user (`onBehalfOf`): its record-level write explanation uses the same inputs as before.
21+
- Objects whose OWD is `public_read_write` already matched and still do.

0 commit comments

Comments
 (0)