feat(driver-mongodb)!: 显式单租户 —— 检测到多租户模式即拒绝启动 (#3724) - #3734
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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
marked this pull request as ready for review
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`.
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.
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)single(OS_TENANCY_POSTURE=group|isolated,含由OS_MULTI_ORG_ENABLED=true推导出的)connect()再查一次(开 socket 之前)MongoDBMultiTenantUnsupportedErrortenancy.enabled: truesyncSchema()/syncSchemasBatch()几个刻意的决定:
resolveTenancyPosture()而不是resolveMultiOrgEnabled()。OS_TENANCY_POSTURE(ADR-0105 D1)是 canonical knob 并且涵盖了那个布尔量;只查旧 flag 的话,OS_TENANCY_POSTURE=isolated而不设OS_MULTI_ORG_ENABLED的部署会从守卫底下溜过去。group和isolated都需要这个 driver 画不出来的组织墙,所以两者都拒绝。connect()。ObjectQLEngine.init()会 catch driver 的 connect 失败、log 一条 error 然后继续启动("may recover via lazy reconnection")。只放connect()的话,"拒绝启动"会退化成"启动了,然后在查询时炸"。@objectstack/driver-sql。CLI (
serve.ts):auto-driver-registration 那段除UnsupportedDriverError外吞掉所有错误——那会把这个错误埋掉、然后带着零个 driver 继续启动。现在同样重新抛出(按codeduck-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 aliasmulti)、declaresTenantScope的边界、批量报错列出全部违规对象、以及 driver 层的接线——构造函数拒绝、posture 在构造后翻转时connect()拒绝(用不可达 URL 证明拒绝发生在任何 I/O 之前)、单租户下不被拦。pnpm --filter @objectstack/driver-mongodb test→ 95 passed(含真实 mongodb-memory-server 集成套件,未改动、全绿)pnpm --filter @objectstack/cli test→ 642 passedturbo build两个包均通过;改动文件 eslint 干净影响面
单租户部署——也就是目前所有能正常工作的 Mongo 部署——不受影响。多租户 + Mongo 的组合从"能启动、不隔离"变成"启动失败并说清原因",这是本 PR 的目的。
Generated by Claude Code