From d237370527f8becebfb8faa5c9f5995cb58f9620 Mon Sep 17 00:00:00 2001 From: os-steve Date: Wed, 19 Aug 2026 06:51:44 +0000 Subject: [PATCH] docs(permissions,protocol): owner-type sharing rules are rejected at parse, not skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja --- .../docs/permissions/permissions-matrix.mdx | 29 ++++++++++----- content/docs/protocol/objectql/security.mdx | 35 ++++++++++++++----- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/content/docs/permissions/permissions-matrix.mdx b/content/docs/permissions/permissions-matrix.mdx index a7e88796a2..80d42a0d1d 100644 --- a/content/docs/permissions/permissions-matrix.mdx +++ b/content/docs/permissions/permissions-matrix.mdx @@ -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" | -**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). @@ -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', @@ -180,10 +179,11 @@ 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', }, @@ -191,6 +191,19 @@ Sharing rules extend access beyond ownership and the depth axis. The declarative } ``` + +**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`). + + --- ## 5. Organization-Wide Defaults (OWD) diff --git a/content/docs/protocol/objectql/security.mdx b/content/docs/protocol/objectql/security.mdx index 45707cb445..9a10b8bc91 100644 --- a/content/docs/protocol/objectql/security.mdx +++ b/content/docs/protocol/objectql/security.mdx @@ -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.