Skip to content

organization/create 的闸门判「请求的 posture」还是「实际生效的 posture」?降级部署(D5)下两者分叉,闸门放行而 /auth/config 隐藏 #5261

Description

@os-zhuang

从 #5233 的修复中分出来的一个需要维护者定夺的语义问题,#5233 本身没有改动它(那是纠正 knob,不是改能力边界)。

事实

修好 #5233 之后,packages/plugins/plugin-auth/src/auth-manager.ts 的两个站点读的是两个不同的事实,在一种部署形状下会分叉:

站点 读什么 语义
organizationHooks.beforeCreateOrganization postureEnforcesWall(resolveTenancyPosture()) 操作者请求的 posture
/auth/config 的 features.multiOrgEnabled tenancy?.posture ?? resolveTenancyPosture() 实际生效的 posture(ADR-0093 D4)

只有一种形状让两者不同 —— ADR-0093 D5 的降级态:请求了 isolated/group,但企业包 @objectstack/organizations 不在(org-scoping 服务缺席),于是 tenancy.posture 解析为 single 且 degraded=true。此时:

  • 闸门:放行(请求的是 isolated);
  • /auth/config:multiOrgEnabled=false,console 把「创建组织」入口藏起来;
  • 结果:UI 不给按钮,但 API 打得通,并且创建出来的组织没有任何引擎强制的墙。

这不是 #5233 引入的

降级前闸门读 resolveMultiOrgEnabled(),那同样是一个纯 env 读,在这里同样返回 true、同样放行。#5233 只是把 knob 从被降级的布尔换成权威的 posture,分叉是原样保留的。已在 packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts 里作为当前行为钉住(用例名带 "pinned as CURRENT behaviour"),所以无论怎么定夺,都必须是有意识地改这条断言。

两个选项

A. 保持现状 —— 闸门判「请求的 posture」。

  • 与 serve.ts 的 D5 boot guard 同源(它也是 resolveTenancyPosture() !== 'single'),两处口径一致。
  • 降级本来就已经是显式选择:serve 在降级时默认拒绝启动,要 OS_ALLOW_DEGRADED_TENANCY=1 才走;既然操作者已经明说「我接受没有墙」,那么再单独禁掉建组织意义不大。
  • 代价:/auth/config 藏按钮而 API 放行,这个分叉长期存在;而且降级部署里建出来的组织边界是「声明了但没强制」——ADR-0049 最讨厌的那一类。

B. 闸门改判「实际生效的 posture」(tenancy?.posture ?? resolveTenancyPosture())。

倾向

倾向 A(即现状),因为降级已经是 OS_ALLOW_DEGRADED_TENANCY 的显式选择,B 的收益主要落在一个已经被 boot guard 拦住的形状上,却要付「OSS 部署失去建组织能力 + 丢掉 #3624 的 e2e 覆盖」这两笔。若要选 B,建议同时给 dogfood 一条别的开路方式(例如 verify harness 提供可注入的 tenancy stub),别让 #3624 的覆盖为此消失。

不指派 —— 按 Prime Directive #10 归档,等维护者定夺。

出处:#5233 的开发(会话 session_015W6nhsDrz6zWQc8je12a1t)。

Activity

  1. claude commented on Aug 4, 2026

    @claude
    Contributor

    ⛔ 认领:session_015W6nhsDrz6zWQc8je12a1t(objectstack:identity 车道 PM),分支 claude/issue-5261-effective-posture-gate,os-dev 单点派发。

    维护者 2026-08-04 裁定:选 B(闸门改判「实际生效的 posture」),并入 v17。 这是一次能力收缩、破坏性变更,只能搭主版本车——错过 v17 就要等 v18,所以现在做。

    裁定同时接受本单「倾向 A」里点名的两笔代价,并按 issue 自己的建议处置第二笔:

    1. 没装企业包的 OSS 自托管部署将无法创建组织 —— 有意为之(降级态不再产出声明了但没强制的组织边界,ADR-0049 最讨厌的那一类)。
    2. packages/qa/dogfood/test/org-create-default-team.dogfood.test.ts(org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 回归)靠「boot 后翻 env、闸门 live 读」开路,选 B 后任何 env 组合都过不了 —— 不允许直接删掉。按本单建议给 dogfood 另一条开路方式(如 verify harness 提供可注入的 tenancy stub),保住 org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 的端到端覆盖。

    ⚠️ packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts 里带 pinned as CURRENT behaviour (#5261) 的用例是故意钉住旧行为的,本次要有意识地翻转它并改掉用例名/注释,不要留下自相矛盾的钉子。

    文件面:packages/plugins/plugin-auth/src/auth-manager.ts + org-create-posture-gate.test.ts + dogfood 开路所需改动 + changeset。⛔ 不碰 reconcile-membership.ts(#5205 排在本条之后)、packages/objectql/plugin-dev/runtime/driver-sql/cli(#5262 在改)、content/docs/releases/。


    Generated by Claude Code

  2. claude commented on Aug 4, 2026

    @claude
    Contributor

    PM 验收(第 19 轮,identity 车道):ACCEPT — PR #5294,CI 绿后合入

    维护者裁定选 B、搭 v17 主版本车,实现到位。逐条复核:

    dev 找到一条我没料到的独立佐证,我已核实为真。 packages/spec/src/kernel/public-auth-features.ts:153-155 原文:

    'Reflects ACTUAL multi-tenancy capability (the tenancy posture enforces ' … 'not just the requested posture.'

    也就是说 spec 早就声明 multiOrgEnabled 反映的是实际能力而非请求的姿态,并由它把守 sys_organization.actions.create_organization。出线的一直是路由,不是 flag —— 这次改动不只是维护者的一个取舍,它让路由回到了自己的声明契约上。这条把方案 B 从「二选一」提升成「修正违约」,是本单最有价值的发现。

    两站点从「两处写法碰巧一致」变成「一次求值」:判据抽成 effectiveTenancyPosture(),getPublicConfig() 调同一个方法。仍是 per-request live 读不缓存,理由正确 —— org-scoping 在 plugin-auth 之后注册,墙确实是 boot 中途立起来的,冻结任一层都会把部署钉死在立墙前的状态(AGENTS.md startup-registry 规则)。

    代价二没有靠删测试解决,这是我在派单里划的红线,dev 守住了:@objectstack/verify 新增 multiTenant: 'posture-only',注册内置 org-scoping 替身,让 tenancy 服务解析出真实、非降级的 isolated。它比原来的 env-flip 更诚实 —— 原来是单租户栈只对一个闸门撒谎,现在模拟的是真实的有墙部署。dogfood 另加一条「守卫的守卫」,直接断言 posture==='isolated'、degraded===false、multiOrgEnabled===true,替身失效时报的是「前置条件塌了」而不是把人引去查一个不存在的 team-insert bug。替身的边界也写死了:不做任何隔离,并点名「用它断言跨租户隔离 = #4700 那个 constant-false 能力探针反过来戴一次面具」;跨租户 gate 在本仓继续 skip 而不是假装通过。harness.posture-only.test.ts 刻意不 mock @objectstack/organizations,因为待证命题正是「它不需要企业包」—— 旁边摆个 mock 会让命题不可证伪。这个反向推理是对的。

    那颗自相矛盾的钉子清干净了:断言/用例名/注释/文件头一起翻,全仓 pinned as CURRENT behaviour 只剩叙述历史的散文。不变量表取消了「降级除外」的例外 —— 例外的消失本身就是这次改动的证明,新增三行降级用例。

    对抗性验证真实执行:回退闸门判据 → 7 failed | 18 passed,expected 200 to be 403,红的正是两种降级、双 env knob、late-probe 与三行不变量表;把 dogfood 换回旧 env-flip → expected 'single' to be 'isolated' + 403,证明代价二真实存在且新开路方式是承重的,不是摆设。

    我替 dev 扫了它点名的那条合入前风险(cloud 侧是否有依赖 D5 降级态还能建组织的 e2e):没有,cloud 不受影响。apps/ee-group-showcase/test/org-create-route.dogfood.test.ts 文件头那句「open workspace has no @objectstack/organizations, so it opens the route by flipping OS_MULTI_ORG_ENABLED」描述的是 framework 自己那条单租户测试(正是本 PR 换掉的那个绕法);cloud 这条测试自身 import { OrganizationsPlugin } 并以真实企业运行时在 group 姿态启动 —— 非降级态,判决不变。cloud 仓内就有 packages/organizations,EE 各栈同理。

    关于 BootOptions 是已发布包公共面这一点(dev 提的第一条 note):我认可现在的做法。它是测试基础设施的公共面,plugin-auth 生产路径零新增 API,替身走的是企业包本来就在用的 org-scoping 注册接缝;替代方案(每个 fixture 各造一份替身)会失去那一处集中写死「它不隔离任何东西」的文档——而那句文档正是防止它被误用的东西。

    证据边界如实:本仓装不了 cloud-private 的 @objectstack/organizations,所以「有企业包时墙真的立着并隔离数据」不在本 PR 证据范围内;本 PR 只证明 posture 生效则放行、不生效则拒绝。

    changeset 为 plugin-auth major + verify minor,与破坏性收缩相符。CI 绿即合入。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions