Роли уровня строк

Роли уровня строк (row-level roles) позволяют ограничить доступ к определенным строкам данных или, другими словами, к экземплярам сущностей.

Пользователь без ролей уровня строк имеет доступ ко всем экземплярам сущностей, разрешенным ресурсными ролями.

Создание ролей уровня строк

Создавать роли уровня строк можно во время разработки с помощью аннотированных интерфейсов Java (see row-level role wizard в Студии) или во время выполнения с помощью экранов UI, доступных в разделе Security → Row-level roles.

Роль должная иметь удобное для пользователя имя и код. Код используется при назначении роли, поэтому не изменяйте его, если эта роль уже назначена некоторым пользователям.

Пример определения роли во время разработки:

@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 Интерфейс может иметь один или несколько методов для определения аннотаций политики (см. ниже). Различные методы используются только для группировки связанных политик. Имена методов могут быть произвольными, они отображаются как Policy group, когда роль показывается в UI.

Политики уровня строк

Роли уровня строк определяют ограничения, задавая политики уровня строк для определенных сущностей.

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} вместо алиаса сущности в выражениях where и join. Фреймворк заменит его реальным алиасом, указанным в запросе.

  • Содержимое where добавляется в выражение where запроса с использованием условия and. Добавление самого слова where не требуется, так как оно будет добавлено автоматически.

  • Содержимое join добавляется в выражение from запроса. Оно должно начинаться с запятой или слов join или left join.

Можно использовать параметры атрибутов сессии и пользователя. Например, параметр current_user_username получает свое значение из атрибута username.

Также можно добавить атрибуты, относящиеся к конкретному приложению, в сущность User и использовать их в JPQL-политиках. Например, представим, что к сущностям User и Customer был добавлен атрибут region. Затем можно ограничить доступ к объектам 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();
}

Предикатная политика

Предикатная политика определяет предикат, который проверяется при выполнении различных действий с сущностью. Если предикат возвращает значение true, данное действие разрешено для данного экземпляра сущности.

Предикатные политики можно определить для следующих действий: READ, CREATE, UPDATE, DELETE.

Предикат READ проверяется при загрузке сущности из базы данных для корневой сущности и (в отличие от JPQL-политики) всех вложенных коллекций вплоть до графа загруженных объектов. Если сущность может быть загружена как коллекция в графе объектов другой сущности, нужно определить для нее как 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());
    }
}

В этом примере показана роль уровня строки, которую следует использовать в дополнение к ресурсным ролям из примера в предыдущем разделе для ограничения доступа к экземплярам CustomerDetail, имеющим атрибут confidential = true. JPQL-политику нельзя использовать для фильтрации экземпляров CustomerDetail из коллекции Customer.details, поскольку она может быть загружена вместе с владельцем Customer в рамках одной операции базы данных. Предикатные политики выполняются в памяти и для корневых сущностей, и для вложенных коллекций.

В роли, определенной во время выполнения, предикатная политика определяется с помощью Groovy-скрипта. Используйте в нем плейсхолдер {E} в качестве переменной, содержащей экземпляр проверяемой сущности. Например, то же условие, что и в роли, определенной во время разработки выше, может быть записано в виде следующего Groovy-скрипта:

!{E}.confidential

Accessing Spring Beans

Если в предикате необходим доступ к бинам Spring, возвращайте из метода io.jmix.security.model.RowLevelBiPredicate. Этот функциональный интерфейс позволяет создавать лямбду, принимающую два параметра: проверяемый экземпляр сущности и ApplicationContext Spring. Например:

@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, например:

import io.jmix.core.security.CurrentAuthentication

def authBean = applicationContext.getBean(CurrentAuthentication)

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