Skip to content

tech-debt/tracking: 让「特权内部写入必须显式声明 isSystem」成为可检查契约(先立约束 → 增量迁移 → 收敛强制) #3166

Description

@os-zhuang

概述

ExecutionContext.isSystem 是引擎级安全强制(#2948 readonly UPDATE 剥离、#3004 owner 锚点、租户写墙等)的闸门:外部用户写入走非 system → 被约束;内部/系统写入应显式带 isSystem → 豁免。但大量核心内部写入方并不显式声明 isSystem,而靠「反正我不经过外部入口」隐式获得信任。

这条隐式信任已经反复咬人:

为什么重要(对齐「让 AI 写代码、少犯错」)

重构后的三步计划(替代「照单迁移 40 处」)

不要把这理解为「机械补 40 处 isSystem」——那无当下收益、且每次改都可能顺带翻转某表的 FLS/owner 行为(把写入方改 system 会绕过这些),为了少犯错反而可能犯错。按下面三步、按安全相关性排序:

步骤 1 —— 立「可检查的契约」(最高杠杆,但需选对工具)

目标:框架内部包里对业务表的 engine.insert/update/delete,要么显式带 isSystem,要么显式标注面向用户,新增违例在 CI / 发布期失败。

⚠️ 实现须知(本 issue 探索得到的关键结论):这不能check-role-word.mjs 那种正则计数 ratchet 来做。「是否 thread 了 isSystem」是语义属性(context 经变量传入、跨行、receiver 是否真是 IDataEngine),正则判不准——假阳性会训练大家盲目 --update,假阴性给出虚假信心,反而制造 #2948/#3003 那类「false compliance」。可行形态二选一:

  • (a) AST / 类型感知的 lint(用 ts-morph / typescript 分析:receiver 类型是 IDataEngine,第三参 options 的 context 是否含 isSystem),配 baseline ratchet 冻结现有 ~40 处、只挡新增。工作量真实,但一次到位。
  • (b) 类型/API 层强制:让引擎写方法要求一个显式的 actor 参数({ actor: 'system' | ExecutionContext }),使「不声明身份就调不通」由编译器保证,无需 lint。最干净,但触及所有调用点,改动更大。

步骤 2 —— 增量迁移(在契约之后)

碰 readonly / owner / 审批类列的写入方优先迁移(better-auth adapter 已在 #3164 完成),每个配测试。碰不到安全相关列的写入方不必为统一而改。

步骤 3 —— 收敛强制

覆盖够高后,把 #3043 的 INSERT 剥离从入口下沉回引擎、删掉入口特例,两处合一,并堵上「插件 hook 里非 system 写入」这类入口拦不到的引擎内路径。

当前判定

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions