Part of #5852(contract-first 拆分第一棒,由分诊座位拆)。本单只做 spec 契约面;producer 填充与断言在 #5852 的 identity 半边,cloud consumer 同步在 objectstack-ai/cloud 的对应单。
事实(已核 origin/main)
packages/spec/src/contracts/sharing-service.ts:396:
export interface HierarchyScopeContext {
userId: string;
organizationId?: string | null;
tenantId?: string | null;
}
两个字段都是 optional、都可为 null,doc 没有任何一句说哪一个承载「调用方的活动组织」。于是:
- producer(
packages/plugins/plugin-sharing/src/sharing-service.ts:875-880)照抄 ExecutionContext,实测填出 { organizationId: null, tenantId: "<活动组织>" } —— 框架的 ExecutionContextLike { userId, tenantId, timezone } 本来就以 tenantId 承载活动组织,plugin-security 的 RLS 也是从 ExecutionContext.tenantId 取 current_user.organization_id;
- consumer(cloud
packages/security-enterprise/src/hierarchy/resolver.ts)只读 context.organizationId,拿到 null 就把自己文档里写明的 "belt-and-braces 租户隔离" 整条跳过。
两边各自符合这份契约,合起来是一个实测可达的跨组织越权(#5852 有 201/403 对照实测)。含糊的是契约本身,不是任何一侧的实现。
完成范围(本单)
- 在
HierarchyScopeContext 上明确单一权威字段并写进 doc 注释 —— 依据是运行时事实:框架里真实承载活动组织的是 tenantId;
- 另一个字段保留为 deprecated 别名并在 doc 注释里写明「resolver 不得单独依赖它」。⛔ 不在本单删除
organizationId —— 它在 api-surface.json:3597 的公开面上,删除走 ADR-0049 enforce-or-remove / ADR-0087 退役单独立单裁决,不做 rider;
IHierarchyScopeResolver.resolveOwnerIds 的 doc 补一句实现方义务:权威字段为空时必须 fail-closed,⛔ 不得静默按「无租户约束」构建 owner set —— 这正是本次越权的形状;
- 按仓内惯例更新生成物 / api-surface 基线。
为什么修在契约而不是在 consumer 加 ?? tenantId
#5852 已论证:consumer 兜底是宽容消费者模式 —— 契约仍然含糊,下一个 resolver 实现还会踩同一个坑,而这一类失效是静默的(隔离不生效不报错,只是多返回了别的组织的 owner id)。
解锁关系
Part of #5852(contract-first 拆分第一棒,由分诊座位拆)。本单只做 spec 契约面;producer 填充与断言在 #5852 的 identity 半边,cloud consumer 同步在 objectstack-ai/cloud 的对应单。
事实(已核
origin/main)packages/spec/src/contracts/sharing-service.ts:396:两个字段都是 optional、都可为 null,doc 没有任何一句说哪一个承载「调用方的活动组织」。于是:
packages/plugins/plugin-sharing/src/sharing-service.ts:875-880)照抄 ExecutionContext,实测填出{ organizationId: null, tenantId: "<活动组织>" }—— 框架的ExecutionContextLike { userId, tenantId, timezone }本来就以tenantId承载活动组织,plugin-security 的 RLS 也是从ExecutionContext.tenantId取current_user.organization_id;packages/security-enterprise/src/hierarchy/resolver.ts)只读context.organizationId,拿到 null 就把自己文档里写明的 "belt-and-braces 租户隔离" 整条跳过。两边各自符合这份契约,合起来是一个实测可达的跨组织越权(#5852 有 201/403 对照实测)。含糊的是契约本身,不是任何一侧的实现。
完成范围(本单)
HierarchyScopeContext上明确单一权威字段并写进 doc 注释 —— 依据是运行时事实:框架里真实承载活动组织的是tenantId;organizationId—— 它在api-surface.json:3597的公开面上,删除走 ADR-0049 enforce-or-remove / ADR-0087 退役单独立单裁决,不做 rider;IHierarchyScopeResolver.resolveOwnerIds的 doc 补一句实现方义务:权威字段为空时必须 fail-closed,⛔ 不得静默按「无租户约束」构建 owner set —— 这正是本次越权的形状;为什么修在契约而不是在 consumer 加
?? tenantId#5852 已论证:consumer 兜底是宽容消费者模式 —— 契约仍然含糊,下一个 resolver 实现还会踩同一个坑,而这一类失效是静默的(隔离不生效不报错,只是多返回了别的组织的 owner id)。
解锁关系
organizationId: nullwhile the active org rides intenantId#5852 的 identity 半边(producer 按权威字段填充 + 断言)@objectstack/security-enterpriseresolver 同步单