Зарисовка: а может к черту любовь ролевую модель?

Привет, Хабр! Писать статьи — дело приятное, но только если нет на плечах релиза. Релиз оказался марафоном на месяцы, где каждый день мы жили задачами и доработками. Мы делились на три фронта: кто-то закрывал критические баги («баг-фиксеры»), кто-то добивал бизнес-логику («бизнес-логеры»), а кто-то всерьез отрабатывал план «Б» — ставил свечи за успешный релиз («молитвенники за прод»). Играли мы на разных уровнях, но финальный босс у всех был один: система, которую мы героически толкали в ПРОД, как кота в переноску: и он не хочет, и нам страшно.

Зарисовка: релиз как он есть. Никто не хочет, но все идут

Но как бы там ни было, сегодня на ПРОДе живет большая система. Прям такая, что, если бы она была организмом, у нее были бы печень, почки и амбулаторная карта в Сфере Знания. 

Пользователи — сотни сотрудников. Система — новая, кнопки — непонятные, интерфейс — как квартира после переезда: ты вроде дома, но даже чайник включить страшно.

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

Зарисовка: «А я могу сюда зайти?» — «Можешь, но не надо».

Решение было очевидным: нужна ролевая модель.

По плану ролевую модель — разграничение видимости интерфейсов и данных на стороне БД — мы должны были выкатить через пару недель после запуска. Но в мире, где перечень техдолгов меняется быстрее, чем погода в Калининграде, пришлось действовать иначе. В итоге, бочком-бочком, мы затолкали ее в боевой релиз буквально на финишной прямой.

Зарисовка: релизный спринт в лицах.

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

Сцена 1. Режим бога: включен по умолчанию

Зарисовка: когда кнопок больше, чем идей, что с ними делать.

Еще до релиза мы давали фокус-группе из представителей заказчика тестировать интерфейс. Каждый раз это выглядело как мем с Винсентом Вегой из «Криминального чтива» — он стоит с пальто, озирается и явно не понимает, куда попал. Пользователи заходили в систему и оказывались в «режиме бога»: доступно всё — от критичных кнопок до внутренних экранов, которые они в реальной жизни никогда бы не использовали. На встречах неизменно звучал один и тот же вопрос: «А что будет делать обычный пользователь, которому половина этих функций вообще не нужна?»

Ситуация изменилась только с выходом на ПРОД, когда вместе с основным релизом мы включили ролевую модель. Она сразу отрезала лишнее: теперь кто-то видит аккуратный набор нужных инструментов, кто-то — только одну кнопку, а у кого-то и вовсе пустой экран. Никакой лишней информации, никакого хаоса — каждый получает ровно то, что ему нужно.

Сцена 2. Доступ — это не демократия

Зарисовка: кнопок нет, но вы держитесь.

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

У нас есть три роли:

  • Оператор — видит все элементы интерфейса на этом экране и может запускать функции, просматривать их логи, повторно запускать при необходимости.

  • Аудитор — видит список функций и их логи, но никаких кнопок запуска у него нет: для его роли они просто отсутствуют на интерфейсе, как будто их никогда и не существовало.

  • Админ — отвечает за настройку системы в целом, а этот экран ему ни к чему, поэтому он его вообще не видит.

По итогу: оператор работает на экране «от и до», аудитор — только наблюдает, админ — даже не подозревает, что экран существует.

Так как же это реализовать? Все просто — через пермишены.

Сцена 3. Пермишены: кирпичики ролевой модели

Каждый элемент интерфейса у нас связан с permission (по-нашему — «доступ»). И не «в целом к экрану», а буквально к каждому действию. Например, для нашего экрана «Функции системы» это может выглядеть так:

  • сам экран с функциями = FunctionsScreenView;

  • кнопка запуска конкретной функции = FunctionRun;

  • просмотр логов выполнения = FunctionLogsView.

Чтобы все было предсказуемо, мы завели головной пермишен на каждый экран (FunctionsScreenView). Его нет — экран невидим. Даже если все дочерние пермишены у тебя есть — ничего не отрисуется. Тут бы Вилли Вонка сказал: «Смотри сколько хочешь — не получишь!»

Что такое permission?
Если просто: это маркер, который говорит системе, что конкретный пользователь имеет право выполнить определенное действие или увидеть конкретный элемент интерфейса. Пермишены — это кирпичики, из которых строится ролевая модель.

В реальности мы отдаем фронту не дерево сложной вложенности, а плоский список разрешений. Например, для наших трех ролей это может быть так:

{
  "Operator": [
	"FunctionsScreenView",
	"FunctionRun",
	"FunctionLogsView"
  ],
  "Auditor": [
	"FunctionsScreenView",
	"FunctionLogsView"
  ],
  "Admin": []
}

Фронт получает этот список и уже сам решает, что показать: кнопка запуска есть — отрисовываем, нет — даже пустого места не будет.

Сцена 4. Архитектурный backstage

Ролевая модель нашей системы живет в пяти таблицах. На первый взгляд они простые, но именно от их связей зависит, что увидит пользователь на экране и сможет ли он это запустить.

Таблица

Назначение

Источник данных

users

Список всех пользователей системы.

Приходит из внешней системы

roles

Набор ролей с кодами и описаниями.

Приходит из внешней системы

permissions

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

Формируем и поддерживаем вручную

role_permission

Связь между ролями и их доступами (permissions).

Заполняем вручную

user_role

Соответствие пользователя его роли.

Приходит из внешней системы

Что здесь понимается под агрегатами

  • users и roles — агрегаты справочников внешних систем.

  • permissions — агрегат внутреннего справочника действий (сущности, которые мы сами придумываем и поддерживаем).

  • role_permission и user_role — агрегаты связей между справочниками.

ER-диаграмма

Почему у нас «один пользователь — одна роль»

Это не случайное ограничение, а осознанное архитектурное решение. Если разрешить несколько ролей для одного пользователя, появятся конфликтующие пермишены.

Пример:

  • Роль «Оператор» — видит экран и кнопку запуска.

  • Роль «Аудитор» — видит экран, но без кнопки запуска.
    Если дать обе роли, что показать на фронте? Кнопку? Тогда зачем ограничение для аудитора?

Решение:

  • Нужны расширенные права — создаем новую роль с полным набором необходимых пермишенов.

  • Это исключает сложную приоритетную логику, которую потом было бы трудно поддерживать.

Главное правило ролевой модели

Если функционал не должен быть доступен пользователю, он не должен даже знать о его существовании.

Так мы убираем риск «зайти куда не надо» просто из любопытства.

SQL: получение карточки пользователя и его пермишенов

SELECT
	u.id,
	u.name,
	u.email,
	p.code AS permission_code
FROM users u
JOIN user_role ur ON ur.user_id = u.id
JOIN role_permission rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = :userId

В результате мы получаем данные о пользователе и список всех его пермишенов.

Пример ответа API

GET /permissions/{userId}
Authorization: Bearer <токен>
Response: 
{
  "user": {
	"id": 42,
	"name": "Иван Иванов",
	"email": "ivan@example.com",
	"role": "OPERATOR"
  },
  "permissions": [
	"FunctionsScreenView",
	"FunctionRun",
	"FunctionLogsView"
  ]
}

Как это используется на фронте:

  • По объекту user отрисовывается карточка пользователя.

  • По массиву permissions фронт решает, какие кнопки и экраны показать.

Сцена 5. JSON-меню для твоего интерфейса

Мы уже разобрали, как в базе хранятся пользователи, роли и пермишены, и даже написали SQL, который достает их в одном запросе.
Следующий шаг — упаковать эти данные в удобный формат и отправить на фронт через API.

