using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;
namespace Ban.Sdaid.Icd.Arch.Tech.Angular
{
/// <summary>
/// ADR-UI-011: Claims и проверка прав на UI. Типизированный union Claim (подсистема/ресурс/действие
/// с wildcard), резолвер ClaimService, структурная директива *icdHasClaim, ClaimGuard через route.data,
/// фильтрация конфиг-действий в TS. Согласовано с backend (claim icd/ui).
/// </summary>
public class ADR_UI_011_Claims : IAdrDocument, IFolder<ArchAngularFolder>
{
public string Name => "ADR-UI-011. Claims и проверка прав на UI";
public string Description =>
@"UI скрывает пункты меню, кнопки и маршруты, недоступные по правам. Модель — типизированные
string-claims (union) формата подсистема/ресурс/действие с иерархическим wildcard; проверка изолирована в
резолвере ClaimService (директива, гард и компоненты спрашивают его). Согласовано с backend (claim icd/ui);
UI — витрина прав, граница безопасности — backend.";
public string Version => "0.1";
public string Status => "proposed";
public string[] Comments => new[]
{
"2026-08-20. Фронт-сторона ADR-011 (ABAC). Адаптировано из rtfm ADR-UI-011; словарь claims переписан под подсистемы ICD (input/queue/cards/documents/admin).",
};
public Type? Supersedes => null;
public static string S1_Context = $"""
## Контекст и постановка задачи
UI должен скрывать пункты меню, кнопки и целые маршруты, недоступные пользователю. Права собираются
на backend (ABAC: функции → claim-ы, {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)}) и
приходят в профиле как UI-claims (ключ `icd/ui`, см.
{nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_010_Auth)}).
Зафиксируем: модель прав на UI; как проверять право в шаблоне/гарде/коде; где живут сущности проверки.
**UI — витрина прав, а не граница безопасности:** реальная проверка — на backend
({nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_006_AuthImplementation)}); UI лишь прячет недоступное.
""";
public static string S2_DecisionDrivers = """
## Драйверы решения
1. **Декларативность** — проверки видны там, где применяются (шаблон, маршрут).
2. **Типобезопасность** — claim не произвольная строка: автокомплит, find-usages, rename, защита
от опечаток.
3. **Изоляция проверки** — решение о доступе в одном резолвере, не размазано по компонентам.
4. **Согласованность с бэкендом** — UI и API используют один набор claim-строк.
""";
public static string S3_ConsideredOptions = """
## Рассмотренные варианты
- **A. Роли на UI** — плохо масштабируются при тонких разрешениях; дублируют серверную логику.
- **B. Плоский список string-claims (выбрано)** — пользователь имеет набор строк; UI важны только
итоговые права. Уточнён типизированным union + резолвером.
- **C. RBAC-матрица на UI** — избыточно.
""";
public static string S4_DecisionOutcome = """
## Решение
Выбран **Вариант B**: типизированный union claim + резолвер `ClaimService`.
### Контракт claim
- **Формат**: `подсистема/ресурс/действие` (напр. `queue/delete`, `admin/user/edit`). Пункт меню —
`подсистема/list`.
- **Wildcard** (иерархия): `*/*` — полный доступ; `<подсистема>/*` — вся подсистема.
- **Источник** — `UserProfile.claims`; backend отдаёт claim-ы ключа `icd/ui` как есть.
### Типизированный `Claim`
Union строковых литералов `Claim` (`src/app/services/claim.service.ts`): автокомплит, find-usages,
rename, защита от опечаток.
```ts
export type Claim = '*/*' | 'input/list' | 'queue/list' | 'queue/delete' | 'admin/user/edit' | …;
```
### `ClaimService` — резолвер (изоляция проверок)
Единственное место принятия решения о доступе; меню/гарды/компоненты спрашивают его, не чекают inline.
```ts
@Injectable({ providedIn: 'root' })
export class ClaimService {
setClaims(claims: Claim[]): void { … } // из профиля (AuthStore.loadProfile)
hasClaim(claim: Claim): boolean { … } // */* → <подсистема>/* → точное
hasAnyOfClaims(claims: Claim[]): boolean { … }
}
```
### Директива `*icdHasClaim`
Скрывает элемент без нужного claim (семантика ANY), принимает `Claim | Claim[]`, реагирует на
claims-сигнал:
```html
<button *icdHasClaim="'queue/delete'">Удалить</button>
```
`src/app/directives/has-claim.directive.ts`.
### Конфиг-управляемые действия (`ClaimService.hasClaim` в TS)
Структурную директиву можно навесить только на элемент шаблона. Действия из **конфиг-массива**
(напр. действия строки таблицы) фильтруются в TS через `computed`:
```ts
protected readonly rowActions = computed(() => [
...(this.claims.hasClaim('queue/delete') ? [deleteAction] : []),
]);
```
Правило: **кнопка в шаблоне → директива `*icdHasClaim`; элемент в конфиг-массиве →
`ClaimService.hasClaim()` в TS.**
### Просмотр без прав (иконка-«глаз»)
Если карточка/дровер служит и для просмотра, и для редактирования: есть `…/edit` → «карандаш»,
иначе → «глаз» (тот же экран на просмотр). Кнопки сохранения/удаления внутри закрыты своими claim-ами.
Замечание: экран-просмотр всё равно дёргает свои `icd/cqrs/query` — у функции «только просмотр» они
должны быть, иначе 403.
### Гард `ClaimGuard` (через `route.data`)
`CanActivateFn`; требуемые claim-ы — в `route.data.requiredClaims` (семантика ANY); при отказе →
`/access-denied`.
```ts
{ path: …, canActivate: [ClaimGuard], data: { requiredClaims: ['queue/list'] satisfies Claim[] } }
```
### Страница «Доступ запрещён»
`/access-denied` (сюда уводят `ClaimGuard` и 403 от backend, см. ADR-UI-010). Даёт выход: «На главную»
и «Войти под другим пользователем» (logout → `/login`), иначе пользователь без прав заперт.
### Чего не делаем
- Не вычисляем права из ролей на UI (плоский список claims).
- Не поддерживаем negative claims (`'!queue/edit'`).
- Wildcard — только иерархический (`*/*`, `<подсистема>/*`); произвольных масок нет.
""";
public static string S5_Vocabulary = """
## Словарь claim-ов (начальный)
Единый источник — union `Claim`; пополняется по ходу разработки экранов/действий. Подсистемы (1-й
сегмент): `input, queue, cards, documents, admin`.
**Разделы меню** (claim пункта/маршрута):
| Меню | claim |
|---|---|
| Ввод данных | `input/list` |
| Очередь обработки | `queue/list` |
| Информационные карты | `cards/list` |
| Документы | `documents/list` |
| Администрирование | `admin/list` |
**Действия** — `подсистема/ресурс/действие` (напр. `input/create` — сдать пакет; `queue/delete`,
`queue/forceDelete`; `cards/edit`, `cards/export`; администрирование — `admin/user/{list,edit}`,
`admin/group/{list,edit}`, `admin/function/list`). Наполняются по модулям (Фаза 2).
""";
public static string S6_PositiveConsequences = """
## Положительные следствия
- Один формат и одно имя claim на UI и в API.
- Типобезопасность (union) — опечатки ловятся компилятором.
- Декларативные проверки в шаблонах/маршрутах; решение — в одном резолвере.
""";
public static string S7_NegativeConsequences = """
## Отрицательные следствия и компромиссы
- Добавление claim требует пополнить union `Claim` (это и плюс — явный реестр).
- Переименование claim на бэке требует правки union на UI (зато find-usages/rename).
- UI-проверка не заменяет серверную: право показать ≠ право выполнить (граница — backend).
""";
public static string S8_Validation = """
## Проверка
- Пункты меню скрываются у пользователя без нужного claim — без правки шаблона.
- Прямой переход по URL на защищённую фичу без права → `/access-denied`.
- В коде нет чтения роли — только `ClaimService.hasClaim()`.
""";
public static string S9_OpenQuestions = """
## Открытые вопросы / отложено
- **Семантика ALL** (требовать все claim-ы) — сейчас только ANY; добавим при первом случае.
- **Локализация запретов** — текст «нужно право X» на `/access-denied` (возможный ADR).
- **Сборка функций из словаря** — наполнение функций (наборы claim-ов) по мере стабилизации словаря.
""";
public static string S99_Related = $"""
## Связанные артефакты
- {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} — системное решение (ABAC).
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_010_Auth)} — источник claims (профиль), 401/403, `/access-denied`.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_006_AuthImplementation)} — backend-сторона (claim `icd/ui`, энфорс).
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_006_Routing)} — `ClaimGuard` в маршрутах.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_008_State)} — claims в сигнале.
""";
}
}