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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions