Привет! Меня зовут Валерия, я java-разработчик компании SimbirSoft. В этой статье я хочу рассказать об одном из способов реализации ролевого контроля над действиями пользователей в системе. Механизм ролевого контроля позволяет сделать бизнес-процессы надежными с точки зрения информационной безопасности и привести их в соответствие с внутренними регламентами организации. Задачи подобного рода так или иначе возникают на любом проекте. 

Есть несколько способов решения. Они зависят от проблематики, требований, доменной области, пожеланий заказчика и т.д. Возможные варианты реализации:

  • Контроль доступа к методам через ldap группы из AD, проверяемые  в приложении: если пользователь входит в какую-то ldap-группу, то ему разрешены соответствующие действия.

  • Контроль доступа к методам через группы ldap, рассматриваемые как роли пользователя, которые помещаются в SecureContext и становятся доступны механизмам SpringSecurity.

  • Контроль доступа к эндпоинтам на основе внутренних ролей приложения (не обязательно завязанные на AD-группы).

  • Контроль доступа (с ролями и без) к объектам приложения на базе AOP.

  • Контроль доступа (с ролями и без) к объектам приложения на базе  Spring ACLs.

Однако довольно часто встречается комбинация методов, когда какие-то действия ограничиваются на уровне контроллера, какие-то на сервисном слое, а какие-то — в зависимости от объекта, который необходимо обработать. Мы рассмотрим такой вариант: роль приложения + ldap группа с ограничениями на уровнях контроллера и сервиса.

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

Авторизация в разрабатываемой системе идет через ldap, а права пользователя определяются через принадлежность к ldap-группе. То есть, сотрудник может работать в подразделении Управление, а иметь доступ к Складу, Охране и Бухгалтерии, если в ldap он состоит в соответствующих группах.

Архитектура разрабатываемой системы — MVC, то есть, созданы:

  • репозиторий-сервис-контроллер для управления документами;

  • репозиторий-сервис-контроллер для заявок;

  • репозиторий-сервис-контроллер для статей.

На уровне контроллеров разделения по подразделениям нет, однако есть разделение по наличию роли администратора (роль приложения). Ограничение на доступ к эндпоинтам редактирования по роли стоит на уровне фильтров Spring Security через AuthorizationManagerRequestMatcherRegistry:

requestMatchers(HttpMethod.PUT, "/papers/**").hasAnyAuthority(“ADMIN”)

То есть put-запрос, выполняющий редактирование статей, доступен только пользователям с ролью администратора. Обратите внимание, здесь (и далее) используются именно authorities, хотя называются ролью.  

Используется стандартный rest-подход: для создания документа посылаем Post-запрос на конечную точку /docs, редактирование – put-запрос на /docs/{guid}, удаление – delete на /docs/{guid}. Со статьями и заявками то же самое. Со стороны интерфейса, естественно, стоит защита, что пользователь не может передать на обработку тот объект, к которому у него нет доступа. Однако по внутренним требованиям безопасности защита должна быть и от curl-запросов. То есть, ролевой контроль нужно сделать именно на сервисном слое.

Описанную проблематику можно нагляднее представить следующей схемой (на примере объекта Документ):

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

Назовем это поле ldap-name и рассмотрим на примере DTO Документа, как это можно сделать:

DocumentDTO { @NotNull  DepartmentDTO department;}

DepartmentDTO { String ldap-name; }

Общая логика такова: метод вызывается авторизованным пользователем (то есть находящемся в SecureContext, а значит потенциально имеющим GrantedAuthority). В метод приходит DocumentDto с обязательно заполненным DepartmentDto. Нам необходимо, до попадания в метод, проверить, есть ли ldap-name из DepartmentDto Документа в списке ролей вызвавшего метод пользователя. Общая схема приведена на рисунке:

Мы видим классическую иллюстрацию для аспектного подхода: у нас есть валидационный метод, который мы привязываем к проверяемому перед его вызовом. В Spring Security для этой цели используется аннотация @PreAuthorize.

Для реализации описанной схемы нам необходимо сделать следующее: 

Первое. Реализовать IAuthService с методом получения ролей текущего пользователя (организацию перехода от полного наименования ldap-групп к ldap-name оставим за скобками, так как это отдельная тема). Приведем класс-реализацию сервиса:

@Service
public class AuthUserServiceImpl implements IAuthService {
    @Override
    public Set<String> getMySecureRoles() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        AuthUserDetails authDitails = (AuthUserDetails) authentication.getPrincipal();
        authDitails.getAuthorities();
        return authDitails.getAuthorities().stream().map(e -> e.toString()).collect(Collectors.toSet());
    }
}

Возможный ответ метода:

[
  "MONEY",
  "SKLAD",
  "CONTROL"
]

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

@Component
@RequiredArgsConstructor
@Component("checkerBean")
public class ActionCheckPermissionImpl implements IActionCheckPermission {

    private final IAuthService authService;
    
    private final DocRepo docRepo;
    
    private final DoMapper docMapper;

    @Override
    public void checkPermission(Object object) {
        String ldapName = "";
        if (object instanceof DocumentDto doc) {
            ldapName = doc.getDepartment).getLdapName0;
        }
        if (object instanceof Document doc) {
            ldapName = doc.getDepartment.getLdapName;
        }
        if (lauthService.getMySecureRoles().contains(ldapName)) {
            throw new AccessDeniedException("Access denied: you have not necessary role");
        }
        // do something ...
    }
}

Третье. В сервисном слое пометить аннотацией необходимые методы

public interface DocumentService {
    
    @PreAuthorize("isAuthenticated()")
    List<DocumentDto> getDocuments();

    @PreAuthorize("hasAuthority('ADMIN')")
    List<DocumentDto> getDocumentsForReview();

    @PreAuthorize(“@checkerBean.checkPermission(#dto)”)
    DocumentDto addDocument(@P(“dto”) DocumentDto dto);

    @PreAuthorize(“@checkerBean.checkPermission(#dto)”)
    List<DocumentDto> updateDocument(@P(“dto”) DocumentDto dto);

    @PreAuthorize(“@checkerBean.checkPermission(@documentRepo.findById(id).get())”)
    void deleteDocument(@P(“id”)UUID id);
}

То есть, dto, пришедшее в метод может быть таким:
{ department : { ldap-name: “SKLAD”}}

Если getMySecureRoles() вернет пользователю вот такой список: ["MONEY",  "SKLAD",  "CONTROL"], проверка будет успешно пройдена. Если же вот такой - ["MONEY", "CONTROL"] возникнет AccessDeniedException.

За скобками осталось несколько моментов использования @PreAuthorize, которые необходимо подсветить.

Во-первых, не забыть поставить аннотацию @EnbleWebSecurity над основным приложением.

Во-вторых, явно указать имя бина, который осуществляет проверку. В нашем случае это выполнялось так:

@Component(“checkerBean”)
public class ActionCheckPermissionImpl implements IActionCheckPermission {
  …
}

В-третьих, через аннотацию @P явно указать имена используемых параметров.

В этой статье мы рассмотрели реализацию ролевого контроля действий над объектами (через @PreAuthorize). Из приведенного примера видно, что @PreAuthorize предназначен для простой и эффективной проверки доступа на основе аспектного подхода. Если доступ отклоняется, он просто предотвращает выполнение метода, передавая управление обработчику ошибок. Если же вам нужно более детальное управление валидацией с возможностью выбрасывания специфических исключений, можно использовать самостоятельную привязку валидационных методов через чистые аспекты.

Спасибо за внимание!

Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.

Комментарии (0)