Skip to content

sys_business_unit_member is unadjudicated in PLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570

Description

@baozhoutao

Blocked-by: #15195

Filed as an out-of-scope finding while working #14547 (sharing rules with a business-unit recipient). Unassigned; recording, not claiming.

What was measured

sys_business_unit_member carries an organization_id column — applySystemFields injects it unconditionally for a managedBy: 'platform' object, and the schema in packages/platform-objects/src/identity/sys-business-unit-member.object.ts declares no tenancy opt-out. Whether a WRITER fills it depends entirely on the path:

write path stamps organization_id? why
REST / session write YES the engine threads execCtx.tenantId into DriverOptions, and the SQL driver's injectTenantOnInsert fills the injected column
seed replay NO packages/metadata-protocol/src/seed-loader.ts withholds its single-org fallbackOrgId from every sys_ / cloud_ / ai_ object, so a seeded membership lands org-less unless the replay pinned an organization or the record spells the column itself
elevated (system-context) write NO sys_business_unit_member is unclassified in PLATFORM_OBJECT_TENANCY (packages/objectql/src/tenancy/platform-object-tenancy.ts), so Engine.resolveSystemInsertOrganization returns early and stamps nothing
driver-memory / driver-mongodb NO neither implements a tenant column at all (both refuse to boot multi-tenant, which is what makes that safe)

The repo's own dogfood fixture is an instance of row 3: packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts inserts membership rows under a bare isSystem context and they land org-less, while the sys_business_unit rows beside them are explicitly stamped.

Why it matters now

sys_business_unit_member is the population an authorization path enumerates. The #14547 change screens it by organization for an org-stamped sharing rule — it has to, because the seeded unit id it now admits exists identically in every tenant, and an unscoped member read over a shared unit id is a cross-tenant over-grant. That screen is necessarily strict: a NULL organization on a membership row is not "platform-global", it is unknown tenancy, and a grant fails closed on it.

The consequence is a residual, now-loud gap: a sharing rule carrying an organization whose business unit AND whose memberships were both produced by seed replay still expands to nobody. SharingRuleService.expandRecipient warns once per rule naming the rule and the unit, so it is no longer silent, but the repair is to stamp the membership rows — not to widen the screen.

What the adjudication is

⚠️ Superseded in kind by ADR-0131 (merged 2026-09-04, #14976) — see the re-aim note in the comments. The paragraph below records the question as it was asked, and is kept for provenance rather than as a live ask.

The PLATFORM_OBJECT_TENANCY header states that an object whose tenancy cannot be determined is LISTED for adjudication rather than guessed either way (2026-08-31 ruling, execution point 2: 「判不了的逐个列出回批呈裁,⛔ 不猜」). This is that listing for one object. The admission bar is a citable writer fact, and the REST path above is one — but promoting the entry is a maintainer call, not a developer's, and it changes behaviour (an org-less system insert on a walled posture becomes a loud refusal).

Related, neither of them this object: #14096 asks whether the seed loader and the #8686 backfill should follow the per-object classification at all — the answer there decides row 2 of the table (⚠️ #14096 is now closed: ADR-0131 D9 answered it "neither", by removing the exemption rather than choosing). #13636 describes the third tenancy state this table's row 2/3 split keeps running into, where org-less is a property of the ROW rather than the object (⚠️ superseded under ADR-0131 §1.6 / C11).

Not proposed here

No backfill of existing rows. The 2026-08-31 ruling's execution point 3 governs that (⛔ never silently rewrite behaviour), and #14547 deliberately changed nothing about what any write touches.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions