Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions content/docs/permissions/permissions-matrix.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,15 +149,14 @@ Field-level security controls visibility and editability of individual fields pe

## 4. Sharing Rule Types

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).
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).

| Mechanism | `type` | Description | Example |
|:---|:---|:---|:---|
| **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" |
| **Criteria-Based** | `criteria` | Share records matching a CEL predicate over field values | All opportunities where `record.amount > 100000` are shared with "VP Sales" |

<Callout type="warn">
**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).
**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).
</Callout>

<Callout type="info">
Expand All@@ -171,7 +170,7 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative
sharingRules: [
{
name: 'high_value_opps_to_vp',
object: 'opportunity',
object: 'opportunity', // sharingModel: 'private'
type: 'criteria',
// condition is a CEL predicate over the record
condition: 'record.amount > 100000',
Expand All@@ -180,17 +179,31 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative
},
{
name: 'west_accounts_to_directors',
object: 'account',
type: 'owner',
// records owned by this group/position are the source set
ownedBy: { type: 'position', value: 'west_region_rep' },
object: 'account', // sharingModel: 'public_read'
type: 'criteria',
// the source set is a field predicate — record ownership is not an
// authorable rule input; see the enforcement callout above
condition: 'record.region == "west"',
sharedWith: { type: 'position', value: 'sales_director' },
accessLevel: 'edit',
},
],
}
```

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

---

## 5. Organization-Wide Defaults (OWD)
Expand Down
35 changes: 26 additions & 9 deletions content/docs/protocol/objectql/security.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -372,30 +372,47 @@ object: account
accessLevel: read # read | edit
condition: 'record.account_type == "Enterprise"'
sharedWith:
type: position # user | group | position | unit_and_subordinates | guest
type: position # user | team | position | unit_and_subordinates | business_unit
value: sales_rep
```

`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).

### Owner-Based Sharing
### Owner-Based Sharing — removed in v17

Share records owned by one group with another (`OwnerSharingRuleSchema`):
Owner-based rules (`type: 'owner'`, `ownedBy`) were removed from the authoring
surface in v17 (#1878), together with the `group` / `guest` recipients. They
depended on live membership the static seeder cannot track, so they validated
but never materialized a share. Use a criteria rule or a scope-depth grant
instead; none of these shapes parses anymore, so a stale definition fails
loudly at authoring time instead of silently doing nothing (ADR-0078).

> **"Skipped" and "rejected" are different instructions to an author.** Before
> v17 these rules were *declared but skipped at seed time*: you could write one,
> it parsed, and the seeder ignored it. They are not skipped now — they **do not
> parse**. `SharingRuleSchema` rejects the block above with three issues:
> `type: 'owner'` fails the `criteria` literal, the required `condition` is
> reported missing, and `ownedBy` comes back as an unrecognized key carrying its
> own guidance message. There is also no `OwnerSharingRuleSchema` to point at —
> `packages/spec/src/security/sharing.zod.ts` exports `CriteriaSharingRuleSchema`,
> and `SharingRuleSchema` *is* that schema.

The retired `share_west_region` rule — "West-region accounts reach the regional
managers" — is expressed by predicating the field instead of the owner:

```yaml
# share_west_region.sharing.yml
name: share_west_region
type: owner
object: account
type: criteria
object: account # sharingModel: private, declared above
accessLevel: edit
ownedBy:
type: position
value: west_region_reps
condition: 'record.region == "west"'
sharedWith:
type: position
value: west_region_managers
```

> **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).
> **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).

> `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.

Expand Down
Loading