Row-level Roles
Row-level roles allow you to restrict access to particular rows of data, in other words to entity instances.
A user without row-level roles has access to all instances of entities permitted by resource roles. |
Creating Row-level Roles
You can create row-level roles at design time using annotated Java interfaces (see row-level role wizard in Studio) or at runtime using UI views available at Security → Row-level roles.
A role has a user-friendly name and a code. The code is used when assigning the role to users, so don’t change if some users already have this role assigned.
Example of defining a design-time role:
@RowLevelRole( (1)
name = "Can see Orders with amount < 1000", (2)
code = "limited-amount-orders") (3)
public interface LimitedAmountOrdersRole {
// ...
void order(); (4)
1 | The @RowLevelRole annotation indicates that the interface defines a row-level role. |
2 | A user-friendly name of the role. |
3 | The role’s code. |
4 | The interface can have one or more methods to define policy annotations (see below). Different methods are used just to group related policies. Method names can be arbitrary, they are displayed as Policy group when the role is shown in UI. |
Row-level Policies
Row-level roles define restrictions by specifying row-level policies for particular entities.
JPQL Policy
JPQL policy specifies the where
(and optionally join
) clause to be used when loading the entity.
JPQL policy transforms the JPQL (and hence SQL) operator and filters out restricted instances on the database level, which is very efficient in terms of performance. But keep in mind that it affects only the root entity of a loaded object graph. If an entity can be loaded as a collection in another entity’s object graph, define both JPQL and predicate policies for it.
In a design-time role, the JPQL policy is defined using the @JpqlRowLevelPolicy
annotation, for example:
@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();
}
Consider the following rules when writing JPQL policies:
-
Use
{E}
placeholder instead of the entity alias inwhere
andjoin
clauses. The framework will replace it with a real alias specified in the query. -
The
where
text is added to thewhere
query clause usingand
condition. Adding thewhere
word is not needed, as it will be added automatically. -
The
join
text is added to thefrom
query clause. It should begin with a comma,join
orleft join
.
You can use session and user attributes in query parameters. For example, the current_user_username
parameter gets its value from the username
attribute.
You can add application-specific attributes to your User
entity and use them in JPQL policies. For example, imagine that you have added the region
attribute to the User
and Customer
entities. Then you can restrict access to Customer
and Order
entities by allowing users to see only entities of their region:
@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();
}
Predicate Policy
Predicate policy defines a predicate that is tested when performing different actions with the entity. If the predicate returns true, the action is permitted for the entity instance.
You can define predicate policies for the following actions: READ
, CREATE
, UPDATE
, DELETE
.
The READ
predicate is tested when the entity is loaded from the database for the root entity and (unlike JPQL policy) all nested collections down to the loaded object graph. If an entity can be loaded as a collection in another entity’s object graph, define both JPQL and predicate policies for it.
The CREATE
, UPDATE
, DELETE
predicates are tested before the entity instance is created, updated or deleted from the database.
In a design-time role, the predicate policy is defined using the @PredicateRowLevelPolicy
annotation, for example:
@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());
}
}
This example demonstrates a row-level role that should be used in addition to resource roles of the example from the previous section to restrict access to instances of CustomerDetail
having confidential = true
attribute. You cannot use a JPQL policy to filter out CustomerDetail
instances from the Customer.details
collection because it can be loaded together with the owning Customer
in a single database operation. Predicate policies are executed in memory for root entities and nested collections.
In a runtime role, the predicate policy is defined using a Groovy script. In the script, use the {E}
placeholder as a variable containing the tested entity instance. For example, the same condition as in the design-time role above can be written as the following Groovy script:
!{E}.confidential
Accessing Spring Beans
If you need to access Spring beans in the predicate, return io.jmix.security.model.RowLevelBiPredicate
from your method. This functional interface allows you to define lambda accepting two parameters: the examined entity instance and Spring’s ApplicationContext
. For example:
@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());
};
}
}
In the Groovy script, you can also use the applicationContext
variable to access any Spring bean, for example:
import io.jmix.core.security.CurrentAuthentication
def authBean = applicationContext.getBean(CurrentAuthentication)
{E}.region != null && {E}.region == authBean.user.region