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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions