返回博客

行级数据权限开启后,创建者反而看不到自己数据

上线「严格行级 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);
正解:在同步入口存 actor,在异步尾端读回并 INSERT IGNORE

关键顺序:entity upsert 之再做 scope 关联的 INSERT IGNORE。如果 entity 本身的保存逻辑会先 DELETE 再 INSERT 它的标签集,提前插入会被清掉。INSERT IGNORE 保证:对已有记录只添加不覆盖,匹配到现有 entity 时也不会重置它已有的其他 scope 标签。

排查步骤速查

  1. 确认记录在 DB 存在但查询不到 → 检查 scope JOIN 是否过滤了它
  2. entity_scope_link 是否有该 entity 的记录
  3. grep 全部创建路径,确认每条都打了 scope
  4. 异步路径确认:任务表有无 operator_dept_id 列 + ON DUPLICATE UPDATE 里是否排除该列

一句话外卖

fail-closed 行级权限要配套「创建即打 creator scope」,异步路径的创建者身份必须在有请求上下文的同步入口就持久化——worker 进程里永远拿不到当前登录用户。