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

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

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

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

Создавать роли уровня строк можно во время разработки с помощью аннотированных интерфейсов Java (см. мастер ролей уровня строк в Студии) или во время выполнения с помощью экранов 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) и отфильтровывает запрещенные экземпляры на уровне базы данных, что очень эффективно с точки зрения производительности. Но нужно иметь в виду, что это влияет только на корневую сущность загруженного графа объектов.

Если cущность может быть загружена

  • как корневая, или

  • как коллекция в графе объектов другой сущности,

определите для нее как JPQL политику (для фильтрации при загрузке корневой сущности) так и предикатную политику (для фильтрации вложенной коллекции).

Создание политики JPQL

Роль уровня строк может содержать любое количество JPQL политик. При написании JPQL политик учитывайте следующие правила:

  • Используйте плейсхолдер {E} вместо алиаса сущности в выражениях where и join. Фреймворк заменит его реальным алиасом, указанным в запросе.

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

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

В роли, определенной во время разработки, JPQL-политика определяется аннотацией @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 не является обязательным. Часто его можно заменить указав цепочку атрибутов в условии where – в таком случае JPA неявно создает нужный join. Таким образом, предыдущий пример можно записать так:

@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 становится необходимым, когда между сущностями нет прямой связи. Для иллюстрации, рассмотрим следующий граф объектов:

row level roles diagram

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

Aтрибуты сессии и пользователя

Можно использовать параметры атрибутов сессии и пользователя. Например, параметр 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-политиках. Например, представим, что к сущностям 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();
}

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

Предикатная политика связывает булево условие (предикат) с операцией над сущностью – READ, CREATE, UPDATE, or DELETE. Если предикат возвращает true, операция разрешена для данного экземпляра сущности.

Момент проверки предиката зависит от операции:

  • READ предикат проверяется при загрузке сущности из базы данных. Проверка охватывает как корневую сущность, так и (в отличие от JPQL‑политики) все вложенные коллекции графа загруженных объектов. Поскольку проверка происходит в памяти, при работе с большим количеством экземпляров она может значительно снижать производительность, так как каждый экземпляр загружается и проверяется отдельно.

    Если cущность может быть загружена

    • как корневая, или

    • как коллекция в графе объектов другой сущности,

    определите для нее как 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