# 平台级行级授权(sys_row_grant)能力 Type: design Status: open Depends on: 05(三机制权限模型已定稿) ## 背景 ticket 05 定稿的三机制权限模型(RBAC + ABAC + ReBAC)中,**ReBAC / 行级授权**是一项跨模块通用能力,超出 `crm-lead` 边界,落在 `crm-auth` / `crm-base`。本 ticket 把它正式立项。 现状(已核实代码): - `@DataScope`(`crm-base`)只有 `module / ownerColumn / deptColumn` 三属性,无行级授权声明。 - `DataScopeInterceptor`(`crm-auth`)只拦截 SELECT,`buildCondition` 只产 `owner_id=X` / `dept_id IN(...)` / `1=0` / 不注入四种结果,**没有 `OR EXISTS(...)` 旁路**。 - `DataVisibilityContext.currentScope(moduleCode)` 按模块取档位;行级授权需要额外拿到"当前用户被授权了哪些资源"。 - `sys_row_grant` 表**不存在**,是全新工作。 ## 需在本 ticket 定清 1. **`sys_row_grant` 表结构**:字段、唯一索引、是否软删。 2. **`@DataScope` 注解扩展**:如何声明"这张表参与行级授权、授权来源类型是什么、resource_id 对应哪一列"。 3. **拦截器 `OR EXISTS` 改造**:`buildCondition` 在部门天花板条件外再 OR 一个 EXISTS 子查询。 4. **池维度翻译**:`resource_type = pool` 的授权如何展开为"该池下全部线索可见"(授权对象类型 ≠ 被查数据类型)。 5. **当前用户授权集合的加载时机**:请求级一次性查 vs 每表查。 --- ## Answer ### 1. sys_row_grant 表结构 ```sql CREATE TABLE sys_row_grant ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '被授权用户', resource_type VARCHAR(32) NOT NULL COMMENT '授权对象类型:pool/opportunity/customer/project', resource_id BIGINT NOT NULL COMMENT '授权对象ID(注意可能≠被查数据ID,见池维度翻译)', created_by BIGINT NOT NULL COMMENT '授权发起人', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_grant (user_id, resource_type, resource_id) ) COMMENT='行级授权通行证(ReBAC)'; ``` **设计决策**: - **硬删**(决策 [PRD-3]):撤销授权 = `DELETE FROM sys_row_grant WHERE user_id=? AND resource_type=? AND resource_id=?`。不设 `deleted` / `revoked_at` 列。"谁何时被加进/移出"由各业务模块的操作日志(如 `lead_history`)留痕,不靠本表。 - **数据稀疏**:只有被显式设为团队成员/协作人/负责人的行才落表,量级远小于业务数据,`uk_grant` 唯一索引足以支撑 EXISTS 查询。 - **`resource_type` 记"授权对象类型",可 ≠ 被查数据类型**:线索侧授权对象是 `pool`(不是 lead),查询时翻译(见第 4 节)。 ### 2. @DataScope 注解扩展 ```java public @interface DataScope { String module(); String ownerColumn() default "owner_id"; String deptColumn() default "dept_id"; // 新增:声明本表的行级授权来源 RowGrant[] rowGrants() default {}; } public @interface RowGrant { /** 授权对象类型,对应 sys_row_grant.resource_type */ String resourceType(); /** 本表哪一列与 resource_id 对齐(直接授权=主键列;池维度=pool_id 列) */ String resourceColumn(); } ``` 用例: - `lead` 表:`@DataScope(module="lead", ownerColumn="owner_user_id", rowGrants=@RowGrant(resourceType="pool", resourceColumn="pool_id"))` - 注意 `lead` 的领取人列是 `owner_user_id`(非注解默认 `owner_id`),须显式覆盖 `ownerColumn`。 - 授权对象是池,`sys_row_grant.resource_id` 存 pool_id,`lead.pool_id` 与之对齐 → **间接(池维度)授权**。 - 商机表(未来):`@DataScope(module="opportunity", rowGrants=@RowGrant(resourceType="opportunity", resourceColumn="id"))` - 授权对象是商机本身,`resource_id` 存商机 id,与主键对齐 → **直接(单条)授权**。 - `lead_pool` 表:**不声明 rowGrants**(决策 [E]:池配置管理不发通行证,走功能权限×数据范围)。 - `lead` 表关注:**不声明**关注相关的 rowGrant(决策 [F]:关注走 `id IN 关注表` 业务语义 + 部门天花板,不发通行证)。 ### 3. 拦截器 OR EXISTS 改造 `buildCondition` 在原有部门天花板条件外,若该表声明了 `rowGrants` 且当前用户有对应授权,则 `OR` 一个 EXISTS 子查询: ```sql -- 改造前(DEPARTMENTS 档) WHERE dept_id IN (10, 11, 12) -- 改造后(该表声明了 rowGrants) WHERE ( dept_id IN (10, 11, 12) OR EXISTS ( SELECT 1 FROM sys_row_grant g WHERE g.user_id = AND g.resource_type = 'pool' AND g.resource_id = <本表>.pool_id ) ) ``` **关键约束**: - **只改 SELECT**:`DataScopeInterceptor` 现状即只拦截 SELECT(`if (!SELECT) return proceed()`),行级授权天生只管读。写权无论如何都由 service 层判(决策 [PRD-2],只读)。 - **ALL_VISIBLE 档不注入**:超管全见,跳过整个改造,与现状一致。 - **NONE_VISIBLE(1=0)仍要 OR EXISTS**:无部门的用户若被行级授权,仍应看到被授权的行 → `(1=0 OR EXISTS(...))`。这是"无归属但有通行证"的正确行为。 - **多个 rowGrants**:一张表可声明多个来源,逐个 OR。 ### 4. 池维度翻译(授权对象 ≠ 被查数据) 核心:`sys_row_grant` 里线索侧存的是 `(user_id, 'pool', pool_id)`,而查询打在 `lead` 表。翻译靠 `@RowGrant.resourceColumn = "pool_id"` 完成——EXISTS 子查询用 `g.resource_id = lead.pool_id` 关联,无需把"池下全部线索"逐条落进 `sys_row_grant`(**间接授权**)。 - **直接授权**(商机团队成员):`resourceColumn = "id"`,`g.resource_id = 商机.id`。 - **间接授权**(线索池负责人/协作人):`resourceColumn = "pool_id"`,`g.resource_id = lead.pool_id`。一条池授权自动覆盖该池下全部线索,池增减线索无需改授权表。 ### 5. 授权集合加载时机 **不预加载全集,走 SQL 相关子查询(EXISTS)实时判**: - 拦截器注入的是 `EXISTS(SELECT 1 FROM sys_row_grant WHERE user_id=? AND ...)`,由 DB 在执行时按行判定,无需在 Java 侧先把用户的全部授权查出来。 - 与 `DataVisibilityContext` 一致的请求级实时性:撤销授权后下一个请求的 EXISTS 立即命中不到(决策 [PRD-3] 立即失效是零成本默认行为)。 - `currentUserId` 从 `DataVisibilityContext` 取(需确认上下文暴露了 userId;若无则补一个 getter,与 `currentOwnership()` 同源)。 --- ## 实现任务拆解 1. **crm-base**:`@DataScope` 加 `rowGrants`,新增 `@RowGrant` 注解;`DataVisibilityContext` 暴露 `currentUserId()`(若尚无)。 2. **crm-auth**: - 建 `sys_row_grant` 表 + `SysRowGrant` 实体 + Mapper(授权/撤销:insert / delete)。 - `DataScopeTables` 扫描时收集 `rowGrants` 声明。 - `DataScopeInterceptor.buildCondition` 增加 OR EXISTS 分支。 3. **crm-rule**:`lead_pool` 的负责人/协作人绑定动作,在增删时同步 insert/delete `sys_row_grant(user_id, 'pool', pool_id)`。 4. **crm-lead**:`lead` 实体加 `@RowGrant(resourceType="pool", resourceColumn="pool_id")`。 5. **测试**:跨部门可见(有授权越天花板)、撤销即失效、无部门+有授权(1=0 OR EXISTS)、池增删线索自动生效(间接授权)、写权不受通行证影响(只读)。 ## 边界说明 - 商机/客户/项目模块的 `@RowGrant` 声明与团队成员增删触发,**待各模块立项时补**,本 ticket 只交付平台能力 + 线索侧接线。 - 本 ticket 属 `crm-auth`/`crm-base` 平台增强,不在 crm-lead/crm-rule/crm-preference 三份 spec 的核心交付内,但为它们的行级可见性提供底座。