You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(plugin-sharing): make package-seeded (org-null) sharing rules visible and addressable by name (#7760)
Rules seeded from an app or package are defined under the system context, so
they are stored with `organization_id = null` (platform-global). `listRules`
and the by-name fallback of `getRule` scoped their reads with a strict
`organization_id = <caller org>` equality, which such a row can never satisfy:
an org-scoped admin saw `GET /api/v1/sharing/rules` answer `{"data":[]}` over a
table of active seeded rules, and by-name GET/evaluate answered 404
`RULE_NOT_FOUND`. Only the by-id branch, which carries no org filter, worked.
Enforcement was unaffected — the boot reconcile also reads under the system
context — which is exactly why this stayed invisible.
Both admin reads now match "this organization OR platform-global", the same
predicate `sys_business_unit` approver expansion settled on in #3807 and the
one `sys_metadata`'s pending-draft listing uses. Another organization's row
still fails the match; only rows belonging to no organization become visible.
`defineRule` is deliberately NOT widened. Its existence lookup decides
upsert-vs-insert, so widening it would let one organization's admin rewrite a
row every other organization reads — a cross-tenant WRITE, a different act from
a cross-tenant read of a platform-global row. A same-named create therefore
still produces a row stamped with the caller's own organization, and
`findRuleRowByName` prefers that row over the platform-global one via two
sequenced lookups rather than one `$or` with `limit: 1`, so the preference is a
decision rather than whichever row a dialect happened to reach first.
The test fake engine's filter matcher short-circuited on `$or` and DROPPED its
sibling field keys, so `listRules`'s `{object_name, active, $or:[…]}` would have
matched the whole table there while driver-sql and driver-memory conjoin the
two. A fake looser than the contract it stands in for is how a green suite
ships a broken filter, so it now conjoins.
Fixes#7676
Claude-Session: https://claude.ai/code/session_01BVc1ekPpi6yaWywAUhfzfd
Co-authored-by: Claude <noreply@anthropic.com>
Make package-seeded sharing rules visible and addressable by name to org-scoped admins
6
+
7
+
Sharing rules seeded from an app or package are defined under the system context, so they are stored with `organization_id = null` (platform-global). `SharingRuleService.listRules` and the by-name fallback of `getRule` scoped their reads with a strict `organization_id = <caller org>` equality, which such a row can never satisfy. An authenticated org-scoped admin therefore saw `GET /api/v1/sharing/rules` return an empty list over a table of active seeded rules, and by-name `GET` and `evaluate` answered 404 `RULE_NOT_FOUND`; only the by-id branch, which was never org-scoped, still worked.
8
+
9
+
Both admin reads now match "this organization OR platform-global", mirroring how enforcement has always read these rows under the system context. Consequences worth knowing:
10
+
11
+
- Seeded rules now appear in the admin rule list and can be fetched, evaluated and deleted by name. An org admin could already do all three **by row id** — the by-id branch carries no org filter — so this adds an address form and discoverability, not a new authority. Deleting a package-seeded rule remains reversible: the next boot reseeds it.
12
+
- Rules belonging to a **different** organization remain invisible and unresolvable by name; only rows belonging to no organization at all become visible.
13
+
-`defineRule` is deliberately **not** widened. Its existence lookup decides upsert-vs-insert, so widening it would let one organization's admin rewrite a row every other organization reads. A same-named create still produces a row stamped with the caller's own organization, and by-name lookups prefer that row over the platform-global one.
14
+
- Callers passing a context with no organization (boot seeding, rule hooks, backfills, the boot reconcile) are unaffected — that path was already unfiltered and is unchanged.
0 commit comments