Skip to content

TenancyConfigSchema.tenantField 的默认值是 'tenant_id',但平台的租户列叫 organization_id —— 两个 spec 文件对「默认值」的说法互相矛盾 #5315

Description

@os-zhuang

发现于 #5081(ADR-0120 17.x 收尾)的 D6 全仓扫荡,与该单契约无关,按 Prime Directive #10 单独记录,未在 PR #5314 中修改

事实

packages/spec/src/data/object.zod.ts:

export const TenancyConfigSchema = lazySchema(() => z.object({
  enabled: z.boolean().describe('Enable multi-tenancy for this object'),
  tenantField: z.string().default('tenant_id').describe('Field name for tenant identifier'),
}, { error: strictTenancyError }).strict());

它上面的 @example 也写着 tenantField: 'tenant_id'

但平台的租户列是 organization_id(v5.0 起 projectenvironmentorganization_id 是 kernel 注入的那一列),而且另外两处对同一个概念给出的默认值是相反的:

  1. packages/spec/src/security/rls.zod.ts:595,注释原文:

    The default tenantField is organization_id to match better-auth's …

    函数签名也是 tenantPolicy: (object: string, tenantField: string = 'organization_id')

  2. packages/plugins/driver-sql/src/sql-driver.tscomputeTenantField:声明的 tenantField 只有在该对象确实拥有这个字段时才采用,否则隐式回落到 organization_id:

    if (tenancyDecl?.tenantField) {
      const declared = String(tenancyDecl.tenantField);
      if (fields && Object.prototype.hasOwnProperty.call(fields, declared)) return declared;
    }
    if (fields && Object.prototype.hasOwnProperty.call(fields, 'organization_id')) return 'organization_id';
    return null;

为什么今天不炸

正因为 driver 有那层回落:作者写 tenancy: { enabled: true } 而不写 tenantField 时,Zod 把默认值填成 'tenant_id',对象通常没有 tenant_id 字段,于是 computeTenantField 跳过它、回落到 organization_id —— 结果是对的。所以没有用户可见缺陷,这条按 observation-class 记录(finding 标签,不挂 pm:queue),严重度请 triage 轮次自行判定。

为什么仍值得记

  • 这个默认值实际上没有消费方(它唯一的作用是让 computeTenantField 的第一个分支去找一个不存在的列然后跳过),属于 ADR-0078「declared-but-inert」那一类的候选。
  • 它是 AI 作者会照抄的面:schema 的 @example 明确示范了 tenantField: 'tenant_id'。一个照抄它、并且真的建了 tenant_id 字段的对象,就会得到一张按 tenant_id 而不是 organization_id 组合的唯一索引 —— 与平台其余部分(RLS 谓词、写入打戳、autonumber 序列)错位。
  • 它正是 ADR-0120 引用 多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696 时点名的那个模式:两个子系统给同一个概念起了不同的名字。ADR-0120 §Terminology 为此把可授权词汇定死为 'organization'('tenant' / 'org' 一律拒绝);这里是同一个词表问题在 tenancy 这一侧的残留。

可能的方向(不预设结论)

  1. 把默认值改成 'organization_id',与 rls.zod.ts 的注释和 driver 的回落对齐,同时更新 @example。属于 spec 默认值变更,要判是否 breaking。
  2. 去掉 .default(...),让「未声明」如实表达为 undefined,由 driver 的既有回落作为唯一真相;这更贴近 ADR-0078(不留没人读的声明)。
  3. 判定为纯文档问题,只改 @example 与 describe。

顺带一提:skills/objectstack-data/** 里长期用 tenant_id 举例的地方,已在 PR #5314 的 D6 扫荡里改成 organization_id,但没有动 spec 的这个默认值。


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions