fix(tenancy): platform-global (tenancy.enabled:false) objects are never driver-org-scoped (#3249)#3273
Merged
Merged
Conversation
…er driver-org-scoped (#3249) An org-context read of a platform-global object (e.g. sys_license, ADR-0066) could return 0 rows for an authenticated caller while an anonymous read saw the data: the engine stamped execCtx.tenantId into driver options unconditionally, and the SQL driver's tenant-field cache could be re-corrupted to organization_id by a partial re-registration (lifecycle archive syncSchema, schema-drift re-sync) whose schema omitted the tenancy block — the implicit organization_id heuristic then org-scoped the table and its NULL-org rows vanished. - spec: new isTenancyDisabled(schema) export from @objectstack/spec/data — single source of truth for the ADR-0066 platform-global posture, shared by registry (tenant-column injection), engine (tenantId propagation) and drivers (native scoping). - objectql: buildDriverOptions is now object-aware and withholds execCtx.tenantId for tenancy-disabled objects (protects every driver at the source). An explicitly-passed base tenantId still wins. - driver-sql (+ inherited by driver-sqlite-wasm): sticky opt-out record (tenantOptOutByTable) — a registration carrying a tenancy declaration is authoritative; one without preserves a previously declared opt-out instead of falling back to the organization_id heuristic. Rotation shards inherit the opt-out. - tests: driver-sql sticky re-registration regressions; port the missing tenancy.enabled:false block to the sqlite-wasm suite; engine tests for tenantId withholding on reads and writes. The orWhereNull global-row inclusion (#2734) is unchanged as defense-in-depth. plugin-security's inline posture checks (security-plugin.ts:2794/2882) are a mechanical follow-up, not touched here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017YcGcBgUxeeKXXxv189kqR
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The check:api-surface CI gate flags any public-API delta; the new export from #3249 is intentional (see the accompanying changeset). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017YcGcBgUxeeKXXxv189kqR
os-zhuang
marked this pull request as ready for review
July 19, 2026 12:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3249.
Problem
On the cloud control plane, a platform admin (whose ExecutionContext carries an active-org
tenantId) readssys_licenseand gets 0 rows, while an anonymous request to the same endpoint returns the row.sys_licenseis declaredtenancy: { enabled: false }(platform-global, ADR-0066); the authz kernel applies no row filter — the rows vanish in the driver-native tenant-scope layer, below plugin-security:buildDriverOptionsstampedexecCtx.tenantIdinto driver options unconditionally — it never received the object name and never consulted the object's tenancy posture.tenantFieldByTable).computeTenantFieldcorrectly returnsnullfortenancy.enabled === false, but any re-registration with atenancy-stripped schema — e.g. the lifecycle archive path (lifecycle-service.tspasses aLifecycleObjectLikewith only{ name, fields }tocold.syncSchema), or a schema-drift re-sync — falls through to the implicitorganization_idheuristic and overwrites the cache with'organization_id'. Nothing ever recomputes it, so the platform-global object becomes org-scoped and its NULL-org rows are hidden from org-context reads.Fix
@objectstack/spec— newisTenancyDisabled(schema)export from@objectstack/spec/data: single source of truth for the ADR-0066 platform-global posture. Previously thetenancy?.enabled === falsesemantic was re-derived independently in the registry, the engine, and the driver.@objectstack/objectql—buildDriverOptionsis now object-aware and withholdsexecCtx.tenantIdfor objects whose registered schema declarestenancy.enabled: false. This protects every driver at the source, independent of per-driver cache state. An explicitly-passed basetenantIdstill wins (deliberate caller intent).registry.tsnow uses the shared predicate for tenant-column injection.@objectstack/driver-sql(inherited bydriver-sqlite-wasm) — sticky opt-out record (tenantOptOutByTable): a registration that carries atenancydeclaration is authoritative (sets/clears the record); a registration without one preserves a previously declared opt-out instead of re-scoping via theorganization_idheuristic. Rotation shards inherit the base table's opt-out.orWhereNullglobal-row inclusion (Freshobjectstack devboot: tenant admin sees ZERO rows in sys_position / sys_permission_set / sys_business_unit over REST (Setup Access Control renders empty) #2734) is unchanged, as defense-in-depth for legitimately scoped objects.Out of scope (mechanical follow-up):
plugin-security/src/security-plugin.ts:2794/2882carry the same inline posture check combined withsystemFields.tenant === false; converting them touches the RLS compiler and is left for a follow-up. The issue's optional apps/cloud mitigation is unnecessary once the engine guard lands.Tests
sql-driver-tenant-scope.test.ts): the previously untested corruption path — a tenancy-lesssyncSchema/registerExternalObjectre-registration preserves the opt-out (scoped read still returns the NULL-org + cross-org rows); an explicittenancy.enabled:truere-registration is authoritative and re-enables scoping.tenancy.enabled:falsedescribe block (incl. the sticky re-registration regressions) — the subclass had no coverage for this posture.engine.test.ts): tenantId is withheld for tenancy-disabled objects on reads and writes; still stamped for undeclared /enabled:trueobjects; timezone and the rest of the context still thread; explicit basetenantIdstill reaches the driver.isTenancyDisabled.Verification
pnpm --filter @objectstack/spec test— 254 files / 6778 tests passpnpm --filter @objectstack/driver-sql test— 32 files / 284 tests passpnpm --filter @objectstack/driver-sqlite-wasm test— 10 files / 125 tests passpnpm --filter @objectstack/objectql test— 72 files / 1016 tests passpnpm build— all 71 packages buildpnpm test— full suite green except one load-flaky 20s-timeout inplugin-audit's i18n summary test (framework#3039 area, unrelated; passes in isolation)A
@objectstack/specminor changeset is included for the newisTenancyDisabledexport.🤖 Generated with Claude Code
https://claude.ai/code/session_017YcGcBgUxeeKXXxv189kqR
Generated by Claude Code