Skip to content

Commit 23502e3

Browse files
os-steveclaude
andauthored
docs(permissions,protocol): owner-type sharing rules are rejected at parse, not skipped (#9904)
`type: 'owner'` / `ownedBy` were removed from `SharingRuleSchema` in v17 (#1878) rather than left declared-but-skipped (ADR-0049/ADR-0078). Two pages still taught them as authorable, and the protocol reference still described the pre-v17 "seed bootstrap skips them (logged)" state — a materially different instruction to an author than "does not parse". permissions-matrix.mdx - the intro claimed a discriminated union with two `type` values; `SharingRuleType` is a one-member enum and `SharingRuleSchema` IS `CriteriaSharingRuleSchema` - dropped the "Owner-Based / [experimental — not enforced] / skipped at seed time" table row, which contradicted the page's own enforcement callout four lines below - the Configuration Example's second rule is now the enforced `criteria` form - added an OWD-posture callout: a rule on a `public_read_write` object parses and grants nothing protocol/objectql/security.mdx - the recipient enum comment listed the removed `group`/`guest` and omitted the enforced `team`/`business_unit` - replaced the "Owner-Based Sharing" section and its `OwnerSharingRuleSchema` reference (no such export exists) with the removal note and a criteria-form rewrite of the same rule - repaired the enforcement callout, keeping the still-accurate half (an unlowerable `condition` is skipped and logged, never seeded as a match-all) Wording converges on content/docs/permissions/sharing-rules.mdx:234. Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 88a9e63 commit 23502e3

2 files changed

Lines changed: 47 additions & 17 deletions

File tree

content/docs/permissions/permissions-matrix.mdx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,14 @@ Field-level security controls visibility and editability of individual fields pe
149149

150150
## 4. Sharing Rule Types
151151

152-
Sharing rules extend access beyond ownership and the depth axis. The declarative `SharingRule` schema is a discriminated union with two `type` values`owner` and `criteria`. Manual and territory sharing are separate mechanisms (see notes below the table).
152+
Sharing rules extend access beyond ownership and the depth axis. The declarative `SharingRule` schema carries a single enforced `type` value`criteria`. (`SharingRuleType` in `packages/spec/src/security/sharing.zod.ts` is a one-member enum, and `SharingRuleSchema` *is* `CriteriaSharingRuleSchema`; it is kept discriminated so a future enforced rule type can rejoin as a union member.) Manual and territory sharing are separate mechanisms (see notes below the table).
153153

154154
| Mechanism | `type` | Description | Example |
155155
|:---|:---|:---|:---|
156-
| **Owner-Based** | `owner` | Share records owned by a specific group/position with another recipient — **[experimental — not enforced]**: owner rules are skipped at seed time and materialize no shares yet | All accounts owned by "West Region" team are shared with "Sales Directors" |
157156
| **Criteria-Based** | `criteria` | Share records matching a CEL predicate over field values | All opportunities where `record.amount > 100000` are shared with "VP Sales" |
158157

159158
<Callout type="warn">
160-
**Enforcement status:** every authorable rule and recipient type is enforced. v17 reconciled the surface with the runtime (#1878): `owner`-type rules and `group` / `guest` recipients — previously declared but skipped at seed time — no longer parse; `group` became the enforced `team` and `business_unit` joined the enum. See [Sharing Rules](/docs/permissions/sharing-rules#recipient-types).
159+
**Enforcement status:** every authorable rule and recipient type is enforced. v17 reconciled the surface with the runtime (#1878): `owner`-type rules (`type: 'owner'`, `ownedBy`) and `group` / `guest` recipients — previously declared but skipped at seed time — **no longer parse**; `group` became the enforced `team` and `business_unit` joined the enum. That is a stronger statement than "declared but not enforced": a skipped rule is still authorable and is ignored, whereas a removed one is rejected by `SharingRuleSchema`, so a stale definition fails loudly at authoring time instead of silently doing nothing (ADR-0078). See [Sharing Rules](/docs/permissions/sharing-rules#recipient-types).
161160
</Callout>
162161

163162
<Callout type="info">
@@ -171,7 +170,7 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative
171170
sharingRules: [
172171
{
173172
name: 'high_value_opps_to_vp',
174-
object: 'opportunity',
173+
object: 'opportunity', // sharingModel: 'private'
175174
type: 'criteria',
176175
// condition is a CEL predicate over the record
177176
condition: 'record.amount > 100000',
@@ -180,17 +179,31 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative
180179
},
181180
{
182181
name: 'west_accounts_to_directors',
183-
object: 'account',
184-
type: 'owner',
185-
// records owned by this group/position are the source set
186-
ownedBy: { type: 'position', value: 'west_region_rep' },
182+
object: 'account', // sharingModel: 'public_read'
183+
type: 'criteria',
184+
// the source set is a field predicate — record ownership is not an
185+
// authorable rule input; see the enforcement callout above
186+
condition: 'record.region == "west"',
187187
sharedWith: { type: 'position', value: 'sales_director' },
188188
accessLevel: 'edit',
189189
},
190190
],
191191
}
192192
```
193193

194+
<Callout type="warn">
195+
**Parsing is not granting — check the target object's OWD.** A sharing rule only
196+
widens what the baseline still withholds, so both rules above grant because of
197+
their objects' postures: `opportunity` is `private`, so both gates apply and the
198+
`read` rule widens the read filter; `account` is `public_read`, which is
199+
*read-open but write-owned*, so read is already universal there and it is the
200+
`edit` level that opens the write gate to non-owners. On a `public_read_write`
201+
or `controlled_by_parent` object both gates are already open, and a rule parses,
202+
materializes, and grants **nothing**`effectiveSharingModel(schema) === 'public'`
203+
short-circuits `buildReadFilter` and `buildWriteFilter` to `null`
204+
(`packages/plugins/plugin-sharing/src/sharing-service.ts`).
205+
</Callout>
206+
194207
---
195208

196209
## 5. Organization-Wide Defaults (OWD)

content/docs/protocol/objectql/security.mdx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -372,30 +372,47 @@ object: account
372372
accessLevel: read # read | edit
373373
condition: 'record.account_type == "Enterprise"'
374374
sharedWith:
375-
type: position # user | group | position | unit_and_subordinates | guest
375+
type: position # user | team | position | unit_and_subordinates | business_unit
376376
value: sales_rep
377377
```
378378

379379
`unit_and_subordinates` expands a **business-unit subtree**: the unit named by `value` plus every descendant unit's members (ADR-0057 D5 / ADR-0090 D3 — the former position-tree walk was re-homed onto the `sys_business_unit` tree).
380380

381-
### Owner-Based Sharing
381+
### Owner-Based Sharing — removed in v17
382382

383-
Share records owned by one group with another (`OwnerSharingRuleSchema`):
383+
Owner-based rules (`type: 'owner'`, `ownedBy`) were removed from the authoring
384+
surface in v17 (#1878), together with the `group` / `guest` recipients. They
385+
depended on live membership the static seeder cannot track, so they validated
386+
but never materialized a share. Use a criteria rule or a scope-depth grant
387+
instead; none of these shapes parses anymore, so a stale definition fails
388+
loudly at authoring time instead of silently doing nothing (ADR-0078).
389+
390+
> **"Skipped" and "rejected" are different instructions to an author.** Before
391+
> v17 these rules were *declared but skipped at seed time*: you could write one,
392+
> it parsed, and the seeder ignored it. They are not skipped now — they **do not
393+
> parse**. `SharingRuleSchema` rejects the block above with three issues:
394+
> `type: 'owner'` fails the `criteria` literal, the required `condition` is
395+
> reported missing, and `ownedBy` comes back as an unrecognized key carrying its
396+
> own guidance message. There is also no `OwnerSharingRuleSchema` to point at —
397+
> `packages/spec/src/security/sharing.zod.ts` exports `CriteriaSharingRuleSchema`,
398+
> and `SharingRuleSchema` *is* that schema.
399+
400+
The retired `share_west_region` rule — "West-region accounts reach the regional
401+
managers" — is expressed by predicating the field instead of the owner:
384402

385403
```yaml
404+
# share_west_region.sharing.yml
386405
name: share_west_region
387-
type: owner
388-
object: account
406+
type: criteria
407+
object: account # sharingModel: private, declared above
389408
accessLevel: edit
390-
ownedBy:
391-
type: position
392-
value: west_region_reps
409+
condition: 'record.region == "west"'
393410
sharedWith:
394411
type: position
395412
value: west_region_managers
396413
```
397414

398-
> **Enforcement status.** Criteria rules with `user` / `position` / `unit_and_subordinates` recipients compile and enforce (the CEL condition lowers to a runtime filter that materializes `sys_record_share` grants, ADR-0058 D3). Owner-type rules and `group`/`guest` recipients are `[experimental — not enforced]`: the seed bootstrap skips them (logged) rather than seeding a permissive match-all (ADR-0049).
415+
> **Enforcement status.** Every authorable rule and recipient type is enforced. Criteria rules with `user` / `team` / `position` / `unit_and_subordinates` / `business_unit` recipients compile and enforce (the CEL condition lowers to a runtime filter that materializes `sys_record_share` grants, ADR-0058 D3). Owner-type rules and the `group` / `guest` recipients are **not** `[experimental — not enforced]` and are no longer skipped at seed time — v17 removed them from the schema, so they do not parse at all (see above). What is still skipped-and-logged is a `condition` the compiler cannot lower (functions, cross-object traversal): it is never seeded as a permissive match-all (ADR-0049).
399416

400417
> `accessLevel` is one of `read` or `edit`. Sharing widens **which rows** a principal reaches, never **which verbs** they may use — an `edit` share opens *update*, not *delete*: delete comes from ownership, the ADR-0057 DEPTH scopes, or the `modifyAllRecords` bypass, enforced by the sharing layer's own `canDelete` gate (distinct from the `canEdit` update gate) on top of the object-level CRUD gate (ADR-0111 D3). A third level `full` ("Full Access — transfer/share/delete") was authorable through protocol 16 but never granted any of those verbs: both enforcement sites matched `edit`/`full` alike, so it was equivalent to `edit` while telling admins otherwise, and it was removed (#3865, ADR-0078). Stacks still authoring it are rewritten to `edit` at load by the `sharing-rule-access-level-full-to-edit` conversion.
401418

0 commit comments

Comments
 (0)