ADR-011. Разграничение доступа по модели ABAC (claims → функции → группы)
Полномочия описываются как claims (пара ключ+значение), собираемые в человекочитаемые функции, а функции — в группы, назначаемые пользователю. Backend проверяет claims на каждой команде/запросе, UI скрывает недоступные разделы и действия. Модель адаптирована из rtfm (ADR-SRV-023) без периметра — в домене ICD орг-структуры для скоупинга нет.
1. Контекст
До сих пор ICD работал без доступа: команды выполняет кто угодно, поля CreatedByUserId /
AssignedToUserId пишутся заглушкой (Guid.Empty), а решение «кто ввёл» vs «кто взял в работу»
зафиксировано, но не защищено (ADR-004 «Кто ввёл и кто взял в работу»).
Видение предполагает две роли — хранитель (ввод, проверка и корректировка, экспорт) и
администратор (конфигурация обработки), — но состав ролей и прав оставлен открытым вопросом
(OQ-V-1 в видение Сервиса). Требования уже опираются на «доступность
действий по правам»: набор операций проверки (FR-005 «Верификация»)
и удаление/переходы в очереди (FR-009 «Очередь обработки»).
Нужна модель разграничения, которая:
- Гранулярна — контроль на уровне отдельной команды/запроса и отдельного UI-действия, а не только грубых ролей.
- Настраивается администратором человекочитаемо, без участия разработчика после первичного посева.
- Едина для backend и UI — фронт потребляет подмножество тех же прав.
В rtfm та же задача решена как ABAC с двумя осями: функциональной (что можно делать) и периметром (к каким объектам — в терминах сетей/организаций). У ICD второй оси нет: сервис оцифровки карт не мультитенантен, признака для скоупинга данных в домене не существует. Поэтому переносим функциональную ось ABAC и исключаем периметр.
2. Решение
Принимается ABAC на функциональных claims (плоский, без периметра).
claim = пара (ключ, значение). Ключи — контролируемый реестр:
icd/cqrs/command— право на выполнение команды (значение = имя класса команды или*);icd/cqrs/query— право на выполнение запроса (значение = имя класса запроса или*);icd/ui— право на UI-элемент (значение —подсистема/ресурс/действие, напр.queue/delete, или*).
Функция — человекочитаемая единица полномочий (напр. «Хранитель», «Администратор», «Только просмотр»); её фактические права — набор claim-значений. Группа набирается из функций; пользователю назначаются группы. Функции сеет разработчик (посев в инфраструктуре), группы и назначения ведёт администратор.
Проверки на двух уровнях:
- Backend — базовый уровень: перед выполнением команда/запрос проверяется по имени своего
класса против claims пользователя (
icd/cqrs/command|icd/cqrs/query). Нет права → отказ (403). Проверка встроена в базовый хендлер — покрывает каждую команду/запрос автоматически, включая будущие (совместимо со skillsnew-command/new-query). Без проверки намеренно — только вход, выход и получение профиля. - UI — фронт получает из профиля только claims ключа
icd/uiи скрывает недоступные разделы меню и действия. UI — витрина прав, не граница безопасности (граница — backend).
Сессия. Вход по логину/паролю выписывает JWT с уникальным jti. Контекст пользователя (его
claims) собирается из БД (группы → функции → claim-значения) и кэшируется по jti на срок жизни
токена. Выход/отзыв — через список инвалидированных токенов + вытеснение контекста из кэша.
Dev-суперпользователь (break-glass). Один сид-пользователь проходит любые проверки (профиль
отдаёт */*). Только для разработки, на проде убирается. Единый источник — backend.
Периметр — вне охвата. Фильтрация данных по орг-структуре не вводится: скоупить нечем. Оставлено точкой расширения — добавляется отдельным решением при появлении разграничения (по образцу rtfm ADR-SRV-024), не ломая claim-инфраструктуру.
3. Обоснование и рассмотренные варианты
- RBAC (только роли). Пользователю назначаются роли с фиксированным набором прав. Просто, но
грубо: не даёт контроль на уровне отдельной команды/запроса и требует ручной расстановки
[Authorize]на каждом endpoint. Плохо ложится на CQRS-конвейер ICD и не даёт авто-покрытия новых операций. - ABAC с периметр-тегами на claim. Каждый claim несёт применимость (к каким объектам). Макс. гибкость, но заметно сложнее модель — а скоупить в ICD нечего. Избыточно.
- ABAC, плоские функциональные claims (выбрано). Гранулярность по каждой команде/запросу/ действию при человекочитаемой настройке (функции→группы). Единый словарь для backend и UI. Проверка в базовом хендлере по имени класса даёт авто-энфорс без ручной разметки и совместима с генераторами команд/запросов.
Функции и группы сохраняем даже для небольшого приложения ради двух вещей: администратор управляет доступом без разработчика, и модель консистентна с rtfm (общий тулинг и подход в проектах организации) — перенос ADR и кода дешевле, чем изобретать упрощение.
4. Следствия
Положительные:
- Тонкая гранулярность (по команде/запросу/действию) при человекочитаемой настройке.
- Единый словарь claims для backend и UI; фронт получает только
icd/ui-подмножество. - Авто-энфорс в базовом хендлере покрывает и текущие, и будущие команды/запросы.
Компромиссы:
- Модель тяжелее «двух ролей»: появляются сущности пользователей/функций/групп/токенов, посев функций, страница администрирования — оправдано управляемостью и преемственностью с rtfm.
- Кэш контекста по
jtiтребует инвалидации при изменении полномочий; между несколькими инстансами локальный кэш не распространяется (для мульти-инстанса — отдельно, отложено).
Вне охвата (точки расширения):
- Периметр — фильтрация данных по орг-структуре; вводится отдельным решением при появлении признака разграничения. Аддитивно: не затрагивает JWT/контекст/claims/guards, добавляет проверки только в те хендлеры, что работают с ограничиваемыми данными.
- Хеширование пароля — на этапе разработки сид-пароль в открытом виде; хеш + соль до продакшна.
- OAuth2 / OIDC, refresh-токен — позже.