12 KiB
线索域数据隔离在既有数据范围机制上的落地
Type: grilling Status: resolved Resolved: 2026-08-13
Question
三类角色(销售/部门负责人/超管)对线索、公海池的可见范围与操作权差异极大,全域横切。需定清如何用既有机制(DataScopeEnum + DataScopeInterceptor + SysRoleDataScope + ADR-0018 按模块可配)表达,而非新建机制。
需在本 ticket 定清:
- 线索模块暴露的权限点(
clue:*)清单:公海查看/领取/关注、我的线索 CRUD、线索管理(分配/合并/删除/导入导出)、线索设置(公海池 CRUD/导入导出)分别对应哪些权限点。参考 crm-dict 的dict:*做法。 - 数据范围维度:线索的数据范围按什么过滤?
- 公海池(crm-rule 侧)的数据范围:部门负责人只见本部门池、超管全见——同样套
DataScopeEnum还是另有规则。 - ADR-0018 的"按模块可配"如何应用于 crm-lead / crm-rule:这两个模块是否各自注册为一个可配数据范围模块。
产出:三机制权限模型规格 + 可被管控点位候选清单 + 数据范围规则表。
Answer
1. 权限模型总览:RBAC + ABAC + ReBAC 三层
线索域的访问控制由三层正交机制组合,学名对应 RBAC + ABAC + ReBAC:
能否执行某动作 = RBAC(功能权限,已录入权限点则校验,未录入则放行)
能否看到某条数据 = ABAC(页面业务语义 AND 部门数据范围天花板)
OR ReBAC(行级授权通行证,仅越过部门天花板)
三层彼此正交、互不替代,必须分开实现:
| 层 | 学名 | 跟着谁走 | 实现方式 | 开关 |
|---|---|---|---|---|
| 功能权限 | RBAC | 角色 | 权限点管理录入后生效 | 配了才管,未配放行 |
| 数据可见范围 | ABAC | 当前用户代入规则 | 页面业务语义(service 写死)+ 部门天花板(DataScopeInterceptor 自动注入) |
always-on |
| 行级授权 | ReBAC | 具体人 + 具体资源 | sys_row_grant 通用表 + 拦截器 OR EXISTS 旁路 |
有授权关系才生效 |
2. 机制一:功能权限(RBAC)
功能权限控制"能不能点某按钮 / 调某接口",与"能作用到哪些行"无关。
- present-then-enforce:通过权限点管理录入后才校验;未录入则放行,不是代码里预定义固定清单。
- 每个权限点录两样:前端按钮编码(控按钮显隐/可点)+ 后端 API URL(控接口调用)。
- 本 ticket 不产出硬编码权限编码表,产出"可被管控点位候选清单"(见第 5 节),管理员按需择录。
3. 机制二:数据可见范围(ABAC)
ABAC 包含两块,都是"按属性算、不逐条落表":
3.1 第一层——页面业务语义(service 显式写死)
每个视图在 service 层显式加 WHERE,定义"这个页面展示哪一类数据":
| 视图 | 第一层业务条件 |
|---|---|
| 线索公海 | status = '待领取' AND pool_id IN (当前用户够得着的池) |
| 我的线索 | owner_id = 当前用户 AND status IN ('已领取','跟进中') |
| 我的关注 | id IN (SELECT lead_id FROM lead_follow WHERE user_id = 当前用户) |
| 线索管理 | 按筛选项,无固定业务条件 |
关键规则:
- "仅本人"的收窄完全靠第一层
owner_id = 我,不依赖第二层部门天花板。 - "我的关注"不走行级授权(见第 4 节 [F]);
id IN 关注表本身就是白名单,但仍受部门天花板约束——可见性每请求按当前部门实时算,防止"人事误操作→关注→改回部门→长期越权"。
3.2 第二层——部门数据范围天花板(DataScopeInterceptor 自动注入)
lead表挂@DataScope(module = "lead", deptColumn = "dept_id"),拦截器自动往 SELECT 注入dept_id IN (...)或不注入(ALL)。lead_pool表(crm-rule 侧)挂@DataScope(module = "lead", deptColumn = "dept_id"),复用同一模块配置(K-1a),部门负责人只见本部门池、超管全见。lead.dept_id含义:恒等于该线索所属公海池的归属部门,全生命周期不变动([I] 已定)。领取后 dept_id 不跟领取人主部门走——因为销售只能领取本部门公海池线索,故领取人与池必同部门,结果等价。
档位折算(按角色在 lead 模块配置,多角色取最宽):
| 典型角色 | 推荐档位 | 说明 |
|---|---|---|
| 销售 | DEPT(本部门) | 默认;管理员可按 ADR-0008 调宽 |
| 部门负责人 | DEPT_AND_CHILD(本部门及下属) | 覆盖下属部门公海+私海 |
| 超级管理员 | ALL(全部) | 不注入部门条件 |
两层叠加不冲突:三视图查同一张 lead 表,第二层注入的都是同一角色档位的 dept_id IN(...)(或 ALL),不打架;"我的 vs 公海"的区分完全由第一层业务条件承担。
4. 机制三:行级授权(ReBAC)
4.1 定位
ReBAC 是加法性质的通行证,只越过机制二的部门天花板,不改页面业务语义:
- 李四被设为 B 部门公海池的协作人 → 他在"线索公海"页能看到该池线索(越过部门天花板)。
- 但李四不会因此出现在"我的线索"页——那页语义是
owner_id = 李四,李四不是 owner,通行证不改这一条。 - 通行证只放行 SELECT(
DataScopeInterceptor只拦截 SELECT);写权由权限点 + service 层 owner/状态机判定,与通行证无关。
4.2 触发动作与授权颗粒度(PRD-1,产品已确认)
| 触发动作 | 授权颗粒度 | resource_type | 线索域翻译 |
|---|---|---|---|
| 将用户设为公海池负责人 | 池维度 | pool |
该池下全部线索可见(间接授权) |
| 将用户设为公海池协作人 | 池维度 | pool |
该池下全部线索可见(间接授权) |
| 将用户加入商机团队成员 | 单条 | opportunity |
该条商机可见(直接授权) |
| 将用户加入客户团队成员 | 单条 | customer |
该条客户可见(直接授权) |
| 将用户加入项目团队成员 | 单条 | project |
该条项目可见(直接授权) |
线索侧两项决策([D][E][F] 已定):
- [D] 线索不开
resource_type = lead单条直接授权通道。分配/指派 = 改owner_id,走第一层业务过滤,不发通行证。 - [E] 池配置管理不发通行证。"能不能改某个池配置" = 功能权限(菜单有没有配)× 数据范围(池 dept_id ∈ 用户部门集合)。
- [F] 线索关注不走行级授权。"我的关注" =
id IN 关注表AND 部门天花板,实时按当前部门算。
4.3 权限边界(PRD-2,产品已确认)
只读。通行证只放行"看得到";看到之后能不能操作(反馈/转商机/编辑/删除),由各自 service 层按 owner / 状态机 / 功能权限判定,通行证不授予任何写权。
4.4 撤销行为(PRD-3,产品已确认)
移出协作人/团队成员名单那一刻即失效:下一个请求的 OR EXISTS 旁路命中不到。授权行硬删(DELETE FROM sys_row_grant WHERE user_id = ? AND resource_type = ? AND resource_id = ?),不做软删 + 失效标记。
依据:DataScopeInterceptor 在 prepare 阶段实时查,天然立即失效,零额外成本;"谁何时被加进/移出"由 lead_history 操作日志留痕,不靠授权表本身。
4.5 实现说明(超出 crm-lead 边界,需独立 ticket)
行级授权为平台级通用能力,crm-auth 需新增:
sys_row_grant表:(id, user_id, resource_type, resource_id, created_by, created_at)。按(user_id, resource_type, resource_id)建唯一索引,数据稀疏,不影响列表查询性能。- 拦截器
OR EXISTS旁路:在DataScopeInterceptor的buildCondition里,对声明了行级授权来源的表,将 WHERE 改写为:(dept_id IN (...) OR EXISTS ( SELECT 1 FROM sys_row_grant WHERE user_id = #{currentUserId} AND resource_type = #{translatedType} AND resource_id = #{translatedId} ))池维度授权(
resource_type = 'pool')在翻译层展开为"该池下全部 lead_id"(pool_id = ?),无需逐条落表。 @DataScope注解扩展:增加rowGrantSource属性,声明该表的行级授权来源(resource_type+ 关联列),供拦截器翻译。
5. 可被管控点位候选清单(RBAC,供权限点管理录入)
管理员择需录入;此处仅为"有哪些点位可纳管"的备选表,不是运行期一定生效的清单。编码格式(前端按钮编码 + API URL)在权限点管理录入时由管理员给定,spec 不预置固定编码串。
"部门负责人只管本部门 / 超管管全部"这类部门收窄由机制二(数据范围)自动完成,不靠功能权限点表达。
crm-lead 候选点位:
| 分组 | 候选动作 |
|---|---|
| 线索公海 | 查看列表、领取、关注/取关 |
| 我的线索 | 查看列表、新增、编辑、反馈(含暂存/附件上传)、转商机、释放 |
| 我的关注 | 查看列表、取关 |
| 线索管理 | 页面访问、分配/批量分配、单条/批量删除、导入、导出、回收任意私海线索、激活失效线索(状态机 F2) |
crm-rule 候选点位(公海池配置):
| 分组 | 候选动作 |
|---|---|
| 线索设置-公海池 | 查看列表、新增、编辑(含负责人/协作人管理)、删除、导入、导出 |
6. 数据范围规则汇总(角色 × 视图 × 过滤维度)
| 视图 | 第一层(业务语义,service 写死) | 第二层(部门天花板,拦截器注入) | ReBAC 旁路 |
|---|---|---|---|
| 线索公海 | status='待领取' AND pool_id IN (够得着的池) |
dept_id IN (本部门集) |
池负责人/协作人:池维度 grant 翻译为 pool_id=? |
| 我的线索 | owner_id=我 AND status IN (已领取,跟进中) |
dept_id IN (本部门集) |
无(owner=我,天花板不额外收窄) |
| 我的关注 | id IN (关注表) |
dept_id IN (本部门集) |
无(关注不走 grant,[F]) |
| 线索管理 | 按筛选项 | dept_id IN (本部门集) |
同公海 |
| 公海池列表(crm-rule) | 无固定条件 | dept_id IN (本部门集)(复用 lead 模块) |
无(池配置管理不发通行证,[E]) |
超管档位 = ALL,第二层不注入任何条件,直接全量。
已 grill 定稿的核心模型(grill 过程记录)
数据隔离两层叠加
最终 WHERE = 第一层业务过滤 AND 第二层数据范围,分工如下:
第一层 —— 业务过滤(service 显式 SQL,定"哪个视图")
- "我的线索" =
owner_id = 当前用户 AND status IN (…) - "我的关注" =
id IN (当前用户关注表) - "线索公海" =
status = 待领取 AND 公海池 ∈ 当前用户够得着的池 - "线索管理" = 治理视图,业务条件按筛选项
- "仅本人"的收窄完全靠第一层
owner_id = 我,不依赖第二层。
第二层 —— 数据范围(DataScopeInterceptor 自动 AND,按角色档位)
- lead 表只挂一个
@DataScope(module = "lead", deptColumn = "dept_id"),只按dept_id做部门档兜底,不使用 owner 列(owner 过滤下放到第一层)。 - 档位折算:销售 → DEPT;部门负责人 → DEPT_AND_CHILD;超管 → ALL。
dept_id存"公海池的归属部门",全生命周期不变。
已定稿决策
- [I] 线索
dept_id恒定、永不变动(选 I-1)。 - [K] 公海池表复用
lead模块(选 K-1 + K-1a)。 - [D] 线索不开单条直接授权通道(产品确认)。
- [E] 池配置管理不发通行证(产品确认)。
- [F] 关注不走行级授权(产品确认)。
- [PRD-1] 触发动作:团队成员(商机/客户/项目,逐条)+ 公海池负责人/协作人(线索,池维度)。
- [PRD-2] 只读:通行证只放行 SELECT,写权由权限点 + service 判。
- [PRD-3] 撤销即失效:硬删授权行。