Repository navigation
记录删除后 source: 'manual' 的 sys_record_share 仍是孤儿 —— #4779 的 afterDelete 只覆盖规则共享,且只覆盖有规则的对象 #5103
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 4, 2026 - added and removedbugSomething isn't workingSomething isn't working
on Aug 4, 2026 裁定(维护者授权车道内继续,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
- addedbugSomething isn't workingSomething isn't workingand removed
on Aug 4, 2026 认领: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
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
- 对「热更新」要求给出了结构性更强的答案:不做 boot 枚举 + 订阅,而是一个全局 beforeDelete/afterDelete 对,在处理器内按当次 live
- added a commit that references this issue
on Oct 7, 2026
在 #4779(PR #5102)的实现中发现。未认领,归属
plugin-sharing,不在 #5102 的范围内。现象
#4779 的 issue 末尾记了「
rule-hooks没绑afterDelete,记录删除后sys_record_share行会成为孤儿」。PR #5102 补上了afterDelete,但它的覆盖面被两层条件夹住,剩下的两块仍然是孤儿:source: 'rule'的行。 这是刻意的 —— 手工共享是人对某一条记录做的决定,任何规则重算都不会重建它,所以规则子系统去扫它属于越界删数据。但结果是:记录被删后,它的source: 'manual'共享行原样留在表里。bindRuleHooks遍历的是rules里出现过的object_name。一个sharingModel: 'private'、只用手工共享、从没配过规则的对象,一个钩子都没有,删除后连规则共享的清理都不会发生(虽然它本来也没有规则共享)。即:手工共享 + 记录删除 = 永久孤儿行,对任何对象都成立。
危害与前提
和 #4779 记的那一条同源:今天危害有限,因为记录已经不存在,
buildReadFilter拼出来的record_id IN (...)匹配不到任何真实行。但这个「有限」完全依赖一个假设:记录 id 永不复用。 那个假设目前没有任何门禁保护 —— 一旦引入 id 复用、app 自定义主键、或者导入时保留原 id,这些孤儿行会立刻变成真实越权:新记录一落到旧 id 上,旧记录的共享对象就直接拿到了新记录的读/写权限。次要的:
sys_record_share单调增长,而 Setup 的 Record Shares 列表会展示指向不存在记录的行。建议方向(需要先定一件事)
清理孤儿共享该由谁负责,有两个自然位置,选哪个决定了它是「共享插件的事」还是「平台的事」:
afterDelete,撤销该记录的全部sys_record_share行(不分 source)。语义最干净 —— 记录没了,任何共享都不可能有效。代价是这个插件要在远比现在多的对象上挂写钩子,而「哪些对象启用了 sharing」是运行期由sharingModel决定的,和现在按规则表绑定的模型不一样。deleteBehavior/ 级联机制,把sys_record_share声明成指向任意对象的「弱引用」并在删除时清理。更通用(sys_attachment、sys_comment、sys_approval_request等一族按(object_name, record_id)挂靠的系统表是同一形状),但需要一个当前不存在的多态级联能力。另外无论选哪条,都值得补一个 boot 期的孤儿清扫(
sweepOrphanedRuleGrants已经是这个形状,只是它按「规则行还在不在」判断,而这里要按「记录还在不在」判断),这样历史数据也能收敛。相关
if (!id) return让sys_record_share授权在批量更新后变陈旧 #4779 / PR fix(plugin-sharing): 谓词式(multi)写入重算共享规则 —— 批量更新后 sys_record_share 不再陈旧 (#4779) #5102 —— 谓词式写入重算,顺带补了规则共享的afterDelete。installAttachmentAccessHooksdoes not authorize an UNSCOPED multi-delete: no id + nowherereads as "nothing to authorize" anddeleteManyruns over the whole table #4757、sys_commenthas no record-level authorization: any org member reads and writes comments on records they cannot see #4630 —— 同一 fail-open 家族的授权守卫。