Skip to content

fix(auth): organization/create 判权威的 OS_TENANCY_POSTURE,不再判被降级的 OS_MULTI_ORG_ENABLED (#5233) - #5267

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5233-org-gate-posture
Aug 4, 2026
Merged

fix(auth): organization/create 判权威的 OS_TENANCY_POSTURE,不再判被降级的 OS_MULTI_ORG_ENABLED (#5233)#5267
os-zhuang merged 3 commits into
mainfrom
claude/issue-5233-org-gate-posture

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5233

问题

ADR-0105 D1 把 OS_TENANCY_POSTURE立为权威 knob,OS_MULTI_ORG_ENABLED 降级成 resolveTenancyPosture()回落输入。但 AuthManager 里有两个站点还在直接读那个被降级的布尔,于是一台按文档只设权威 knob 的部署(OS_TENANCY_POSTURE=isolated,遗留布尔不设)——组织墙整套挂上、tenancy 服务 isolated、D5 不报警——POST /api/v1/auth/organization/create 依然 403 "Creating additional organizations is disabled on this deployment."

cloud#1012 的维护者决策(方案 B)是自助注册自动开通组织,org-less 用户由 console 导到引导式「Create your workspace」。那条路径就是这个 403,所以平台对「新注册之后怎么办」的唯一答案是死路。与 cloud#1020 同形状,换了一个站点。

改法

两个站点,都在 packages/plugins/plugin-auth/src/auth-manager.ts:

  1. organizationHooks.beforeCreateOrganization —— 改判 postureEnforcesWall(resolveTenancyPosture()),即 serve.ts 的 ADR-0093 D5 boot guard 用的同一个 knob。意图完全不变(单组织仍然拒绝),只是把 knob 从被降级的布尔换成权威的那个;判的仍然是「操作者请求的 posture」,与旧布尔同一个语义层级。判据抽成 multiOrgPostureRequested(),per-request live 读、不缓存。
  2. /auth/configfeatures.multiOrgEnabled —— 仍然优先读 tenancy 服务(它才知道墙是否真的生效),但它没有服务时的 fallback 原本也是 resolveMultiOrgEnabled() ? 'isolated' : 'single',同一份过期契约;现在回落到 resolveTenancyPosture()。同时用 spec 自己的 postureEnforcesWall() 替掉手写的 !== 'single'

packages/types/src/env.tsresolveMultiOrgEnabled() 那段「every site … MUST call this」的注释(写在降级之前,正是它把这两个站点带偏的)订正为反向指引:明确写「拿它回答『是不是多组织』就是 bug」,并指向 postureEnforcesWall(resolveTenancyPosture()) / tenancy 服务。纯注释,代码语义零改动 —— resolveTenancyPosture() 的 fallback 链原样保留(遗留布尔的 back-compat 全靠它)。

packages/spec/** 零改动。

兼容性

不需要任何部署改配置:

配置 改前 改后
OS_TENANCY_POSTURE=isolated(遗留布尔不设) 403 200
OS_TENANCY_POSTURE=group 403 200
OS_MULTI_ORG_ENABLED=true(posture 不设) 200 200(经 posture resolver 的回落)
OS_TENANCY_POSTURE=isolated + OS_MULTI_ORG_ENABLED=true(此前的绕法) 200 200
single / 都不设 403 403

cloud 控制面无影响:它的 worker 本来就显式设 OS_MULTI_ORG_ENABLED=true,两种读法同解。

测试

新增 packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts(19 例),走真实 better-auth 管线 —— 真实 sign-up 拿 cookie,再 POST /organization/createAuthManager.handleRequest,断言真实 HTTP status/body(遵循 #3585 / #4785 的先例:改真东西,别 stub 我们自己的代码)。只断言 hook 函数会重演原来的盲区:现场那个 403 是挂载的路由吐出来的,那就得由路由来回答。

每个场景同时断言 /auth/configfeatures.multiOrgEnabled,末尾一张表把「flag 必须精确预测路由的答案」钉成不变量 —— #5233 之所以难看见,正是因为 flag 说 yes、路由说 no,而没有任何东西比对过这两者。

对未修复源码跑,19 例中 10 例失败(AssertionError: expected 403 to be 200),修复后全绿。

下游证据:cloud PR #1091 在 cloud 侧落了一条 e2e,这个修复要把它从 403 翻成 200 —— 那是 cloud#1012 钉住的真实 objectstack serve + 真实 HTTP 探针,本仓跑不了它;本 PR 的框架侧测试是照那个形状写的。

pnpm --filter @objectstack/plugin-auth test        → 34 files / 744 tests passed
turbo typecheck --filter=@objectstack/plugin-auth --filter=@objectstack/types → OK
pnpm --filter @objectstack/spec check:generated    → 9/9 up to date(合了 origin/main 的 spec 改动后)
@objectstack/dogfood org-create-default-team       → 1 passed(#3624 的回归,见下)
eslint(改动文件)/ check:adr-anchors / check:doc-authoring /
check:org-identifier / check:role-word / check:startup-registry-verdict /
check:durability-log-level                          → 全部 OK

两条刻意没有在本 PR 里做的事(已各自建 issue)

#5261 —— 闸门判「请求的」还是「实际生效的」posture? 降级态(ADR-0093 D5:请求了墙但企业包缺席)下两者分叉:闸门放行、/auth/config 藏按钮。这个分叉不是本 PR 引入的(旧布尔同样是纯 env 读,在这里同样返回 true),改它是一次实打实的能力收缩 —— 没装企业包的 OSS 部署将完全无法建组织,并且 packages/qa/dogfood/test/org-create-default-team.dogfood.test.ts(#3624 的 e2e)靠「boot 后翻 env、闸门 live 读」开路,收紧之后无论如何都过不了、只能改写或删掉。那是维护者的决定,不是 knob 纠正,所以本 PR 把它作为当前行为钉在测试里(用例名带 "pinned as CURRENT behaviour (#5261)"),无论将来怎么定,都必须有意识地改那条断言。已验证 #3624 的 dogfood 在本 PR 下仍然通过

#5262 —— plugin-auth 之外还有 6 处同类误读。 issue 要求的普查做完了:objectql/src/registry.ts(多租户系统字段注入)、plugin-dev(压根不加载 @objectstack/organizations → 直接掉进降级态)、runtime/src/app-plugin.ts 两处(inline seed / 热重载 seeder)、driver-sql(tenant-audit 告警,还把结果缓存了)、cli/verify。本 PR 的文件面被限定在 plugin-auth + types 的一条注释,一处未改,全部归档在 #5262,建议一处一处改并各自带测试。

🤖 Generated with Claude Code

https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t


Generated by Claude Code

claude added 2 commits August 4, 2026 13:19
…moted OS_MULTI_ORG_ENABLED (#5233)

ADR-0105 D1 made `OS_TENANCY_POSTURE` the canonical tenancy knob and demoted
`OS_MULTI_ORG_ENABLED` to a back-compat INPUT of `resolveTenancyPosture()`.
Two sites in AuthManager kept reading the demoted boolean directly, so a
deployment configured the documented way — posture only, legacy boolean unset —
mounted the entire organization wall and still answered
`403 Creating additional organizations is disabled on this deployment.`

- `organizationHooks.beforeCreateOrganization` now judges
  `postureEnforcesWall(resolveTenancyPosture())`, the same knob serve.ts's
  ADR-0093 D5 boot guard keys on. Intent unchanged (single-org still refuses);
  only the knob is corrected, so the gate reads the REQUESTED posture exactly
  as the old boolean did.
- `/auth/config`'s `features.multiOrgEnabled` keeps preferring the `tenancy`
  service, but its no-service fallback now resolves the posture instead of the
  demoted boolean.

`resolveMultiOrgEnabled()`'s doc comment — which still instructed both of those
sites to call it, written before the demotion — now says the opposite.
Its code semantics, and resolveTenancyPosture()'s fallback chain, are untouched.

New `org-create-posture-gate.test.ts` drives the real better-auth pipeline
(sign-up + POST /organization/create through AuthManager.handleRequest) across
posture-only, legacy-only, group, single and degraded deployments, and asserts
`/auth/config` predicts the route's answer in each. Ten of its cases fail
against the pre-fix source.

Fixes #5233
Refs cloud#1012, cloud#1020, #5261, #5262

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

vercel Bot commented Aug 4, 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 Aug 4, 2026 1:35pm

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/types.

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

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/contracts/cache-service.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth, @objectstack/types)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

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.

…ectQL delete dispatch

The #5233 fake engine's delete() accepted call shapes ObjectQLEngine.delete
refuses, so check:engine-double-contract flagged it as an unpinned double.
Route it through assertEngineDeleteDispatch from '@objectstack/objectql' —
the same in-package pattern as auth-manager.jwt-eddsa-fallback.test.ts and
session-of-record.test.ts (#4550) — rather than taking a baseline entry.
The devDependency was already present from #5044.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
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 size/l tests tooling

Projects

None yet

2 participants