行级数据权限开启后,创建者反而看不到自己数据
上线「严格行级 scope」的那天,产品过来说:「诶,我刚录入的客户怎么不见了?」
日志里记录写入成功,数据库里确实有这条记录,但列表死活不显示。折腾二十分钟后发现——权限规则本身就是元凶。
现象
系统实现了部门级行级隔离:某部门用户只能看到「所属分组」中包含其部门的记录,没有分组标签的行对他们完全不可见(fail-closed)。
但新建记录时没有自动打标签,所以刚创建出来的行处于「无标签」状态,在 scope 查询下等同于隐身。于是:
- 用户手动填写表单创建 → 记录消失
- 后台导入文件建档 → 记录消失
- 异步任务在后台自动归档 → 记录消失,且这条路径根本没有人注意到
根因
严格 scope 的核心逻辑是:
-- 只返回与当前用户部门有关联的行
SELECT e.* FROM entity e
JOIN entity_scope_link esl ON esl.entity_id = e.id
WHERE esl.scope_id = :currentUserDeptId新建的行没有 entity_scope_link 记录,JOIN 自然匹配不到,查询结果里就没有它。权限系统完全正确地执行了规则——问题在于:规则没有覆盖「创建者本身」这个基线场景。
系统通常有不止一条创建路径。手动表单走 HTTP 路由,能拿到 currentUser().deptId;但文件导入、后台异步任务、AI 自动归档等路径在 worker 进程里运行,根本没有「当前登录用户」的概念。
大多数人只修了手动表单那条路,剩下的路径继续留坑,而这些路径往往是数据量最大的。
正解
1. 立规则:新建行自动打创建者 scope
每条新建记录,都要把创建者的部门 ID 写入 scope 关联表,作为基线。如果该用户有权手动选择分组,则取「手动选择 ∪ 创建者部门」的并集,不做替换。
const deptIds = unique([currentUser.deptId, ...manualPickedDeptIds]);
await db.entityScopeLink.createMany({
data: deptIds.map(id => ({ entityId, scopeId: id })),
skipDuplicates: true,
});2. 审计所有创建路径
用 grep 找出所有向目标表写入记录的路由 / 队列处理器 / 脚本,逐一确认是否补了 scope 标签。
grep -r "INSERT INTO entity\|entity.create\|entity.upsert" --include="*.ts" src/3. 异步路径:在同步入口存储 actor scope
异步 worker 不能调 getCurrentUser(),解法是在有用户上下文的同步入口(如上传文件的 HTTP 接口)就把部门 ID 持久化到任务记录里:
-- 上传接口时写入
ALTER TABLE extraction_tasks ADD COLUMN operator_dept_id INT;
-- ON DUPLICATE KEY UPDATE 里故意排除这列,防止后续 upsert 覆盖
INSERT INTO extraction_tasks (id, file_url, operator_dept_id, ...)
VALUES (?, ?, ?, ...)
ON DUPLICATE KEY UPDATE file_url = VALUES(file_url), ...
-- operator_dept_id 不在 UPDATE 列表里worker 异步归档时,先 SELECT 回 operator_dept_id,再 INSERT IGNORE 关联:
-- worker 里
SELECT operator_dept_id FROM extraction_tasks WHERE id = ?;
-- entity upsert 完成后
INSERT IGNORE INTO entity_scope_link (entity_id, scope_id)
VALUES (:entityId, :operatorDeptId);关键顺序:entity upsert 之后再做 scope 关联的 INSERT IGNORE。如果 entity 本身的保存逻辑会先 DELETE 再 INSERT 它的标签集,提前插入会被清掉。INSERT IGNORE 保证:对已有记录只添加不覆盖,匹配到现有 entity 时也不会重置它已有的其他 scope 标签。
排查步骤速查
- 确认记录在 DB 存在但查询不到 → 检查 scope JOIN 是否过滤了它
- 查
entity_scope_link是否有该 entity 的记录 - grep 全部创建路径,确认每条都打了 scope
- 异步路径确认:任务表有无
operator_dept_id列 + ON DUPLICATE UPDATE 里是否排除该列
一句话外卖
fail-closed 行级权限要配套「创建即打 creator scope」,异步路径的创建者身份必须在有请求上下文的同步入口就持久化——worker 进程里永远拿不到当前登录用户。