Skip to content

feat(driver-mongodb)!: 显式单租户 —— 检测到多租户模式即拒绝启动 (#3724) - #3734

Merged
os-zhuang merged 2 commits into
mainfrom
claude/mongodb-driver-multi-tenant-kikwui
Jul 28, 2026
Merged

feat(driver-mongodb)!: 显式单租户 —— 检测到多租户模式即拒绝启动 (#3724)#3734
os-zhuang merged 2 commits into
mainfrom
claude/mongodb-driver-multi-tenant-kikwui

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3724 —— 采用 issue 的方案 B:这个 driver 定位为单租户/嵌入式,检测到多租户模式就硬失败,而不是静默地不隔离。

为什么是 B

MongoDBDriver 从不读 DriverOptions.tenantId:读路径不加租户谓词,写路径不打租户戳。SQL driver 的 resolveTenantField + applyTenantScope 这一层在这里根本不存在,而它上面的一切——对象元数据的 tenancy 块、applySystemFields 注入的 organization_id、engine 往每次 driver 调用里穿的 tenantId——都按"租户隔离是平台保证"运作。多租户部署一旦把 datasource 指向 Mongo,每个查询都能读/改/删别的租户的文档,而且是静默的。

方案 B 成本低、立刻消除静默失败,且不必先赌 Mongo 的多租户需求真实存在。真有需求时再做 A(真正实现行级隔离)。

改了什么

新增 mongodb-tenancy-guard.ts(driver-mongodb)

信号 检查位置 结果
租户 posture 不是 singleOS_TENANCY_POSTURE=group|isolated,含由 OS_MULTI_ORG_ENABLED=true 推导出的) 构造函数connect() 再查一次(开 socket 之前) MongoDBMultiTenantUnsupportedError
对象声明 tenancy.enabled: true syncSchema() / syncSchemasBatch() 抛错,一次列出所有违规对象

几个刻意的决定:

  • resolveTenancyPosture() 而不是 resolveMultiOrgEnabled() OS_TENANCY_POSTURE(ADR-0105 D1)是 canonical knob 并且涵盖了那个布尔量;只查旧 flag 的话,OS_TENANCY_POSTURE=isolated 而不设 OS_MULTI_ORG_ENABLED 的部署会从守卫底下溜过去。groupisolated 都需要这个 driver 画不出来的组织墙,所以两者都拒绝。
  • 守卫放在构造函数里,不只放在 connect() ObjectQLEngine.init() 会 catch driver 的 connect 失败、log 一条 error 然后继续启动("may recover via lazy reconnection")。只放 connect() 的话,"拒绝启动"会退化成"启动了,然后在查询时炸"。
  • 没有 override 环境变量。 逃生阀会把这次要消除的静默不隔离原样放回来。要用 Mongo 就保持单租户;要多租户就换 @objectstack/driver-sql

CLI (serve.ts):auto-driver-registration 那段除 UnsupportedDriverError 外吞掉所有错误——那会把这个错误埋掉、然后带着零个 driver 继续启动。现在同样重新抛出(按 code duck-type 匹配,CLI 不引入对 driver 包的依赖),启动 exit 1 并打印可执行的信息。

文档:driver README 顶部警示 + Multi-tenancy 小节、content/docs/data-modeling/drivers.mdx 新增 "Multi-tenancy: not supported"、self-hosting 的 OS_DATABASE_URL 行标注 mongodb:// 仅单租户。

mongodb-schema.ts:把 #3717 留在 FieldDef.unique 上的注释收口——单字段 unique 索引现在是"按构造正确",而不是"因为缺失所以先这样"。

测试

src/mongodb-tenancy-guard.test.ts(16 个用例,不需要 MongoDB 二进制):posture 解析的每个分支(含 legacy alias multi)、declaresTenantScope 的边界、批量报错列出全部违规对象、以及 driver 层的接线——构造函数拒绝、posture 在构造后翻转时 connect() 拒绝(用不可达 URL 证明拒绝发生在任何 I/O 之前)、单租户下不被拦。

  • pnpm --filter @objectstack/driver-mongodb test → 95 passed(含真实 mongodb-memory-server 集成套件,未改动、全绿)
  • pnpm --filter @objectstack/cli test → 642 passed
  • turbo build 两个包均通过;改动文件 eslint 干净

影响面

单租户部署——也就是目前所有能正常工作的 Mongo 部署——不受影响。多租户 + Mongo 的组合从"能启动、不隔离"变成"启动失败并说清原因",这是本 PR 的目的。


Generated by Claude Code

`MongoDBDriver` has NO row-level tenant isolation: it never reads
`DriverOptions.tenantId`, so reads carry no tenant predicate and writes are
never stamped with a tenant column. The layer the SQL driver has
(`resolveTenantField` + `applyTenantScope`) does not exist here, while
everything above the driver — the object metadata `tenancy` block,
`applySystemFields` injecting `organization_id`, the engine threading
`tenantId` into every driver call — runs on the assumption that tenant
isolation is a platform guarantee. Point a multi-tenant deployment's
datasource at Mongo and every query read, updated and deleted OTHER tenants'
documents, silently.

This is option B of #3724: rather than gamble that Mongo multi-tenancy is
really wanted, declare the driver single-tenant and fail fast.

  - `assertSingleTenantPosture()` refuses any tenancy posture other than
    `single` — `OS_TENANCY_POSTURE=group|isolated`, including the posture
    derived from `OS_MULTI_ORG_ENABLED=true` — resolved through the shared
    `resolveTenancyPosture()` (ADR-0105 D1) so the driver can never disagree
    with auth / the registry / the CLI about the mode.
  - It runs in the **constructor**, not only in `connect()`, because
    `ObjectQLEngine.init()` catches a driver's connect rejection and boots
    anyway ("may recover via lazy reconnection"). A connect-only guard would
    have degraded "refuses to start" into "starts, then throws at query
    time". `connect()` re-checks in case a host flips the posture in between.
  - `assertObjectsNotTenantScoped()` refuses objects declaring
    `tenancy.enabled: true` at `syncSchema` / `syncSchemasBatch`, naming
    every offender in one message.

Both throw `MongoDBMultiTenantUnsupportedError` with
`code === 'MONGODB_MULTI_TENANT_UNSUPPORTED'`; the message names the detected
signal, the remedy, and `@objectstack/driver-sql` as the multi-tenant option.

`serve.ts` swallowed every driver-registration error except
`UnsupportedDriverError`, which would have buried this one and booted with no
driver at all — it now re-throws the coded error too (duck-typed by `code`,
so the CLI keeps no dependency on the driver package) and exits 1.

There is deliberately NO override env var: an escape hatch would restore
exactly the silent non-isolation this removes. Single-tenant deployments —
every currently-working Mongo deployment — are untouched; the real-Mongo
integration suite passes unchanged.

Also closes the loop on the `unique` index comment left by #3717: a
single-field unique index is now correct by construction here, not by
omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Hk4hSCirLNWLkgkNkj8YT
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 1:00am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/l labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/driver-mongodb.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-mongodb)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-mongodb)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/driver-mongodb)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-mongodb)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/driver-mongodb)
  • content/docs/releases/v16.mdx (via @objectstack/cli)

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 package catalog's "When to use" line advertised the Mongo driver for any
"existing MongoDB infrastructure" with no mention that it cannot isolate
tenants and now refuses to boot outside a `single` tenancy posture. Flagged by
the docs-drift check on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Hk4hSCirLNWLkgkNkj8YT
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 01:10
@os-zhuang
os-zhuang merged commit d1557d9 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/mongodb-driver-multi-tenant-kikwui branch July 28, 2026 01:10
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…3741) (#3751)

`ObjectQLEngine.init()` wrapped every driver's `connect()` in a try/catch, logged one error line, and carried on. A server whose database was unreachable "started successfully" and then failed every request with an error that reads nothing like *the database is down*. The warning it printed — "Operations may recover via lazy reconnection or fail at query time" — was half fiction: no reconnection exists in driver-sql or driver-mongodb. The caller made it worse: `ObjectQLPlugin.start()` runs `syncRegisteredSchemas()` immediately after `init()`, issuing DDL against a driver that isn't there.

The structural half was worse: the catch removed a driver's ability to refuse startup at all. Any fatal startup check (licence, server version, incompatible configuration, missing capability) is expressed by throwing from `connect()`, and every one was silently downgraded to a runtime error — which is why driver-mongodb's tenancy guard had to be hoisted into its constructor in #3734.

- `init()` now throws `DriverConnectError` (`code: 'ERR_DRIVER_CONNECT'`) when any boot-registered driver's `connect()` rejects, aborting kernel bootstrap. It attempts every driver first, so one failed boot names all of them. The message is self-contained because the CLI prints `error.message` alone.
- `connect()` is now a supported place for a driver to veto boot.
- Removed the misleading "lazy reconnection" claim.
- Escape hatch `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` restores the lenient boot, defaults off, and announces itself with a DEGRADED BOOT banner written to stderr as well as the logger — `os serve` swallows all of stdout during boot and `Logger` routes `warn` there, so logger-only it would be invisible in exactly the deployment the flag is for.
- Documented the connect-time boot contract for operators and driver authors in `data-modeling/drivers.mdx`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

driver-mongodb 完全没有行级租户隔离:读不加谓词、写不打戳,多租户下跨租户可读写

2 participants