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 root, or

  • as a collection in another entity’s object graph,

create both a JPQL (for root loads) and a predicate policy (for nested loads) for that entity.

Creating a JPQL Policy

A row‑level role can contain any number of JPQL policies. Keep these guidelines in mind when writing them:

  • 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.

In a design‑time role, add policies with the @JpqlRowLevelPolicy annotation, for example:

@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();
}

The join clause is optional. Since JPA creates implicit joins for path expressions, you can often achieve the same result by using a path expression in the where clause. The previous example can therefore be rewritten as:

@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();
}

Session and User Attributes

You can use session and user attributes in query parameters. In the example below, the current_user_username parameter gets its value from the username attribute of the current user.

@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();
}

Application-specific Attributes

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 ties a boolean condition (the predicate) to an entity operation – READ, CREATE, UPDATE, or DELETE. If the predicate returns true, the operation is permitted for that entity instance.

The moment the predicate is tested differs by operation:

  • READ predicate is tested when the entity is loaded from the database. This includes both the root entity and (unlike JPQL policy) all nested collections down to the loaded object graph. This in-memory test can significantly impact performance when run on many instances because they will be loaded and tested one at a time.

    If an entity can be loaded

    • as a root, or

    • as a collection in another entity’s object graph,

    create both a JPQL (for root loads) and a predicate policy (for nested loads) for that entity.

  • 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.

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