行级角色
行级角色支持对特定数据行的访问限制,即,限制对实体实例的访问。
| 没有行级角色的用户可以访问 资源角色 允许的所有实体实例。 |
创建行级角色
可以在设计时使用带注解的 Java 接口(参考 Studio 的 行级角色创建向导)或在运行时使用 Security(安全) → 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 策略。编写时,请使用以下规则:
-
使用
{E}占位符代替where和join子句中的实体别名。框架将用查询中指定的真实别名进行替换。 -
where中设置的文本会用and条件添加到where查询子句。因此无需使用where这个单词,框架会自动添加。 -
join中设置的文本会添加至from查询子句中。应该以逗号、join或left join开头。
在一个设计时的角色中,使用 @JpqlRowLevelPolicy 注解添加策略,示例:
@RowLevelRole(name = "Can see orders of active customers",
code = "active-customers-role")
public interface ActiveCustomersRole {
@JpqlRowLevelPolicy(entityClass = Order.class,
join = "join {E}.customer c",
where = "c.active = TRUE")
void order();
}
join 子句是可选的。由于 JPA 会为路径表达式创建隐式连接表,因此,可以在 where 子句中使用路径表达式来达到相同的结果。于是前面的示例可以重写为:
@RowLevelRole(name = "Can see orders of active customers",
code = "active-customers-role")
public interface ActiveCustomersRole {
@JpqlRowLevelPolicy(entityClass = Order.class,
where = "{E}.customer.active = TRUE")
void order1();
}
两种写法生成同样的行级过滤器。
但是,如果实体之间没有直接的联系,那么需要显式使用 join。请看下面的对象图:
Customer 和 Employee 没有直接的引用属性,但仍然通过中间的 CustomerAccess 实体相关联。通过关联 CustomerAccess,可以创建一个角色,用于确定 哪个员工允许查看哪个客户:
@RowLevelRole(name = "AccessToCustomerRole", code = AccessToCustomerRole.CODE)
public interface AccessToCustomerRole {
String CODE = "access-to-customer-role";
@JpqlRowLevelPolicy(entityClass = Customer.class,
join = "join CustomerAccess ca on ca.customer = {E}",
where = "ca.employee.user.id = :current_user_id")
void customer();
}
会话和用户属性
可以在查询参数中使用 会话和用户属性。在下面的示例中,current_user_username 参数的值从当前用户的 username 属性获取。
@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();
}
应用程序特定属性
可以为 User 实体添加项目特定的属性,并在 JPQL 策略中使用。例如,假设已将 region 属性添加到 User 和 Customer 实体。然后,仅允许用户查看其区域内的实体限制对 Customer 和 Order 的访问:
@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();
}
谓词策略
谓词策略将一个布尔条件(谓词)与实体操作(READ、CREATE、UPDATE 或 DELETE)绑定。如果谓词返回 true,则该实体实例允许执行该操作。
使用谓词的时机因操作而异:
-
READ谓词在从数据库加载实体时进行测试。包括根实体以及(与 JPQL 策略 不同)加载对象图的所有引用属性集合。这种内存中的测试在大量实例上运行时会显著影响性能,因为实例会被逐个加载并测试。 -
CREATE、UPDATE、DELETE谓词在实体实例被创建、更新或从数据库删除之前进行测试。
在设计时角色中,谓词策略使用 @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 = true 的 CustomerDetail 实例的访问。无法使用 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