ADR-SRV-006. Аутентификация и энфорс ABAC на бэкенде

ADR Версия: 0.1 proposed

Бэкенд-реализация решения о доступе (ADR-011): вход по логину/паролю выдаёт JWT с уникальным jti, контекст пользователя (claims) собирается из БД и кэшируется по jti, а право на операцию проверяется в базовом хендлере по имени класса команды/запроса. Выход и отзыв — через список инвалидированных токенов. Фиксируем схему токена, точку энфорса, доменный модуль доступа и EF-раскладку. Периметр не вводится.

⟨/⟩ Исходник

1. Контекст и постановка задачи

ADR_011_AbacAuthorization выбрал ABAC на функциональных claims (claims → функции → группы, без периметра). Нужно зафиксировать, как это работает на бэкенде:

Точка встраивания уже есть: операции проходят через процессор и базовые хендлеры (ADR_SRV_001_Cqrs) — это естественное место для проверки прав. Сейчас аутентификации нет: CreatedByUserId пишется как Guid.Empty (ADR-004 «Кто ввёл и кто взял в работу»), pipeline ещё не введён.

2. Драйверы решения

  1. Сплошное покрытие. Право проверяется у каждой команды/запроса автоматически, включая будущие — без ручной разметки endpoint-ов.
  2. Минимум состояния на сервере. JWT + кэшируемый контекст; БД трогаем на входе/выходе и при промахе кэша.
  3. Настоящий выход. Возможность отозвать токен до истечения (logout) — через чёрный список.
  4. Совместимость с CQRS-конвейером (ADR_SRV_001_Cqrs) и генераторами команд/запросов (skills new-command/new-query).
  5. Конфиг и секреты по средам (ADR_SRV_004_AppConfigAndCors): подпись токена — секрет вне git.
  6. Единые правила сущностей/EF (ADR_SRV_003_EntityAndEfMapping).

3. Рассмотренные варианты

4. Решение

Выбран Вариант C. Аутентификация — JWT Bearer; авторизация — claim-проверка в базовом хендлере.

4.1. Схема токена

4.2. Pipeline в Program.cs

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o => { o.MapInboundClaims = false; o.TokenValidationParameters = ...; });

builder.Services.AddAuthorization(o =>
    o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
// ...
app.UseAuthentication();
app.UseAuthorization();

FallbackPolicy закрывает всё по умолчанию; открыто только помеченное [AllowAnonymous] — вход.

4.3. Энфорс права (базовый хендлер)

Базовые классы IcdBaseCommandHandler / IcdBaseQueryHandler перед выполнением спрашивают AbacService:

if (!await abac.CanCqrsCommandAsync(typeof(TCommand).Name, ct))
    throw new ForbiddenException($"Нет права на команду {typeof(TCommand).Name}");

CanCqrsCommandAsync / CanCqrsQueryAsync проверяют, есть ли у пользователя claim с ключом icd/cqrs/command (или .../query) и значением = имя класса или *. ForbiddenException маппится в HTTP 403 (обработчик исключений API). Без проверки намеренно — только LoginByPassword, Logout, GetUserProfile.

4.4. Контекст пользователя и кэш

4.5. Вход / выход / отзыв

4.6. Доменный модуль доступа (50_Domain/Access/)

Сущности (без инфраструктурного префикса, группировка папкой — как Capture/): User, Function, Group, Claim (справочник ключей), UserGroup, GroupFunction, FunctionClaimValue, UserActiveToken, UserInvalidatedToken. EF-конфигурации — в Infrastructure по ADR-SRV-003; отдельная миграция. Сид: функции + claim-значения сеет разработчик; dev-пользователь-администратор (break-glass) проходит все проверки (профиль отдаёт */*), на проде убирается.

4.7. Заполнение полей пользователя

CreatedByUserId / AssignedToUserId (ADR-004) заполняются из IUserContext.UserId, а не заглушкой.

4.8. Периметр — вне охвата

Фильтрация данных по орг-структуре не вводится (в домене нечего скоупить). Точка расширения: при появлении признака разграничения добавляется отдельным решением, не затрагивая токен/контекст/энфорс.

5. Положительные следствия

6. Отрицательные следствия и компромиссы

7. Проверка

8. Открытые вопросы / отложено

Связанные артефакты

Документы