Skip to content

Commit 3ee4384

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-30073-personal-view-conditions
2 parents 9080504 + b7075d9 commit 3ee4384

57 files changed

Lines changed: 6024 additions & 3235 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the `page:header` title row drops the retired `breadcrumb` and names the live `placedActions: true`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `skills/objectui/guides/page-builder.md`: the `page:header` sentence named `breadcrumb` among the title row's keys. `PageHeaderProps.breadcrumb` is a `retiredKey` tombstone since @objectstack/spec 17 (ADR-0087 D2), and the props lint refuses an authored one. The row now lists `title`, `subtitle`, `actions` or `placedActions: true`; the last two are refused together.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-security': minor
4+
'@object-ui/app-shell': minor
5+
---
6+
7+
fix(spec)!: an object permission that edits or deletes a record also reads it
8+
9+
Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened.
10+
Metadata-protocol: narrowed
11+
Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves.
12+
13+
<!-- adr-0087: registered permission-write-without-read-refused -->
14+
15+
**BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`).
16+
17+
- **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it.
18+
- **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client.
19+
- **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source.
20+
- **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id.
21+
- **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry.
22+
- Unchanged: `allowCreate` and `allowTransfer` are not part of this rule.
23+
24+
**FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means).

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ export const SalesUser = definePermissionSet({
5555
| `viewAllRecords` | Read ALL records regardless of ownership (super-user read) |
5656
| `modifyAllRecords` | Edit and delete ALL records regardless of ownership (super-user write); `allowDelete: false` beside it is refused |
5757

58+
An object permission that edits or deletes a record also reads it. An entry
59+
granting `allowEdit: true` or `allowDelete: true` must also grant read —
60+
`allowRead`, `viewAllRecords` or `modifyAllRecords` — or the permission set is
61+
refused, one issue at the bit's own path. The evaluator grants edit and delete
62+
as `read ∧ grant`, merged across the caller's sets like the other bits, so the
63+
read half may come from another set. A set stored before this rule existed
64+
keeps loading, and its unread edit or delete grant grants nothing. In Setup,
65+
ticking Edit or Delete ticks Read, and unticking any bit that reads (Read,
66+
View All or Modify All) unticks Edit and Delete once no bit that reads remains
67+
on the entry.
68+
5869
### `allowExport` — the export axis
5970

6071
Read and export are not the same privilege. Reading a record on screen and

‎content/docs/references/api/auth.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ const result = AuthProvider.parse(data);
104104
| :--- | :--- | :--- | :--- |
105105
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
106106
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
107-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
108-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
107+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
108+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
109109
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
110110
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
111111
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -165,8 +165,8 @@ This schema accepts one of the following structures:
165165
| :--- | :--- | :--- | :--- |
166166
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
167167
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
168-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
169-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
168+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
169+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
170170
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
171171
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
172172
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎content/docs/references/api/protocol.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,8 +1191,8 @@ This schema accepts one of the following structures:
11911191
| :--- | :--- | :--- | :--- |
11921192
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
11931193
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
1194-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
1195-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
1194+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
1195+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
11961196
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
11971197
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
11981198
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -1652,8 +1652,8 @@ One changed key path of the pending draft, and the last write that changed it.
16521652
| :--- | :--- | :--- | :--- |
16531653
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
16541654
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
1655-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
1656-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
1655+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
1656+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
16571657
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
16581658
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
16591659
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎content/docs/references/security/permission.mdx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ const result = AdminScopeSchema.parse(data);
4646
| :--- | :--- | :--- | :--- |
4747
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
4848
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
49-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
50-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
49+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
50+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
5151
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
5252
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
5353
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -95,8 +95,8 @@ const result = AdminScopeSchema.parse(data);
9595
| :--- | :--- | :--- | :--- |
9696
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
9797
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
98-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
99-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
98+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
99+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
100100
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
101101
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
102102
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -142,8 +142,8 @@ const result = AdminScopeSchema.parse(data);
142142
| :--- | :--- | :--- | :--- |
143143
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
144144
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
145-
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
146-
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
145+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that edits a record also reads it. |
146+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission. Requires read on the same entry (allowRead, viewAllRecords or modifyAllRecords): an object permission that deletes a record also reads it. |
147147
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
148148
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
149149
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

0 commit comments

Comments
 (0)