fix(auth,verify)!: organization/create 判实际生效的 posture,降级部署不再产出无墙的组织 (#5261) - #5294
Merged
Merged
Conversation
…5261) `beforeCreateOrganization` 此前判操作者**请求的** posture (`postureEnforcesWall(resolveTenancyPosture())`),与 `/auth/config` 的 `features.multiOrgEnabled`(判**生效** posture)在 ADR-0093 D5 降级态下分叉: console 藏起「创建组织」按钮,API 却照建,且建出来的每个组织都是没有任何引擎 强制的租户边界 —— 声明了但没强制,ADR-0049 最讨厌的那一类。 闸门改判 `tenancy?.posture ?? resolveTenancyPosture()`,与 `/auth/config` 共用同一次求值(`effectiveTenancyPosture()`),两站点从此同解、永不分叉。 `packages/spec` 的 `PUBLIC_AUTH_FEATURES` 本就声明 `multiOrgEnabled` 把守 `sys_organization.actions.create_organization` 且「reflects ACTUAL … not just the requested posture」——出线的一直是路由。 BREAKING:没有安装企业包 `@objectstack/organizations` 的部署将完全无法创建组织, 任何 env 组合都不行。有意为之,搭 v17 主版本车;`serve.ts` 本就在降级态默认拒绝 启动(要 `OS_ALLOW_DEGRADED_TENANCY=1`)。 #3624 的 dogfood 此前靠「boot 后翻 env、闸门 live 读」开路,本次收缩把这个绕法 关死。**未删测试**:`@objectstack/verify` 的 `BootOptions.multiTenant` 新增 `'posture-only'`,注册内置的 `org-scoping` 服务替身,让 tenancy 服务解析出真实、 非降级的 `isolated` posture —— 模拟真实的有墙部署,而不是让闸门放行。该选项 明确声明不做任何租户隔离,跨租户证明仍只认 `multiTenant: true` + 真实企业包。 对抗性验证:对未修复源码跑 org-create-posture-gate.test.ts,25 例中 7 例红 (`expected 200 to be 403`);把 dogfood 换回旧的 env-flip 绕法,得到 403 "Creating additional organizations is disabled on this deployment." Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 16:05
os-zhuang
enabled auto-merge
August 4, 2026 16:05
This was referenced Aug 4, 2026
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 #5261
维护者 2026-08-04 裁定:选 B(闸门改判「实际生效的 posture」),破坏性能力收缩,搭 v17 主版本车。
问题
#5233 修好之后,
auth-manager.ts的两个站点读的是两个不同的事实:organizationHooks.beforeCreateOrganizationpostureEnforcesWall(resolveTenancyPosture())—— 操作者请求的 posture/auth/config的features.multiOrgEnabledtenancy?.posture ?? resolveTenancyPosture()—— 实际生效的 posture只有一种形状让两者分叉,而那恰恰是最不该放行的一种 —— ADR-0093 D5 降级态:请求了
isolated/group,但企业包@objectstack/organizations缺席,tenancy.posture解析为single且degraded=true。此时闸门放行,/auth/config却multiOrgEnabled=false把入口藏起来:UI 不给按钮而 API 打得通,且建出来的每个组织都是没有任何引擎强制的租户边界 —— 声明了但没强制,ADR-0049 最讨厌的那一类,发生在部署层。一条独立佐证:
packages/spec的PUBLIC_AUTH_FEATURES早就声明multiOrgEnabled把守sys_organization.actions.create_organization,并写明它 "Reflects ACTUAL multi-tenancy capability … not just the requested posture"。出线的一直是路由,不是 flag —— 本 PR 让路由回到它自己的声明契约上。改法(方案 B)
packages/plugins/plugin-auth/src/auth-manager.ts,判据抽成一处、两个站点共用同一次求值:tenancy服务权威,是因为只有它知道墙是否真的立着(它 probe 企业org-scoping运行时,把立不住的请求降解为single+degraded);只有从未注册该服务的精简嵌入才回落到 env 解析。getPublicConfig()现在调用的是同一个方法 —— 两站点不再是「两处写法碰巧一致」,而是一次求值。仍是 per-request live 读、不缓存:
org-scoping在 plugin-auth 之后注册,墙确实是 boot 中途立起来的,冻结任何一层都会把部署钉死在立墙前的状态(AGENTS.md 的 startup-registry 规则)。两笔代价的处置
代价一(有意为之):没装企业包的部署将完全无法创建组织,任何 env 组合都不行。
isolated/group(墙真立着)single/ 两个 knob 都不设tenancy服务的精简嵌入serve.ts本就在降级态默认拒绝启动(要OS_ALLOW_DEGRADED_TENANCY=1),所以这条收缩命中的是一个已经需要显式选择才能到达的形状:从此那里的 org-create 路由也一并拒绝,而不是半通不通。cloud 控制面与任何装了企业包的部署不受影响。代价二(未删测试):
packages/qa/dogfood/test/org-create-default-team.dogfood.test.ts(#3624 回归)靠「boot 后翻 env、闸门 live 读」开路,本次收缩把这个绕法关死了。按 issue 自己的建议给它另一条开路方式:@objectstack/verify的BootOptions.multiTenant新增取值'posture-only'—— 注册一个内置的org-scoping服务替身(SimulatedOrgScopingPlugin),挂在企业插件自己的槽位上,于是tenancy服务解析出真实、非降级的isolatedposture,整个栈口径一致。这比原来的 env flip 更诚实:原来是单租户栈只对一个闸门撒谎(BYPASS),现在模拟的是真实的有墙部署(DEPLOYMENT)。dogfood 因此新增一条「守卫的守卫」用例,直接断言
tenancy.posture === 'isolated'、degraded === false且/auth/config的multiOrgEnabled === true—— 若替身哪天失效,报出来的会是「前置条件塌了」,而不是把人引去查一个并不存在的 team-insert bug。⛔ 该选项明确声明不做任何租户隔离(不 stamp
organization_id、不 scope 查询):它让部署的 posture 为真,不是让墙为真。文档里写死了这条边界,并点名「用它断言跨租户隔离 = #4700 那个 constant-false 能力探针反过来戴一次面具」。跨租户隔离的唯一诚实证明仍是multiTenant: true+ 真实企业包,那些 gate 在本仓继续 skip 而不是假装通过。新增
packages/verify/src/harness.posture-only.test.ts覆盖这个新公共 API,刻意不 mock@objectstack/organizations(与harness.posture.test.ts相反)—— 待证命题正是「它根本不需要企业包」,旁边摆一个 mock 会让该命题不可证伪。那颗自相矛盾的钉子
org-create-posture-gate.test.ts里带pinned as CURRENT behaviour (#5261)的用例是 #5267 故意钉住旧行为的,本次就是那个「将来」:断言、用例名、注释、文件头一起翻,没有留下说着「这是当前行为」却断言着新行为的钉子(全仓 greppinned as CURRENT behaviour只剩本 PR 里那句叙述历史的散文)。不变量表(#5267 建立的「flag 必须精确预测路由的答案」)现在覆盖每一种形状、不再挖例外:此前它显式排除降级行,因为那里两站点按设计答不同的事实;例外的消失本身就是这次改动的证明。新增三行降级用例(
isolated降级、group降级、两个 env knob 一起喊 yes 的降级)。测试沿用 #5267 的形状:真实 better-auth 管线(真实 sign-up 拿 cookie →
POST /organization/create经AuthManager.handleRequest),断言真实 HTTP status/body;每个场景同时断言/auth/config。另加一条「墙在 plugin-auth 之后才立起来」的 live-probe 用例。对抗性验证(真实执行)
1. 对未修复源码跑(只把闸门判据回退成
postureEnforcesWall(resolveTenancyPosture())):修复恢复后:
Test Files 1 passed (1) / Tests 25 passed (25)。2. 把 dogfood 换回旧的 env-flip 绕法(证明新开路方式是承重的、代价二真实存在):
测试
changeset:
@objectstack/plugin-authmajor(能力收缩讲清楚 + 迁移指引)、@objectstack/verifyminor。⛔ 未触碰content/docs/releases/。证据边界
objectstack serve+ 真实企业许可(本仓装不了 cloud-private 的@objectstack/organizations)。'posture-only'替身只让 posture 为真,不隔离任何数据;本 PR 没有任何一条断言依赖它去证明隔离。reconcile-membership.ts(finding(auth): reconcileMembership 与 backfillMemberships 对「词表外的 policy」判断相反 —— 一个 fail-open,一个 fail-safe #5205)、objectql/plugin-dev/runtime/driver-sql/cli(普查:plugin-auth 之外还有 6 处把被降级的 OS_MULTI_ORG_ENABLED 当「是不是多组织」在读 —— 只设 OS_TENANCY_POSTURE 的部署会静默走错分支 #5262)、packages/types/src/env.ts、content/docs/releases/。🤖 Generated with Claude Code
https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t
Generated by Claude Code