Привет! Меня зовут Валерия, я 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/
Добавить комментарий