ADR-UI-011. Claims и проверка прав на UI

ADR Версия: 0.1 proposed

UI скрывает пункты меню, кнопки и маршруты, недоступные по правам. Модель — типизированные string-claims (union) формата подсистема/ресурс/действие с иерархическим wildcard; проверка изолирована в резолвере ClaimService (директива, гард и компоненты спрашивают его). Согласовано с backend (claim icd/ui); UI — витрина прав, граница безопасности — backend.

⟨/⟩ Исходник

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

UI должен скрывать пункты меню, кнопки и целые маршруты, недоступные пользователю. Права собираются на backend (ABAC: функции → claim-ы, ADR_011_AbacAuthorization) и приходят в профиле как UI-claims (ключ icd/ui, см. ADR_UI_010_Auth).

Зафиксируем: модель прав на UI; как проверять право в шаблоне/гарде/коде; где живут сущности проверки. UI — витрина прав, а не граница безопасности: реальная проверка — на backend (ADR_SRV_006_AuthImplementation); UI лишь прячет недоступное.

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

  1. Декларативность — проверки видны там, где применяются (шаблон, маршрут).
  2. Типобезопасность — claim не произвольная строка: автокомплит, find-usages, rename, защита от опечаток.
  3. Изоляция проверки — решение о доступе в одном резолвере, не размазано по компонентам.
  4. Согласованность с бэкендом — UI и API используют один набор claim-строк.

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

4. Решение

Выбран Вариант B: типизированный union claim + резолвер ClaimService.

4.1. Контракт claim

4.2. Типизированный Claim

Union строковых литералов Claim (src/app/services/claim.service.ts): автокомплит, find-usages, rename, защита от опечаток.

export type Claim = '*/*' | 'input/list' | 'queue/list' | 'queue/delete' | 'admin/user/edit' | …;

4.3. ClaimService — резолвер (изоляция проверок)

Единственное место принятия решения о доступе; меню/гарды/компоненты спрашивают его, не чекают inline.

@Injectable({ providedIn: 'root' })
export class ClaimService {
  setClaims(claims: Claim[]): void { … }   // из профиля (AuthStore.loadProfile)
  hasClaim(claim: Claim): boolean { … }     // */* → <подсистема>/* → точное
  hasAnyOfClaims(claims: Claim[]): boolean { … }
}

4.4. Директива *icdHasClaim

Скрывает элемент без нужного claim (семантика ANY), принимает Claim | Claim[], реагирует на claims-сигнал:

<button *icdHasClaim="'queue/delete'">Удалить</button>

src/app/directives/has-claim.directive.ts.

4.5. Конфиг-управляемые действия (ClaimService.hasClaim в TS)

Структурную директиву можно навесить только на элемент шаблона. Действия из конфиг-массива (напр. действия строки таблицы) фильтруются в TS через computed:

protected readonly rowActions = computed(() => [
  ...(this.claims.hasClaim('queue/delete') ? [deleteAction] : []),
]);

Правило: кнопка в шаблоне → директива *icdHasClaim; элемент в конфиг-массиве → ClaimService.hasClaim() в TS.

4.6. Просмотр без прав (иконка-«глаз»)

Если карточка/дровер служит и для просмотра, и для редактирования: есть …/edit → «карандаш», иначе → «глаз» (тот же экран на просмотр). Кнопки сохранения/удаления внутри закрыты своими claim-ами. Замечание: экран-просмотр всё равно дёргает свои icd/cqrs/query — у функции «только просмотр» они должны быть, иначе 403.

4.7. Гард ClaimGuard (через route.data)

CanActivateFn; требуемые claim-ы — в route.data.requiredClaims (семантика ANY); при отказе → /access-denied.

{ path: …, canActivate: [ClaimGuard], data: { requiredClaims: ['queue/list'] satisfies Claim[] } }

4.8. Страница «Доступ запрещён»

/access-denied (сюда уводят ClaimGuard и 403 от backend, см. ADR-UI-010). Даёт выход: «На главную» и «Войти под другим пользователем» (logout → /login), иначе пользователь без прав заперт.

4.9. Чего не делаем

5. Словарь 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).

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

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

8. Проверка

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

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

Документы