Процесс работы API:

  1. Аутентификация — пользователь логинится, API извлекает его userId из токена.

  2. Определение роли — по таблице user_role находим роль пользователя.

  3. Получение пермишенов — по связке role_permission → permissions собираем коды доступов.

  4. Формирование ответа:

    • объект user с информацией о пользователе (чтобы фронт мог отрисовать карточку или подпись в шапке),

    • массив permissions, по которому фронт понимает, что можно показывать.

  5. Отправка на фронт — ответ в JSON.

Пример запроса:

GET /permissions/{userId}
Authorization: Bearer <токен>

Пример ответа:

{
  "user": {
	"id": 42,
	"name": "Иван Иванов",
	"email": "ivan@example.com",
	"role": "OPERATOR"
  },
  "permissions": [
	"FunctionsScreenView",
	"FunctionRun",
	"FunctionLogsView"
  ]
}

Почему такой подход удобен:

  • Фронт получает готовое «меню возможностей» без лишней логики.

  • Любое изменение доступов в БД моментально отражается в интерфейсе, без деплоя фронта.

  • Нет «призрачных кнопок» — если в массиве нет пермишена, элемент просто не отрисовывается.

  • Выполняется главное правило ролевой модели: если чего-то нет в списке, пользователь даже не узнает, что такая функция существует.

Советы, проверенные болью и продакшеном

  1. Всегда начинай с головного пермишена.
    Это твой главный «входной билет» к экрану. Если его нет — экран не отрисуется, даже если у пользователя есть доступы к отдельным кнопкам внутри. Так ты защищаешь систему от случайных «обходов» и неожиданных путей доступа.

  2. Роль — не склад случайных пермишенов.
    Пермишены должны соответствовать реальным действиям пользователя в системе. Не нужно добавлять «на всякий случай» — так только увеличивается зона риска и растет хаос. Четкая роль = предсказуемый интерфейс и поведение.

  3. Одно правило доступа — один пользователь.
    Да, можно дать несколько ролей, но это рождает конфликты: одна разрешает, другая запрещает. Придется изобретать приоритеты, вес пермишенов и другие способы усложнить себе жизнь. Вместо этого создай расширенную роль с нужным набором прав.

  4. Отказ по умолчанию.
    Нет пермишена — нет элемента. Даже пустого места. Если функционал не положен, пользователь не должен о нем знать. Любопытство — мощная сила, но не в продакшене.

  5. Пермишены должны быть осознанными.
    Каждый код в permissions — это не случайная строчка в БД, а конкретное действие, за которое ты готов отвечать. Формируй этот список как часть архитектуры, а не как свалку «а давай еще вот это добавим».

Финал: от хаоса к прозрачности

Ролевая модель — это не только про безопасность, но и про удобство, когда она построена грамотно:

  • база данных хранит простые, но мощные связи,

  • API отдает фронту ровно то, что нужно,

  • интерфейс не перегружен лишними кнопками,

  • а пользователи не блуждают в режиме «угадай, что тут можно нажать».

Главное правило, которое стоит забить на внутренний баннер над рабочим местом:

Если доступ не положен — пользователь не должен даже знать, что функционал существует.

Так вы снизите количество случайных ошибок, упростите поддержку и, главное, убережете себя от ситуации, когда кто-то в пятницу вечером решает «просто посмотреть, что будет, если нажать».
Подсказка: ничего хорошего.

Спасибо, что дочитали статью до конца! Теперь моя очередь увидеть ваши истории: как вы строили ролевую модель и решали вопрос “видеть или не видеть”. Ну и парочку позитивных комментариев можно оставить — они, как пермишены, всегда повышают настроение.

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


  1. OlegIct
    09.09.2025 20:47

    в Oracle Database роль - сборник привилегий и других ролей. Ограничение роли назначить нельзя. Есть ощутимое на практике ограничение на число включённых ролей. Вероятно, неверно выбраны хабы: в СУБД всё не так, как кажется с middle tier