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.
 
 
 
 
 
 

7.8 KiB

平台级行级授权(sys_row_grant)能力

Type: design Status: open Depends on: 05(三机制权限模型已定稿)

背景

ticket 05 定稿的三机制权限模型(RBAC + ABAC + ReBAC)中,ReBAC / 行级授权是一项跨模块通用能力,超出 crm-lead 边界,落在 crm-auth / crm-base。本 ticket 把它正式立项。

现状(已核实代码):

  • @DataScopecrm-base)只有 module / ownerColumn / deptColumn 三属性,无行级授权声明。
  • DataScopeInterceptorcrm-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 表结构

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 注解扩展

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 子查询:

-- 改造前(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 = <currentUserId>
              AND g.resource_type = 'pool'
              AND g.resource_id = <本表>.pool_id
        ) )

关键约束

  • 只改 SELECTDataScopeInterceptor 现状即只拦截 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] 立即失效是零成本默认行为)。
  • currentUserIdDataVisibilityContext 取(需确认上下文暴露了 userId;若无则补一个 getter,与 currentOwnership() 同源)。

实现任务拆解

  1. crm-base@DataScoperowGrants,新增 @RowGrant 注解;DataVisibilityContext 暴露 currentUserId()(若尚无)。
  2. crm-auth
    • sys_row_grant 表 + SysRowGrant 实体 + Mapper(授权/撤销:insert / delete)。
    • DataScopeTables 扫描时收集 rowGrants 声明。
    • DataScopeInterceptor.buildCondition 增加 OR EXISTS 分支。
  3. crm-rulelead_pool 的负责人/协作人绑定动作,在增删时同步 insert/delete sys_row_grant(user_id, 'pool', pool_id)
  4. crm-leadlead 实体加 @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 的核心交付内,但为它们的行级可见性提供底座。