Skip to content

[#5852 契约半边] HierarchyScopeContext 未声明 organizationId / tenantId 哪个权威 —— producer 只填一个、consumer 只读另一个,两边都「符合契约」 #5858

Description

@os-zhuang

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.tenantIdcurrent_user.organization_id;
  • consumer(cloud packages/security-enterprise/src/hierarchy/resolver.ts)只读 context.organizationId,拿到 null 就把自己文档里写明的 "belt-and-braces 租户隔离" 整条跳过。

两边各自符合这份契约,合起来是一个实测可达的跨组织越权(#5852 有 201/403 对照实测)。含糊的是契约本身,不是任何一侧的实现。

完成范围(本单)

  1. HierarchyScopeContext明确单一权威字段并写进 doc 注释 —— 依据是运行时事实:框架里真实承载活动组织的是 tenantId;
  2. 另一个字段保留为 deprecated 别名并在 doc 注释里写明「resolver 不得单独依赖它」。⛔ 不在本单删除 organizationId —— 它在 api-surface.json:3597 的公开面上,删除走 ADR-0049 enforce-or-remove / ADR-0087 退役单独立单裁决,不做 rider;
  3. IHierarchyScopeResolver.resolveOwnerIds 的 doc 补一句实现方义务:权威字段为空时必须 fail-closed,⛔ 不得静默按「无租户约束」构建 owner set —— 这正是本次越权的形状;
  4. 按仓内惯例更新生成物 / api-surface 基线。

为什么修在契约而不是在 consumer 加 ?? tenantId

#5852 已论证:consumer 兜底是宽容消费者模式 —— 契约仍然含糊,下一个 resolver 实现还会踩同一个坑,而这一类失效是静默的(隔离不生效不报错,只是多返回了别的组织的 owner id)。

解锁关系

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions