Skip to content

Two MORE clean inheritors of the #8323 class the #8554 sweep missed: sys_notification_subscription and sys_audience_binding_suggestion — both measured live #8577

Description

@os-zhuang

Found while implementing #8554 (the five-instance batch), by re-running that card's own executed sweep and then probing every tenant-scoped finding instead of eyeballing which ones looked like authored names. Not fixed there#8554's R5 fixes its scope at exactly five objects, so these are filed rather than folded in.

Per the maintainer ruling of 2026-08-13 on #8468, both objects below are the ruled shape — tenant-scoped, no tenancy opt-out, bare unique: true on a declared index over authored content — so they inherit that answer and go straight to the lane queue. Neither carries a semantic argument for an installation-wide namespace. The judgement cases the same sweep turned up are filed separately.

Both are measured, not read

Real engine (SqlDriver, better-sqlite3), OS_TENANCY_POSTURE=isolated, driving the real shipped declarations loaded from source.

1. sys_notification_subscriptionpackages/services/service-messaging/src/objects/notification-subscription.object.ts

indexes: [{ fields: ['topic', 'principal'], unique: true }, { fields: ['topic'] }]
materialized: uniq_sys_notification_subscription_topic_principal on (topic, principal)
org_jia creates (billing.invoice, role:sales_manager) → 201
org_yi creates the SAME pair → 409 UNIQUE_VIOLATION
org_yi creates (billing.invoice, role:only_yi) → 201 ← control
org_yi creates (billing.invoice, user:u1) → 201 ← control
org_yi GET on the colliding pair → total 0

This is the direct sibling of sys_notification_preference, one of #8554's five: same package, same directory, same ADR-0030 Layer 3, same managedBy: 'system-data', same archetype. Its own header says the rows are "Admin/user-writable DATA on a platform-defined schema: authored from the Setup 'Notification Subscriptions' grid" — admin-authored content by the ruling's own phrase.

principal is role:x / team:x / user:id / a bare user id. Note what that means now: role and position names are per-organization (#8461, #8556), so role:sales_manager denotes a different principal in each organization while colliding on one installation-wide key. And a user in two organizations cannot subscribe to the same topic in both — the exact symptom measured on sys_user_preference in #8323.

2. sys_audience_binding_suggestionpackages/plugins/plugin-security/src/objects/sys-audience-binding-suggestion.object.ts

indexes: [{ fields: ['package_id', 'permission_set_name', 'anchor'], unique: true }, …]
materialized: uniq_sys_audience_binding_suggestion_79a05fef
on (package_id, permission_set_name, anchor) ← hash-suffixed, name is over 60 chars
org_jia creates (com.acme.crm, sales_readonly, everyone) → 201
org_yi creates the SAME triple → 409 UNIQUE_VIOLATION
org_yi creates (com.acme.crm, other_set, everyone) → 201 ← control

⚠️This one is worse than a naming oracle, and I think it is the most serious finding in the whole #8323 lineage so far. The key is not a name two tenants happen to choose alike — it is package_id + the package's OWN permission-set name + anchor, i.e. exactly the same triple for every tenant that installs the same package. The row is per-tenant by construction (the object's header: rows are produced when a package's declaration is observed, and "resolved when a tenant admin confirms").

So the consequence is not an enumeration side channel, it is a functional dead end on the package-install path: the second and every later organization to install a package silently never gets its binding suggestion row, so its admin is never asked to bind the package's default permission set to the everyone anchor, and its users never receive that set. ADR-0090 D5/D9 exists precisely so this is never auto-bound; the effect here is that for every tenant after the first, it is never bound at all and nothing says so.

This does not depend on #8554 landing — package-declared set names are identical across tenants today. #8554 widens the collision surface (admin-authored set names become per-organization and may now legitimately repeat), but the dead end above is live on main right now.

Fix shape (unchanged from #8461 / #8468 / #8554)

  1. Respell each to unique: 'organization', spelled explicitly.
  2. fix(platform-objects,plugin-security,driver-sql): scope sys_user_preference and sys_capability uniqueness per organization (#8323) #8461's replace_unique_index arm covers both unchanged — verified on the five of Five more instances of the #8323 class: admin- and user-authored names on tenant-scoped objects still carry installation-wide unique indexes #8554 without touching schema-drift.ts, including a 3-column composite and a hash-suffixed replacement name.
  3. ⚠️ The pin must exercise the deployed-installation migration path, not only a fresh database — respelling changes the index's generated name, so drift otherwise reads as two findings and an operator applying only the safe half keeps the defect while the plan reads as applied.
  4. Check published text for uniqueness claims. Neither object's declaration carries one in its own field text; the generated reference pages should be re-checked at fix time.

⚠️sys_audience_binding_suggestion's replacement index name is hash-suffixed (buildIndexName truncates past INDEX_NAME_MAX = 60), as sys_notification_preference's is in #8554. That is handled by the existing arm — measured — but it is the case where a name-based assumption would hide.

Related

Filed unassigned. Found by session session_012WMpuAfA2KSdDjGF6tm1bH.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions