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
The audience-binding-suggestion reconciler writes with a tenant-less system context, so one organization-less row serves every tenant — the second half of the #8577 install-path dead end #8617
Found while implementing #8577 (scoping sys_audience_binding_suggestion's declared unique index per organization), by measuring the install path end to end through the real reconciler rather than stopping at the driver-level 409/201 oracle. Filed separately because #8577's scope is exactly the two index respellings; nothing here is fixed there.
#8577 makes the storage able to hold one suggestion row per organization: (package_id, permission_set_name, anchor) stops being an installation-wide key. Measured with a tenant-threaded execution context on a real ObjectQL + SqlDriver engine, driving the real syncAudienceBindingSuggestions:
PRE-fix org_jia sync -> created 1 | org_yi sync -> created 0, org_yi sees 0 rows
POST-fix org_jia sync -> created 1 | org_yi sync -> created 1, each sees its own
But the shipped call path never threads a tenant.packages/plugins/plugin-security/src/suggested-audience-bindings.ts reads and writes through a module-level constant:
constSYSTEM_CTX={isSystem: true};
and security-plugin.ts invokes the reconciler with the bare engine at boot and after a package-door permission publish. listAudienceBindingSuggestions gates on the caller's identity but then reconciles with SYSTEM_CTX too.
Measured on a real engine (better-sqlite3, OS_MULTI_ORG_ENABLED=true, OS_TENANCY_POSTURE=isolated)
measurement
insert under { isSystem: true }
stores organization_idNULL
insert under { isSystem: true, tenantId: X }
stores organization_id = X
find under { isSystem: true }
sees every organization's rows
find under { isSystem: true, tenantId: X }
sees X's rows and the NULL-organization rows
The last row is the platform's declared behaviour, not a surprise: the driver expands a tenant predicate to organization_id = :tenant OR organization_id IS NULL (ADR-0120 D3's platform bucket).
Running the reconciler exactly as shipped, twice, on the post-#8577 schema:
Identical before and after #8577. So on a shared-runtime multi-organization installation the surface holds one organization-less row that every tenant reads, and its consequences are:
The first tenant admin to confirm or dismiss flips that single row. Every other tenant's console then shows a resolved suggestion and never prompts them — while confirmAudienceBindingSuggestion created the actual sys_position_permission_set binding with the caller's context, i.e. in the confirming tenant's organization only. So for every other tenant the package's default permission set is still not bound, and the surface says it is.
findAnchorPositions resolves sys_position by name under the same tenant-less context with limit 1, so the everyone anchor a suggestion is checked against is whichever organization's row the driver returns first.
bindingExists therefore answers a question about some other tenant's binding.
This is the same functional dead end #8577 describes — the second organization's admin is never prompted — reached by a second, independent road. Fixing the index is necessary (without it even a correctly tenant-scoped write is refused) but it is not sufficient on this deployment shape.
Suggested shape (not a decision — the tenancy question is the maintainer's)
Two readings, and they lead to different architectures, so this needs the ruling before code:
B. The suggestion surface is installation-wide (one row per package x set x anchor, deliberately organization-less). Then the object should declare a tenancy opt-out and its index should be an explicit unique: 'global' — and the per-tenant prompt has to live somewhere else, because one row cannot carry N tenants' decisions.
B contradicts the object's declared shape and ADR-0090 D5/D9's per-tenant confirm gate, so A looks right, but the choice is a public-contract question about the object.
Pinned, not just described
packages/plugins/plugin-security/src/suggested-audience-bindings-install-path.test.ts (added by #8577) records today's behaviour in its section 3, the shipped SYSTEM_CTX call path is tenant-blind (recorded, not endorsed). Those two assertions are a record, not an endorsement: the fix for this issue must DELETE them, and if they still pass afterwards the fix did not work.
Found while implementing #8577 (scoping
sys_audience_binding_suggestion's declared unique index per organization), by measuring the install path end to end through the real reconciler rather than stopping at the driver-level 409/201 oracle. Filed separately because #8577's scope is exactly the two index respellings; nothing here is fixed there.What #8577 fixes, and what it does not
#8577 makes the storage able to hold one suggestion row per organization:
(package_id, permission_set_name, anchor)stops being an installation-wide key. Measured with a tenant-threaded execution context on a realObjectQL+SqlDriverengine, driving the realsyncAudienceBindingSuggestions:But the shipped call path never threads a tenant.
packages/plugins/plugin-security/src/suggested-audience-bindings.tsreads and writes through a module-level constant:and
security-plugin.tsinvokes the reconciler with the bare engine at boot and after a package-doorpermissionpublish.listAudienceBindingSuggestionsgates on the caller's identity but then reconciles withSYSTEM_CTXtoo.Measured on a real engine (better-sqlite3,
OS_MULTI_ORG_ENABLED=true,OS_TENANCY_POSTURE=isolated){ isSystem: true }organization_idNULL{ isSystem: true, tenantId: X }organization_id= X{ isSystem: true }{ isSystem: true, tenantId: X }The last row is the platform's declared behaviour, not a surprise: the driver expands a tenant predicate to
organization_id = :tenant OR organization_id IS NULL(ADR-0120 D3's platform bucket).Running the reconciler exactly as shipped, twice, on the post-#8577 schema:
Identical before and after #8577. So on a shared-runtime multi-organization installation the surface holds one organization-less row that every tenant reads, and its consequences are:
confirmAudienceBindingSuggestioncreated the actualsys_position_permission_setbinding with the caller's context, i.e. in the confirming tenant's organization only. So for every other tenant the package's default permission set is still not bound, and the surface says it is.findAnchorPositionsresolvessys_positionby name under the same tenant-less context withlimit 1, so theeveryoneanchor a suggestion is checked against is whichever organization's row the driver returns first.bindingExiststherefore answers a question about some other tenant's binding.This is the same functional dead end #8577 describes — the second organization's admin is never prompted — reached by a second, independent road. Fixing the index is necessary (without it even a correctly tenant-scoped write is refused) but it is not sufficient on this deployment shape.
Suggested shape (not a decision — the tenancy question is the maintainer's)
Two readings, and they lead to different architectures, so this needs the ruling before code:
organization_idis injected with no tenancy opt-out). Then the reconciler must run per organization with that organization's context, and every read in it must be tenant-scoped. This is the reading Two MORE clean inheritors of the #8323 class the #8554 sweep missed: sys_notification_subscription and sys_audience_binding_suggestion — both measured live #8577's respelling assumes.unique: 'global'— and the per-tenant prompt has to live somewhere else, because one row cannot carry N tenants' decisions.B contradicts the object's declared shape and ADR-0090 D5/D9's per-tenant confirm gate, so A looks right, but the choice is a public-contract question about the object.
Pinned, not just described
packages/plugins/plugin-security/src/suggested-audience-bindings-install-path.test.ts(added by #8577) records today's behaviour in its section 3,the shipped SYSTEM_CTX call path is tenant-blind (recorded, not endorsed). Those two assertions are a record, not an endorsement: the fix for this issue must DELETE them, and if they still pass afterwards the fix did not work.Related
sys_position.nameis the third instance of the #8323 class: an admin-authored name on a tenant-scoped RBAC object carries an installation-wide unique index #8468 / Five more instances of the #8323 class: admin- and user-authored names on tenant-scoped objects still carry installation-wide unique indexes #8554 — the tenant-scoped declared unique index class.Filed unassigned, unlabeled for triage. Found by session
session_012WMpuAfA2KSdDjGF6tm1bH(dev seat for #8577).