Skip to content

记录删除后 source: 'manual' 的 sys_record_share 仍是孤儿 —— #4779 的 afterDelete 只覆盖规则共享,且只覆盖有规则的对象 #5103

Description

@os-zhuang

在 #4779(PR #5102)的实现中发现。未认领,归属 plugin-sharing,不在 #5102 的范围内。

现象

#4779 的 issue 末尾记了「rule-hooks 没绑 afterDelete,记录删除后 sys_record_share 行会成为孤儿」。PR #5102 补上了 afterDelete,但它的覆盖面被两层条件夹住,剩下的两块仍然是孤儿:

  1. 只撤 source: 'rule' 的行。 这是刻意的 —— 手工共享是人对某一条记录做的决定,任何规则重算都不会重建它,所以规则子系统去扫它属于越界删数据。但结果是:记录被删后,它的 source: 'manual' 共享行原样留在表里。
  2. 只在「至少有一条生效共享规则」的对象上绑。 bindRuleHooks 遍历的是 rules 里出现过的 object_name。一个 sharingModel: 'private'、只用手工共享、从没配过规则的对象,一个钩子都没有,删除后连规则共享的清理都不会发生(虽然它本来也没有规则共享)。

即:手工共享 + 记录删除 = 永久孤儿行,对任何对象都成立。

危害与前提

和 #4779 记的那一条同源:今天危害有限,因为记录已经不存在,buildReadFilter 拼出来的 record_id IN (...) 匹配不到任何真实行。但这个「有限」完全依赖一个假设:记录 id 永不复用。 那个假设目前没有任何门禁保护 —— 一旦引入 id 复用、app 自定义主键、或者导入时保留原 id,这些孤儿行会立刻变成真实越权:新记录一落到旧 id 上,旧记录的共享对象就直接拿到了新记录的读/写权限。

次要的:sys_record_share 单调增长,而 Setup 的 Record Shares 列表会展示指向不存在记录的行。

建议方向(需要先定一件事)

清理孤儿共享该由谁负责,有两个自然位置,选哪个决定了它是「共享插件的事」还是「平台的事」:

  • A. plugin-sharing 在所有启用了 sharing 的对象上绑一个 afterDelete,撤销该记录的全部 sys_record_share 行(不分 source)。语义最干净 —— 记录没了,任何共享都不可能有效。代价是这个插件要在远比现在多的对象上挂写钩子,而「哪些对象启用了 sharing」是运行期由 sharingModel 决定的,和现在按规则表绑定的模型不一样。
  • B. 走平台的 deleteBehavior / 级联机制,把 sys_record_share 声明成指向任意对象的「弱引用」并在删除时清理。更通用(sys_attachment、sys_comment、sys_approval_request 等一族按 (object_name, record_id) 挂靠的系统表是同一形状),但需要一个当前不存在的多态级联能力。

另外无论选哪条,都值得补一个 boot 期的孤儿清扫(sweepOrphanedRuleGrants 已经是这个形状,只是它按「规则行还在不在」判断,而这里要按「记录还在不在」判断),这样历史数据也能收敛。

相关

Activity

  1. claude commented on Aug 4, 2026

    @claude
    Contributor

    裁定(维护者授权车道内继续,2026-08-04):方案 A —— plugin-sharing 在所有启用 sharing 的对象上绑 afterDelete,撤销该记录全部 sys_record_share 行(不分 source);同批补 boot 期按「记录还在不在」的孤儿清扫。

    两轴:A 立即恢复不变量(记录没了,任何共享都不可能有效),落点全在本车道(plugin-sharing),且不排斥 B —— 平台级多态弱引用级联(sys_attachment/sys_comment/sys_approval_request 同族)是更普适的机制,值得立项,但它尚不存在,不该让授权面的孤儿行等一个未立项的平台能力。B 已另立设计卡交 engine 车道(见下条评论链接);B 落地之日,A 的钩子可收敛进去。

    绑定模型说明:「启用 sharing 的对象」由 sharingModel 运行期决定,与现在按规则表绑定不同 —— dev 需按对象元数据枚举绑定,并处理元数据热更新(对象后来开 sharing)的情形;做不到热更新则至少 boot 时全量绑定 + 文档写明边界。

    摘 needs-user-decision,本轮派发。


    Generated by Claude Code

  2. claude commented on Aug 4, 2026

    @claude
    Contributor

    认领:PM 循环第 12 轮
    会话:session_015W6nhsDrz6zWQc8je12a1t
    分支:claude/issue-5103-sharing-orphan-cascade
    Worktree:objectstack-issue-5103
    文件面:packages/plugins/plugin-sharing/**(afterDelete 全源撤销 + boot 孤儿清扫)+ 测试 + changeset;域 domain:identity


    Generated by Claude Code

  3. claude commented on Aug 4, 2026

    @claude
    Contributor

    PM 验收(第 12 轮,identity 车道):ACCEPT — PR #5196,CI 绿后转 ready 入队

    • 对「热更新」要求给出了结构性更强的答案:不做 boot 枚举 + 订阅,而是一个全局 beforeDelete/afterDelete 对,在处理器内按当次 live sharingModel 判定 —— 没有绑定表就没有陈旧问题,运行期开 sharing 的对象下一次删除即被覆盖。第三类回归(退回按规则表 boot 绑定)被 h.options.object === undefined 断言 + 热更新测试结构性杀死
    • 不对称处理正确:有界删同步集合撤销(全 source,200 分块);无界删不走规则路径的撤销-重发(手工共享无人能重建),转对象级孤儿清扫 —— 语义差异被理解而不是被模板套用
    • 清扫的工程质量:keyset 分页、每对象每页一次批量存在性探针、扫描上限自报截断、探针抛错的对象整体放过(fail-safe 方向正确)
    • 测试证据 ✓:32 例新增、305 全包过、verify harness 真核启动跑过新绑定与清扫;两组 mutation(撤销谓词加回 source:'rule' → 7 红;清扫走回盲区 → 11 红)+ durability/startup 两个门禁绿
    • 共享规则 hook 对谓词式(multi)写入不重算:if (!id) return 让 sys_record_share 授权在批量更新后变陈旧 #4779 的 stash 挪到 resolver 旁消重,顺手但正确 ✓;spec/objectql 零触碰 ✓

    衍生 #5190 已核并入队(identity 车道):sys_share_link 同族且更糟 —— resolve 查 token/revoked/expires 却不查记录还在不在,而 share link 是无身份的能力令牌,id 复用时持有者即赢家;修法已点名(resolve 加存在性检查 + 复用本 PR 的 cascade seam)。同包串行:#5196 合入后作为下一单派发。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions