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 in where and join clauses. The framework will replace it with a real alias specified in the query.

  • The where text is added to the where query clause using and condition. Adding the where word is not needed, as it will be added automatically.

  • The join text is added to the from query clause. It should begin with a comma, join or left 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