От раскопок до космоса: permission-система без RBAC, которая вытащила нас из 2-летнего слоя if-ов и не тормозит, когда права плодятся

Архитектура и масштабируемость

Архитектуры, теория программирования
Single page application, толстый клиент
Тестирование фронтенда
Legacy системы, жизненный цикл продуктов
Управление и персонал в enterprise
Взаимодействие с серверной стороной (REST, GraphQL, gRPC)

Программный комитет ещё не принял решения по этому докладу

Целевая аудитория

- Архитекторам и tech lead, проектирующим доступ в корпоративных приложениях - Разработчикам, у которых роли множатся быстрее, чем матрица доступов - Тем, кто мигрирует с role-driven на permission-driven подход - Всем, кто сталкивался с «археологическими раскопками» чужих if-ов в проде

Тезисы

Если в вашей системе разграничение прав выглядит как 'if(isSupervisor && !isTrainer)', спрятанное где-то на 20-й странице — этот доклад для вас. У нас было 6 ролей и 2 года таких if-ов. Пока мы не построили permission-систему, которая выдержала форк на новую бизнес-единицу за 2 дня.

Контекст

Я работаю в ecom.tech — это продуктово-технологическая команда, которая делает технологии для ритейла, логистики и e-commerce. Внутри мы разрабатываем edukado — платформу управления стажировками для Самоката. Сегодня через неё проходит 600 обучений в месяц (20 000+ обучений за всё время) и это рабочий инструмент 1849 активных пользователей: 1284 наставника, 274 супервайзера, 197 сотрудников подбора, 60 админов, 20 тренеров, 14 руководителей.

Проблема

Изначально в системе было 2 роли: администратор и наставник. Просто, предсказуемо. Но бизнес рос, появлялись новые зоны ответственности — тренеры, подбор персонала, супервайзеры. На фоне огромного количества других требований про разграничение прав просто «забыли». Каждую новую роль на стороне фронтенда фиксировали точечным if-ом в коде, без единой модели, на бэке была свалка из пользователей в двух таблицах.

Как это выглядело на практике. Тренеры и группа подбора имели админский доступ — отдельной роли для них просто не было. Результат: сотрудник подбора из Новосибирска заходит в интерфейс и видит обучения всех регионов. Может ошибочно завершить чужое обучение. Просто потому, что интерфейс ему это позволяет. И такие инциденты были регулярными. Супервайзеры — те, кто управляет наставниками в регионах — вообще не имели доступа к системе. Бизнес просил: «дайте им хотя бы смотреть». Но даже «смотреть» требовало архитектуры.

За 2 года накопилось: 6 ролей, десятки ключей доступа, размазанных по 20+ страницам. Каждая ошибка — обращение в поддержку и точечный фикс в коде. В среднем на спринт.

Что нужно было спроектировать

Систему прав, которая: - выдерживает пересекающиеся, а не иерархические роли (один человек может быть наставником, тренером и супервайзером с разным набором действий); - даёт единую точку проверки вместо размазанных if-ов; - переживает смену набора ролей без переписывания архитектуры.

Как решали

Отказались от классического RBAC. Он предполагает наследование прав сверху вниз, а наши роли не вложены, а пересекаются — тренер не «выше» наставника, у него другой набор функций. Наследование означало бы случайные лишние права.

Вместо иерархии — плоская константа PERMISSIONS (в начале пути было 42 ключа, теперь — 70+ и их количество продолжает расти) и матрица UserRolesPermissions, сопоставляющая каждой роли её ключи. Каждое новое право — просто строка в константе и одна запись в матрице. Минус — и готово.

Поверх матрицы — 3 уровня ограничений (SELF / SELF_REGION / SELF_ROLE). Они отвечают не только на вопрос «есть ли право», но и «право на что именно»: свой профиль, свой регион, свою роль. Единая функция hasPermission применяется на трёх уровнях одновременно: - в навигации (какие пункты меню видны) - в действиях (когда рендерится кнопка) - в данных (какие записи возвращает запрос)

Потому что скрытая кнопка не спасает, если данные всё равно приходят с сервера.

Отдельная находка: сама матрица живет не как документ в Confluence (синхронизировать который «можно умом тронуться»), а как читаемый конфиг в коде — наглядная таблица «роль × право», которая одновременно служит инструментом «археологии» для легаси: новый человек в проекте за минуты видит, что реализовано и почему, вместо того чтобы распутывать двухлетний слой if-ов.

Профит

Добавление нового доступа к существующей фиче — 5 минут. Одна строка в матрице, проставить роли. Ограничивающий фактор — только цикл деплоя.

Систему адаптировали под компанию-побратима с другой оргструктурой, другим набором ролей, другой системой подразделений. Разграничение по ролям заняло 2 дня. Весь форк — месяц чистой разработки. Менять пришлось конфиг, а не архитектуру.

И ещё один уровень профита, который мы получили уже в процессе: перенесли конфигурацию прав с фронта на бэк. Точечные изменения теперь может вносить дежурная поддержка, а не разработчик.

Что бы сделали иначе

Если начинать заново: закладывать аудит логов для проверок прав с первого дня, покрывать permission-логику тестами сразу, а не постфактум, и синхронизировать модель прав с бэкендом на этапе проектирования. Но даже так — эта архитектура сэкономила нам месяцы разработки и пару седых волос.

Математик по образованию, участница ACM ICPC. В промышленной разработке с 2015 года — начинала в EPAM с enterprise SPA для финансового сектора. Сейчас React, TypeScript, GraphQL, миграции и архитектура. В ecom.tech с 2020 года: переезжала B2B-приложение с Angular на React, сейчас веду платформу управления обучением edukado, которая раскатывается на все операционные роли компании и смежные бизнес-единицы.

Специализация: frontend-архитектура, enterprise UI, платформенные решения, миграция сложных систем. Выступаю на профильных конференциях и на рок-концертах — и там, и там за клавиатурой.

Видео

Другие доклады секции

Архитектура и масштабируемость