You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

12 KiB

线索域数据隔离在既有数据范围机制上的落地

Type: grilling Status: resolved Resolved: 2026-08-13

Question

三类角色(销售/部门负责人/超管)对线索、公海池的可见范围与操作权差异极大,全域横切。需定清如何用既有机制(DataScopeEnum + DataScopeInterceptor + SysRoleDataScope + ADR-0018 按模块可配)表达,而非新建机制。

需在本 ticket 定清:

  1. 线索模块暴露的权限点clue:*)清单:公海查看/领取/关注、我的线索 CRUD、线索管理(分配/合并/删除/导入导出)、线索设置(公海池 CRUD/导入导出)分别对应哪些权限点。参考 crm-dict 的 dict:* 做法。
  2. 数据范围维度:线索的数据范围按什么过滤?
  3. 公海池(crm-rule 侧)的数据范围:部门负责人只见本部门池、超管全见——同样套 DataScopeEnum 还是另有规则。
  4. 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,通行证不改这一条。
  • 通行证只放行 SELECTDataScopeInterceptor 只拦截 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 = ?),不做软删 + 失效标记。

依据:DataScopeInterceptorprepare 阶段实时查,天然立即失效,零额外成本;"谁何时被加进/移出"由 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 旁路:在 DataScopeInterceptorbuildCondition 里,对声明了行级授权来源的表,将 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] 撤销即失效:硬删授权行。