行级角色

行级角色支持对特定数据行的访问限制,即,限制对实体实例的访问。

没有行级角色的用户可以访问 资源角色 允许的所有实体实例。

创建行级角色

可以在设计时使用带注解的 Java 接口(参考 Studio 的 行级角色创建向导)或在运行时使用 Administration(管理) → Row-level roles(行级角色) 的 UI 界面创建行级角色。

每个角色有一个用户友好的名称和一个编码。给用户分配角色时使用编码,因此如果已经有用户分配了某个角色,不要再次修改此角色的编码了。

定义设计时角色的示例:

@RowLevelRole( (1)
        name = "Can see Orders with amount < 1000", (2)
        code = "limited-amount-orders")             (3)
public interface LimitedAmountOrdersRole {

    // ...
    void order(); (4)
1 @RowLevelRole 注解表示这个接口定义了行级角色。
2 用户友好的角色名。
3 角色的编码。
4 接口可以有一个或多个方法用来定义策略注解(见下文)。不同的方法只是用来对相关的策略进行分组。方法名没有限制,当角色在 UI 展示时,方法名作为 Policy group(策略组) 展示。

行级策略

行级角色通过指定 行级策略 为特定实体定义访问限制。

JPQL 策略

JPQL 策略指定 where(以及可选的 join)子句,在加载实体时使用。

JPQL 策略通过转换 JPQL(以及 SQL)运算符,在数据库级别过滤出受限的实体实例,因此有非常好的性能。但要记住,该策略仅影响对象图根实体的加载。如果一个实体可以作为集合加载到另一个实体的对象图中,需要为其定义 JPQL 和 断言 策略。

在设计时角色中,JPQL 策略使用 @JpqlRowLevelPolicy 注解定义,示例:

@RowLevelRole(
        name = "Can see only Orders created by themselves",
        code = "orders-created-by-themselves")
public interface CreatedByMeOrdersRole {

    @JpqlRowLevelPolicy(
            entityClass = Order.class,
            where = "{E}.createdBy = :current_user_username")
    void order();
}

请按照下列规则编写 JPQL 策略:

  • 使用 {E} 占位符代替 wherejoin 子句中的实体别名。框架将用查询中指定的真实别名进行替换。

  • where 中设置的文本会用 and 条件添加到 where 查询子句。因此无需使用 where 这个单词,框架会自动添加。

  • join 中设置的文本会添加至 from 查询子句中。应该以逗号、joinleft join 开头。

查询参数可以使用 会话和用户属性。例如,current_user_username 参数从用户的 username 属性中获取值。

可以为 User 实体添加项目特定的属性,并在 JPQL 策略中使用。例如,假设已将 region 属性添加到 UserCustomer 实体。然后,仅允许用户查看其区域内的实体限制对 CustomerOrder 的访问:

@RowLevelRole(
        name = "Can see Customers and Orders of their region",
        code = "same-region-rows")
public interface SameRegionRowsRole {

    @JpqlRowLevelPolicy(
            entityClass = Customer.class,
            where = "{E}.region = :current_user_region")
    void customer();

    @JpqlRowLevelPolicy(
            entityClass = Order.class,
            where = "{E}.customer.region = :current_user_region")
    void order();
}

断言策略

断言策略定义了在对实体执行不同操作时进行测试的断言。如果断言返回 true,则允许实体实例执行该操作。

可以为这些操作定义断言策略:READCREATEUPDATEDELETE

READ 断言在从数据库加载根实体和所有内部的实体集合(整个对象关系图)时进行测试,这一点与 JPQL 策略 不同。如果一个实体可以作为集合加载到另一个实体的对象图中,需要同时定义 JPQL 和断言策略。

CREATEUPDATEDELETE 断言在实体实例被创建、更新或从数据库中删除之前进行测试。

在设计时角色中,断言策略使用 @PredicateRowLevelPolicy 注解定义的,示例:

@RowLevelRole(
        name = "Can see only non-confidential rows",
        code = "nonconfidential-rows")
public interface NonConfidentialRowsRole {

    @PredicateRowLevelPolicy(
            entityClass = CustomerDetail.class,
            actions = {RowLevelPolicyAction.READ})
    default RowLevelPredicate<CustomerDetail> customerDetailNotConfidential() {
        return customerDetail -> !Boolean.TRUE.equals(customerDetail.getConfidential());
    }
}

此示例演示了一个行级角色,除了使用 前一节 定义的示例资源角色之外,还应使用此角色来限制对属性 confidential = trueCustomerDetail 实例的访问。无法使用 JPQL 策略从 Customer.details 集合中过滤掉 CustomerDetail 实例,因为在一次数据库操作中与其所属的 Customer 一并加载。而断言策略是在内存中对根实体和嵌套集合都执行测试。

在运行时角色中,断言策略是使用 Groovy 脚本定义的。在脚本中,使用 {E} 占位符作为包含测试实体实例的变量。例如,与上面设计时角色相同的条件可以写成以下 Groovy 脚本:

!{E}.confidential

访问 Spring Bean

如需在断言中访问 Spring bean,可以从方法返回 io.jmix.security.model.RowLevelBiPredicate。这个功能接口支持定义 lambda,使用两个参数:被检查的实体实例和 Spring 的 ApplicationContext。示例:

@RowLevelRole(
        name = "Can see Customers of their region",
        code = "same-region-customers-role")
public interface SameRegionCustomersRole {

    @PredicateRowLevelPolicy(
            entityClass = Customer.class,
            actions = {RowLevelPolicyAction.READ})
    default RowLevelBiPredicate<Customer, ApplicationContext> customerOfMyRegion() {
        return (customer, applicationContext) -> {
            CurrentAuthentication currentAuthentication = applicationContext.getBean(CurrentAuthentication.class);
            return customer.getRegion() != null
                    && customer.getRegion().equals(((User) currentAuthentication.getUser()).getRegion());
        };
    }
}

在 Groovy 脚本中,可以使用 applicationContext 变量来访问任何 Spring bean,示例:

import io.jmix.core.security.CurrentAuthentication

def authBean = applicationContext.getBean(CurrentAuthentication)

{E}.region != null && {E}.region == authBean.user.region