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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
8 changes: 4 additions & 4 deletions .changeset/bu-tenant-screen-relanding.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
'@objectstack/plugin-sharing': patch
---

Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data — and tenant-screen the member reads that widening exposes.
Tenant-screen the `sys_business_unit_member` reads, and warn when an active business-unit rule grants nobody.

`BusinessUnitGraphService.orgScope` screened `sys_business_unit` with a strict `organization_id` equality, while the platform's own read-side chokepoint (`SqlDriver.applyTenantScope`) is null-inclusive: `(organization_id = ? OR organization_id IS NULL)`, because a NULL organization marks a platform/seeded row every tenant may see. A sharing rule always carries the caller's organization, but a business unit written by seed data carries none — a seed cannot know the id the runtime mints at boot — so the two never matched. The seed check read the unit as "does not exist", both recipient widths (`business_unit` and `unit_and_subordinates`) expanded to zero users, and the rule stayed active having materialised no `sys_record_share` row and logged nothing. `orgScope` now applies the platform's null-inclusive screen, the same predicate `plugin-approvals` already applies to these very rows and `SharingRuleService.adminOrgScope` applies to the rule table.
⚠️ This entry originally also announced that `BusinessUnitGraphService.orgScope` had been widened to the platform's null-inclusive `(organization_id = ? OR organization_id IS NULL)` screen, fixing the case where a sharing rule naming a **seed-written** business unit granted nobody. That half was **reverted before 17.3 was cut** and does not ship: it re-implemented, a second time and in a second place, the predicate `SqlDriver.applyTenantScope` already owns — the duplication the v18 org-ownership decision (PR #14976) exists to retire — and it had not been released. `orgScope` keeps the strict `organization_id` equality 17.2.0 ships, so a rule naming a seeded unit still expands to nobody. That defect is fixed structurally on the v18 line by the v18 org-ownership decision (PR #14976), C1 (the Default Organization exists before application seed datasets load, and the seed loader stamps `sys_business_unit` seeds). What follows is the part of this change that DOES ship.

The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. Widening the unit screen alone would have turned a silent under-grant into a silent cross-tenant over-grant, since a seeded unit id exists identically in every tenant. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.
The member reads are now tenant-screened, which they were not before. Both `expandUnitMembers` and `expandUsers` queried `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either, so the strict unit screen was the only thing keeping an org-stamped rule away from that unscoped query. This matters independently of the unit screen: other organizations' member rows sit on org-stamped — visible — units too, so a strict unit screen narrows which units are reachable but does not close the unscoped member read. The member screen is strict rather than null-inclusive on purpose: seed replay and elevated system writes both leave `sys_business_unit_member.organization_id` NULL, so a NULL there means unknown tenancy rather than platform-global, and an org-scoped rule does not grant to it. The sibling recipient widths already read their membership rows this way.

An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That case — a rule whose unit and membership rows were both seeded — is the one combination that still grants nobody, and it is no longer silent.
An active business-unit rule that expands to no recipients now warns once per rule per process, naming the rule, the object, the recipient kind, the unit and the organization. That is what keeps the remaining seed-data symptom observable at the moment it happens, rather than surfacing only as "the right people cannot see the record".
30 changes: 30 additions & 0 deletions .changeset/revert-null-inclusive-business-unit-screen.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
"@objectstack/plugin-sharing": patch
---

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added after 17.2.0

17.3 does not ship the NULL-inclusive business-unit screen added after 17.2.0;
#14547 remains as in 17.2.0 and is fixed structurally in v18 (the v18 org-ownership decision (PR #14976), C1: the
Default Organization exists before application seed datasets load, and the seed
loader stamps `sys_business_unit` seeds).

`BusinessUnitGraphService.orgScope` briefly read
`$or: [{ organization_id: <rule org> }, { organization_id: null }]` so that an
org-stamped sharing rule could name a seeded (org-less) `sys_business_unit`
row. It is restored to the strict `organization_id = <rule org>` equality
17.2.0 ships. That shape re-implemented, a second time and in a second place,
the predicate `SqlDriver.applyTenantScope` already owns — the duplication
the v18 org-ownership decision (PR #14976) exists to retire (#10103 cause 1) — and it had not been released, so
reverting costs nothing while shipping it would have owed v18 a breaking change
and a migration.

The other half of the same change is KEPT and is not touched:
`BusinessUnitGraphService.memberScope` still screens both
`sys_business_unit_member` reads with a strict equality. Those reads previously
carried no organization predicate at all, so an org-stamped rule reaching any
visible unit collected every tenant's membership rows hanging off it; a strict
unit screen narrows which units are reachable but does not close that, because
other organizations' member rows sit on org-stamped units too.
`SharingRuleService.warnOnEmptyUnitExpansion` is also kept: it is what keeps the
remaining #14547 symptom loud instead of silent.
Loading
Loading