Skip to content

fix(tenancy): platform-global (tenancy.enabled:false) objects are never driver-org-scoped (#3249)#3273

Merged
os-zhuang merged 2 commits into
mainfrom
claude/tenant-scope-null-org-rows-3r3ohy
Jul 19, 2026
Merged

fix(tenancy): platform-global (tenancy.enabled:false) objects are never driver-org-scoped (#3249)#3273
os-zhuang merged 2 commits into
mainfrom
claude/tenant-scope-null-org-rows-3r3ohy

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #3249.

Problem

On the cloud control plane, a platform admin (whose ExecutionContext carries an active-org tenantId) reads sys_license and gets 0 rows, while an anonymous request to the same endpoint returns the row. sys_license is declared tenancy: { 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:

  1. Engine: buildDriverOptions stamped execCtx.tenantId into driver options unconditionally — it never received the object name and never consulted the object's tenancy posture.
  2. SQL driver: the tenant field is resolved from a write-once cache (tenantFieldByTable). computeTenantField correctly returns null for tenancy.enabled === false, but any re-registration with a tenancy-stripped schema — e.g. the lifecycle archive path (lifecycle-service.ts passes a LifecycleObjectLike with only { name, fields } to cold.syncSchema), or a schema-drift re-sync — falls through to the implicit organization_id heuristic 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 — new isTenancyDisabled(schema) export from @objectstack/spec/data: single source of truth for the ADR-0066 platform-global posture. Previously the tenancy?.enabled === false semantic was re-derived independently in the registry, the engine, and the driver.
  • @objectstack/objectqlbuildDriverOptions is now object-aware and withholds execCtx.tenantId for objects whose registered schema declares tenancy.enabled: false. This protects every driver at the source, independent of per-driver cache state. An explicitly-passed base tenantId still wins (deliberate caller intent). registry.ts now uses the shared predicate for tenant-column injection.
  • @objectstack/driver-sql (inherited by driver-sqlite-wasm) — sticky opt-out record (tenantOptOutByTable): a registration that carries a tenancy declaration is authoritative (sets/clears the record); a registration without one preserves a previously declared opt-out instead of re-scoping via the organization_id heuristic. Rotation shards inherit the base table's opt-out.
  • The orWhereNull global-row inclusion (Fresh objectstack dev boot: 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/2882 carry the same inline posture check combined with systemFields.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

  • driver-sql (sql-driver-tenant-scope.test.ts): the previously untested corruption path — a tenancy-less syncSchema / registerExternalObject re-registration preserves the opt-out (scoped read still returns the NULL-org + cross-org rows); an explicit tenancy.enabled:true re-registration is authoritative and re-enables scoping.
  • driver-sqlite-wasm: ported the entire missing tenancy.enabled:false describe block (incl. the sticky re-registration regressions) — the subclass had no coverage for this posture.
  • objectql (engine.test.ts): tenantId is withheld for tenancy-disabled objects on reads and writes; still stamped for undeclared / enabled:true objects; timezone and the rest of the context still thread; explicit base tenantId still reaches the driver.
  • spec: unit tests for isTenancyDisabled.

Verification

  • pnpm --filter @objectstack/spec test — 254 files / 6778 tests pass
  • pnpm --filter @objectstack/driver-sql test — 32 files / 284 tests pass
  • pnpm --filter @objectstack/driver-sqlite-wasm test — 10 files / 125 tests pass
  • pnpm --filter @objectstack/objectql test — 72 files / 1016 tests pass
  • pnpm build — all 71 packages build
  • pnpm test — full suite green except one load-flaky 20s-timeout in plugin-audit's i18n summary test (framework#3039 area, unrelated; passes in isolation)

A @objectstack/spec minor changeset is included for the new isTenancyDisabled export.

🤖 Generated with Claude Code

https://claude.ai/code/session_017YcGcBgUxeeKXXxv189kqR


Generated by Claude Code

…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
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 19, 2026 12:23pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/m labels Jul 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql, @objectstack/driver-sqlite-wasm)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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
os-zhuang marked this pull request as ready for review July 19, 2026 12:35
@os-zhuang
os-zhuang merged commit 86d30af into main Jul 19, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/tenant-scope-null-org-rows-3r3ohy branch July 19, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Driver-native tenant scope hides platform-global (NULL-org) rows from an org-context read (sys_license: admin sees 0, anon sees row)

2 participants