Skip to content
11 changes: 11 additions & 0 deletions .changeset/bu-tree-share-recompute.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
'@objectstack/plugin-sharing': patch
---

Recompute `unit_and_subordinates` / `business_unit` sharing-rule grants when the business-unit graph changes.

**Security:** read access granted through a business-unit sharing rule is now withdrawn immediately when the business unit is moved out of the shared subtree, deactivated or deleted, or when the membership row is removed — previously it survived until the shared record happened to be written next, which put no bound at all on how long a revoked recipient kept reading the record.

The sharing-rule recompute hooks were registered only on each rule's own object, and nothing was registered on `sys_business_unit` or `sys_business_unit_member`. A rule whose recipient resolves through the business-unit tree therefore never re-materialised its `sys_record_share` grants when the tree or a membership moved. Writes to both tables now drive a recompute: the withdrawal is synchronous and complete before the write returns (scoped by recipient, so it needs no scan of the records the rule matches), and the grant direction — a unit moved *into* a shared subtree — is queued on the existing re-grant queue.

Only recipient kinds that actually read the business-unit graph are recomputed (`business_unit`, `unit_and_subordinates`); `user`, `team`, `position` and `queue` rules are untouched by a business-unit write.
3 changes: 2 additions & 1 deletion content/docs/permissions/sharing-rules.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,12 +166,13 @@ unaffected.
A sharing rule's grants are **materialized** — evaluating a rule writes real
`sys_record_share` rows with `source: 'rule'` and `source_id` set to the rule.
Because those rows outlive the evaluation that produced them, withdrawal has to
be an explicit act, and it happens at **three** moments:
be an explicit act, and it happens at **four** moments:

| When | What is reconciled |
|:--|:--|
| **The write that deactivates or edits the rule** | That rule's grants, immediately — deactivating with `active: false` (or `POST {basePath}/sharing/rules` with the same name) revokes them before the call returns |
| **The next insert/update of a matching record** | That record's grants for every rule on the object — an inactive rule desires nothing, so its rows are revoked |
| **A `sys_business_unit` or `sys_business_unit_member` write** | The grants of rules whose recipients read the BU graph — `unit_and_subordinates` and `business_unit`, and only those two. Recipients the rule no longer reaches are revoked **before the write returns**; the grant direction (a unit moved *into* a shared subtree) is queued and coalesced per rule (objectstack#7729) |
| **Every boot** | All rules, plus a sweep of `source: 'rule'` rows whose `source_id` no longer resolves to any rule |

Deleting a rule withdraws its grants too, whether you delete it through
Expand Down
Loading
Loading