Ролевой контроль в приложении: вариант реализации

от автора

Привет! Меня зовут Валерия, я 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.


ссылка на оригинал статьи https://habr.com/ru/articles/928104/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *