using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;
namespace Ban.Sdaid.Icd.Arch.Tech.CSharp
{
/// <summary>
/// ADR-SRV-006: реализация аутентификации и энфорса ABAC на бэкенде. Фиксирует схему токена
/// (JWT + jti), где и как проверяются права (базовый хендлер по имени CQRS-класса), как собирается
/// и кэшируется контекст пользователя, как устроены вход/выход и отзыв токена. Периметр — вне охвата.
/// </summary>
public class ADR_SRV_006_AuthImplementation : IAdrDocument, IFolder<ArchCSharpFolder>
{
public string Name => "ADR-SRV-006. Аутентификация и энфорс ABAC на бэкенде";
public string Description =>
@"Бэкенд-реализация решения о доступе (ADR-011): вход по логину/паролю выдаёт JWT с уникальным jti,
контекст пользователя (claims) собирается из БД и кэшируется по jti, а право на операцию проверяется в
базовом хендлере по имени класса команды/запроса. Выход и отзыв — через список инвалидированных токенов.
Фиксируем схему токена, точку энфорса, доменный модуль доступа и EF-раскладку. Периметр не вводится.";
public string Version => "0.1";
public string Status => "proposed";
public string[] Comments => new[]
{
"2026-08-20. Backend-сторона ADR-011 (ABAC). Адаптировано из rtfm ADR-SRV-023 без периметра; пароль на dev — открытым текстом (хеш отложен).",
};
public Type? Supersedes => null;
public static string S1_Context = $"""
## Контекст и постановка задачи
{nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} выбрал ABAC на функциональных claims
(claims → функции → группы, без периметра). Нужно зафиксировать, **как это работает на бэкенде**:
- какой схемой аутентифицируется запрос и что несёт токен;
- где именно проверяется право на команду/запрос (чтобы покрыть все операции, а не расставлять
атрибуты вручную);
- как из токена получается контекст пользователя (его claims) и как он кэшируется;
- как устроены вход, выход и отзыв токена;
- где живёт доменный модуль доступа и как он ложится на EF.
Точка встраивания уже есть: операции проходят через процессор и **базовые хендлеры**
({nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_001_Cqrs)}) — это естественное место для проверки
прав. Сейчас аутентификации нет: `CreatedByUserId` пишется как `Guid.Empty`
({nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_004_CreatorVsAssignee)}), pipeline ещё не введён.
""";
public static string S2_DecisionDrivers = $"""
## Драйверы решения
1. **Сплошное покрытие.** Право проверяется у каждой команды/запроса автоматически, включая
будущие — без ручной разметки endpoint-ов.
2. **Минимум состояния на сервере.** JWT + кэшируемый контекст; БД трогаем на входе/выходе и при
промахе кэша.
3. **Настоящий выход.** Возможность отозвать токен до истечения (logout) — через чёрный список.
4. **Совместимость с CQRS-конвейером** ({nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_001_Cqrs)})
и генераторами команд/запросов (skills `new-command`/`new-query`).
5. **Конфиг и секреты по средам** ({nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_004_AppConfigAndCors)}):
подпись токена — секрет вне git.
6. **Единые правила сущностей/EF** ({nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_003_EntityAndEfMapping)}).
""";
public static string S3_ConsideredOptions = """
## Рассмотренные варианты
- **A. `[Authorize]`-атрибуты и политики на контроллерах/действиях.** Штатно для ASP.NET, но
разметка ручная: новую ручку легко забыть закрыть, а гранулярность «по команде» выражается
политиками неудобно.
- **B. Проверка только в middleware.** Централизованно, но middleware не знает, какая именно
CQRS-операция выполняется, — нет гранулярности «по классу команды/запроса».
- **C. Энфорс в базовом хендлере по имени CQRS-класса (выбрано).** Проверка стоит ровно там, где
исполняется операция; покрывает все команды/запросы автоматически; имя claim = имя класса.
""";
public static string S4_DecisionOutcome = """
## Решение
Выбран **Вариант C**. Аутентификация — JWT Bearer; авторизация — claim-проверка в базовом хендлере.
### Схема токена
- **JWT, подпись HMAC-SHA256.** `MapInboundClaims = false` (не переименовывать типы claim-ов).
- Токен несёт **минимум**: `jti` (уникальный id токена), `IcdUserId`, `IcdUserLogin`. **Права в
токен не кладутся** — они собираются из БД в контекст (см. ниже).
- Валидация: issuer, audience, lifetime, signing key; `ClockSkew` ~30 c.
- Параметры (issuer/audience/lifetime) — в `appsettings`; **signing key — секрет** (appsettings.Local
/ генератор конфигов, ADR-SRV-004).
### Pipeline в `Program.cs`
```csharp
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]` — вход.
### Энфорс права (базовый хендлер)
Базовые классы `IcdBaseCommandHandler` / `IcdBaseQueryHandler` перед выполнением спрашивают
`AbacService`:
```csharp
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`.
### Контекст пользователя и кэш
- `IUserContext` (scoped) — `UserId`, `Login`, `IsAuthenticated`, `Claims` (ключ → множество значений),
`HasClaimValue(key, value)`.
- `UserContextProvider` строит контекст из БД: группы пользователя → функции групп → claim-значения
функций, сгруппированные по ключу claim. Кэш — `IMemoryCache` по ключу `userctx:{jti}`, TTL = срок
жизни токена. Инвалидация: при logout и при изменении полномочий (правка групп/функций пользователя).
- **Коллизия имени `Claim`.** В `UserContextProvider` и `JwtTokenService` встречается
`System.Security.Claims.Claim`; чтобы не конфликтовать с доменным `Claim`, в этих файлах —
алиас `using SecurityClaim = System.Security.Claims.Claim;`. Прочий код домена импортирует только
доменный `Claim`.
### Вход / выход / отзыв
- **Вход** (`LoginByPasswordHandler`): проверить логин/пароль → сгенерировать `jti` (UUID v7) и JWT →
записать `UserActiveToken` (UserId, Jti, IssuedAt, ExpiresAt) → вернуть токен. Истёкшие active-записи
подчищаются.
- **Выход** (`LogoutHandler`): по `jti` из токена → перенести в `UserInvalidatedToken` (чёрный список),
убрать из active, вытеснить контекст из кэша.
- **Middleware чёрного списка**: для не-`[AllowAnonymous]` запросов — если `jti` в
`UserInvalidatedToken`, ответ **401**.
### Доменный модуль доступа (`50_Domain/Access/`)
Сущности (без инфраструктурного префикса, группировка папкой — как `Capture/`):
`User`, `Function`, `Group`, `Claim` (справочник ключей), `UserGroup`, `GroupFunction`,
`FunctionClaimValue`, `UserActiveToken`, `UserInvalidatedToken`. EF-конфигурации — в Infrastructure по
ADR-SRV-003; отдельная миграция. Сид:
функции + claim-значения сеет разработчик; dev-пользователь-администратор (break-glass) проходит все
проверки (профиль отдаёт `*/*`), на проде убирается.
### Заполнение полей пользователя
`CreatedByUserId` / `AssignedToUserId` (ADR-004) заполняются из `IUserContext.UserId`, а не заглушкой.
### Периметр — вне охвата
Фильтрация данных по орг-структуре не вводится (в домене нечего скоупить). Точка расширения: при
появлении признака разграничения добавляется отдельным решением, не затрагивая токен/контекст/энфорс.
""";
public static string S5_PositiveConsequences = """
## Положительные следствия
- Каждая команда/запрос закрыта правом автоматически — новую операцию нельзя «забыть» защитить.
- Права в БД, человекочитаемо администрируются; токен остаётся тонким.
- Настоящий logout: отозванный токен отклоняется до истечения.
- Контекст кэшируется по jti — БД не дёргается на каждый запрос.
""";
public static string S6_NegativeConsequences = """
## Отрицательные следствия и компромиссы
- Кэш контекста по jti требует инвалидации при изменении полномочий; между несколькими инстансами
локальный `IMemoryCache` не согласуется (мульти-инстанс — отдельно).
- Проверка в базовом хендлере обязывает операции, обходящие базовый класс, проверять право вручную.
- Чёрный список требует обращения к БД/кэшу на каждый защищённый запрос (митигируется кэшем).
- Доменный `Claim` требует алиаса в двух auth-файлах (осознанно, ради чистых имён в домене).
""";
public static string S7_Validation = """
## Проверка
- Запрос без токена к защищённой ручке → 401 (fallback policy).
- Пользователь без claim на команду → 403 при её вызове.
- После logout запрос с тем же токеном → 401 (чёрный список).
- Новый command-хендлер, унаследованный от базового, закрыт правом без дополнительной разметки.
- `CreatedByUserId` созданного пакета = id вошедшего пользователя, не `Guid.Empty`.
""";
public static string S8_OpenQuestions = """
## Открытые вопросы / отложено
- **Хеширование пароля.** На dev сид-пароль в открытом виде; до продакшна — хеш + соль.
- **OAuth2 / OIDC, refresh-токен** — позже, отдельными решениями.
- **Инвалидация кэша между инстансами** — для мульти-инстанса нужен внешний сигнал (шина/Outbox).
- **Периметр** — вводится при появлении признака разграничения (см. ADR-011, «вне охвата»).
""";
public static string S99_Related = $"""
## Связанные артефакты
- {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} — системное решение (ABAC), сторону которого реализует этот ADR.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_001_Cqrs)} — базовые хендлеры как точка энфорса.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_003_EntityAndEfMapping)} — правила сущностей и EF для модуля `Access/`.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_004_AppConfigAndCors)} — signing key и параметры токена как конфиг/секреты.
- {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_004_CreatorVsAssignee)} — поля пользователя, заполняемые из контекста.
- UI-сторона: {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_010_Auth)} (аутентификация), {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_011_Claims)} (claims на UI).
- Домен `50_Domain/Access/`, endpoints `45_Api/Endpoints/Auth/`, хендлеры `55_Backend/Handlers/Auth/` — оформляются отдельной задачей.
""";
}
}