arm-ka-docs/dev/frontend-auth-guide.md
2026-10-09 23:44:04 +04:00

24 KiB
Raw Blame History

Аутентификация и авторизация arm-core: инструкция для разработчика фронта

Источник — код arm-core на main (06.10.2026): api/src/core/auth.py, api/src/api/dependencies/auth.py, api/src/api/endpoints/{me,dev_auth}.py, api/src/domain/policy.py, эталонный клиент web/src/{api,auth}/; спека — arm-core/openspec/specs/authn-authz/spec.md; роли и права — context/05-domain-rules.md §10. Здесь — только то, что нужно, чтобы писать клиент. Правила предметной области и контракты экранов — в api-contract-slice-4.md и context/10-user-journey.md.

1. Модель в трёх предложениях

  1. Сессий на сервере нет. Каждый запрос к /api/v1/* несёт Authorization: Bearer <JWT>; сервер проверяет подпись и срок, берёт из токена email, находит пользователя в app_user и его права. Нет cookie, нет refresh, нет logout-ручки: «выйти» = забыть токен на клиенте.
  2. Права — данные, роли — нет. Что можно пользователю, решает множество кодов прав из GET /me → permissions (объединение прав всех его ролей). Код роли (KA_RF, ADMIN…) в бизнес-логике сервера не проверяется и фронтом использоваться для ветвления не должен: матрица «роль → право» — черновик, администратор правит её в UI.
  3. Клиент только прячет, сервер решает. Скрывать пункты меню и кнопки по permissions можно и нужно, но любой запрос может вернуть 401/403/404/409 — клиент обязан их обрабатывать, а не считать, что «кнопка была видна, значит можно».

Порядок проверок на любом защищённом маршруте: право (403) → видимость объекта (404) → статус/блокировка (409) → операция. Первые два — предмет этой инструкции.

2. Два режима получения токена

Разработка (DEBUG=true, AUTH_DEV_ISSUER=true) Прод (DEBUG=false)
Откуда токен POST /api/v1/dev/token по email, без пароля SSO РАИСА (Keycloak); SPA получает JWT от SSO и подставляет в заголовок
Подпись HS256, общий секрет AUTH_DEV_SECRET RS256/ES256 по AUTH_JWKS_URL или AUTH_PUBLIC_KEY
Срок 8 часов, продления нет задаёт РАИСА
Что в токене sub = email, email = email, iat, exp sub, exp, email в claim AUTH_EMAIL_CLAIM (по умолчанию email)
Ручка /dev/token есть не существует (404 {"detail":"Not Found"}), dev-токены отвергаются 401
CORS * без credentials только источники из CORS_ORIGINS; токен в заголовке, cookie не нужны
/docs, /openapi.json есть 404

Параметры JWT РАИСА неизвестны (OQ-54 в context/07-open-questions.md): URL JWKS, iss, aud, имя claim с email, как именно SPA получит токен (в старом АРМ — Keycloak через REACT_APP_KEYCLOAK_URL/REALM/CLIENT_ID, в web/package.json подключён raisa-fastapi-protected-web как optionalDependency). Поэтому слой «получить токен» на фронте держите отдельно от слоя «использовать токен»: второй уже не изменится, первый в проде заменится целиком.

3. Ручки

Префикс везде /api/v1. Форматы тела ошибки {"detail": …} — три: строка (401/403/404 «Not Found»/405/413), объект {code, message, …} (доменные ошибки, в том числе 404 NOT_FOUND и 409), массив pydantic ([{loc, msg, type}], 422 валидации тела). Эталонный разбор — web/src/api/client.ts:parseErrorBody.

3.1 POST /dev/token — dev-токен (только разработка)

Без авторизации. Тело {"email": "ka_rf@armka.local"} (регистр и пробелы по краям не важны).

Ответ Когда
200 {"access_token": "<JWT>", "token_type": "bearer"} активный пользователь с таким email есть; в журнал пишется LOGIN
404 строка пользователь не найден или отключён email не найден или is_active = false
409 строка неоднозначная учётная запись в БД несколько пользователей с email, различающимся регистром
422 массив пустой email
404 {"detail":"Not Found"} ручка выключена (DEBUG=false или AUTH_DEV_ISSUER=false) — так выглядит прод

Dev-пользователи (make seed-dev), по одному на роль (тот же make seed-dev создаёт и dev-ГК с ЮЛ под документы из fixtures — ГК «Русский Бисквит» и три ГК пакета ДКБ, см. qa-guide.md раздел 4.2; экраны справочников и консолидации не пустые сразу после сида):

Email Роль Филиал Видимость
admin@armka.local ADMIN Головной офис всё; единственный с USERS_ADMIN и DIRECTORY_ADMIN
ka_go@armka.local KA_GO Головной офис всё, правки
curator_go@armka.local CURATOR_GO Головной офис всё, правки справочников
expert@armka.local EXPERT Головной офис всё, только чтение
ka_rf@armka.local KA_RF Ростовский РФ свой филиал, правки
rka@armka.local RKA Ростовский РФ свой филиал
client_manager@armka.local CLIENT_MANAGER Краснодарский РФ свой филиал, чтение + загрузка документов

Для проверок разными ролями берите несколько токенов — они независимы, живут в разных вкладках.

3.2 GET /me — кто я и что мне можно

Нужен любой действующий токен; прав не требует. Единственная ручка, которую вызывают «просто с токеном».

{
  "id": "uuid",
  "email": "ka_rf@armka.local",
  "full_name": "Dev: Аналитик РФ",
  "regional_office": { "id": "uuid", "name": "Ростовский региональный филиал", "is_head_office": false },
  "business_segment": "DKB",
  "roles": ["KA_RF"],
  "permissions": ["CONSOLIDATION_EDIT", "CONSOLIDATION_VIEW", "DIRECTORY_EDIT", "…"]
}

roles и permissions — коды, отсортированы по алфавиту. business_segment может быть null. regional_office.is_head_office — единственное, что фронту полезно знать о видимости: пользователь ГО видит всё, пользователь филиала — только ГК своего филиала (деталей клиент не вычисляет, см. §5).

Когда дёргать.

  • Сразу после получения токена (dev-форма или SSO) — чтобы построить меню и контекст пользователя.
  • При старте приложения, если токен уже сохранён (перезагрузка страницы): ответ 200 — пользователь «вошёл», 401 — токен протух, показать вход.
  • Не перечитывается автоматически после того, как администратор поменял роли или права: пользователь увидит изменения после перезагрузки страницы. Это известное ограничение, не баг клиента.

Ответы /me и что они значат.

Ответ Смысл Что делать клиенту
200 вошёл сохранить профиль в контексте, строить UI по permissions
401 строка (есть WWW-Authenticate: Bearer) токена нет, он просрочен, подпись не прошла, «аутентификация не настроена» на сервере сбросить токен, показать вход
403 строка пользователь не зарегистрирован в АРМ / учётная запись отключена / неоднозначная учётная запись токен валиден, но этого человека нет в АРМ показать «нет доступа в АРМ, обратитесь к администратору»; повторный вход не поможет, токен сбрасывать не обязательно

3.3 Всё остальное — защищённые маршруты

Каждый маршрут объявляет одно право. Полные таблицы «метод и путь → право» — arm-core/README.md (разделы «API справочников», «API файлов и документов», «Консолидация»), для Среза 4 — api-contract-slice-4.md §2. Сводка:

Область Чтение Правки Прочее
/directories/regional-offices, /company-groups, /related-groups, /legal-entities DIRECTORY_VIEW DIRECTORY_EDIT удаление ГСК/алиаса — DIRECTORY_DELETE; POST /directories/import — DIRECTORY_ADMIN
/legal-entities/{id}/files, /consolidations/{cid}/legal-entities/{le}/files (GET — DOCUMENT_VIEW, POST — DOCUMENT_EDIT), /files/{id}, /documents/{id} DOCUMENT_VIEW DOCUMENT_EDIT (загрузка, удаление, подтверждение)
/consolidations (кроме …/legal-entities/{le}/files), /company-groups/{id}/consolidations, /consolidations/versions/{id}/* CONSOLIDATION_VIEW CONSOLIDATION_EDIT (создание, блокировка, статус, периметр, сопоставление) экспорт xlsx — CONSOLIDATION_VIEW
/admin/users, /admin/roles, /admin/permissions USERS_ADMIN USERS_ADMIN проверка права — до разбора тела: без права 403 всегда
/me любой пользователь —
/healthcheck (без префикса /api/v1) без токена — живость сервера, не признак «вошёл»

Права KFO_*, EX_VIEW, EXPORT, AUDIT_VIEW в каталоге есть, но маршрутов под них пока нет (Срезы 5–7).

Ответы, общие для всех защищённых маршрутов:

Ответ Смысл Что делать клиенту
401 строка как у /me сбросить токен, экран входа — глобально, одним обработчиком в HTTP-клиенте
403 строка недостаточно прав: требуется <КОД> права нет показать «нет прав»; обычно кнопка не должна была показываться — проверить permissions
403 строка про «не зарегистрирован / отключена / неоднозначная» пользователя удалили или отключили, пока токен жив как у /me: «нет доступа в АРМ»
404 объект {"code": "NOT_FOUND", …} объекта нет или он вне видимости пользователя — ответы неразличимы по статусу и коду показать «не найдено»; не ветвиться по тексту message
405 строка, заголовок Allow неверный метод на существующий маршрут ошибка клиента
404 {"detail":"Not Found"} строкой маршрута нет (в том числе /dev/token в проде) ошибка клиента или выключенный флаг
409 / 422 объект доменная ошибка (LOCK_REQUIRED, LOCKED_BY_OTHER, VERSION_NOT_EDITABLE, LAST_ADMIN, …) ветвиться по code; каталог — README «Ошибки API»

Как отличить два вида 403 программно: оба — строки без кода. Надёжный признак — контекст запроса: 403 на /me бывает только аутентификационным; на остальных маршрутах 403 почти всегда «недостаточно прав». Если нужно различать точнее, при 403 с текстом не про право повторите GET /me: он вернёт тот же 403 аутентификации.

4. Права и роли: что есть сейчас

Каталог прав (16 кодов, api/seed/core/permissions.yaml; формат <ОБЪЕКТ>_<ДЕЙСТВИЕ>):

Объект Коды
DIRECTORY DIRECTORY_VIEW, DIRECTORY_EDIT, DIRECTORY_DELETE, DIRECTORY_ADMIN (импорт)
DOCUMENT DOCUMENT_VIEW, DOCUMENT_EDIT
CONSOLIDATION CONSOLIDATION_VIEW, CONSOLIDATION_EDIT
KFO KFO_VIEW, KFO_EXPORT, KFO_APPROVE, KFO_VERIFY
EX EX_VIEW
EXPORT EXPORT
USERS USERS_ADMIN
AUDIT AUDIT_VIEW

Роли (7 системных, roles.yaml): KA_RF, KA_GO, RKA, CLIENT_MANAGER, CURATOR_GO, EXPERT, ADMIN. Стартовая матрица «роль → права» — role_permissions.yaml и таблица §10 05-domain-rules.md. Новых прав и ролей через API не создать (422 UNKNOWN_PERMISSION / UNKNOWN_ROLE); права роли меняет PUT /admin/roles/{code}/permissions, роли пользователя — PUT /admin/users/{id}/roles.

Правила для фронта:

  • Константы кодов прав — в одном модуле (web/src/auth/permissions.ts); завести код в клиенте можно только если он есть в permissions.yaml. Типы PermissionObject/PermissionAction — web/src/api/types.ts.
  • Элемент UI привязывается к праву, а не к роли: «кнопка «Взять в работу» видна при CONSOLIDATION_EDIT», а не «видна КА». Пользователь может иметь несколько ролей; permissions — уже объединение.
  • roles из /me — только для отображения («Роль: Кредитный аналитик РФ») и экрана администратора.

5. Чего клиент не вычисляет: policy-слой

Поверх прав сервер применяет правила видимости и редактируемости (domain/policy.py), которые фронт не дублирует:

  • Видимость по филиалу. Пользователь ГО (regional_office.is_head_office = true) видит все ГК, ЮЛ и версии; пользователь филиала — ГК, закреплённые за его филиалом, их ЮЛ и ГСК, монозаёмщиков своего филиала. Списки уже отфильтрованы сервером; чужой объект по id — 404 NOT_FOUND, как несуществующий.
  • Редактируемость версии консолидации. Правки — только в статусах «в работе» (INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION) и при своей активной блокировке; иначе 409 VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER. Для UI признаки есть в карточке версии (VersionCard.is_in_work, lock), см. api-contract-slice-4.md.
  • Зона обслуживания (business_segment) пока не фильтрует (OQ-86).

6. Как это сделано в эталонном клиенте web/

Повторять не обязательно, но контракт с бэкендом здесь уже отработан.

Файл Что делает
src/api/client.ts apiFetch: база ${REACT_APP_API_URL}/api/v1, ставит Authorization: Bearer из token.ts; на 401 сбрасывает токен и шлёт событие armka:unauthorized на window; парсит три формы detail в ApiError {status, code, message}; сетевую ошибку отдаёт как ApiError(0, 'NETWORK'); 204 и пустое тело → undefined
src/auth/token.ts токен в sessionStorage (ключ armka.token) с запасом в памяти, если хранилище недоступно. Вкладка закрылась — токен пропал; новая вкладка — новый вход (решение D-9)
src/auth/useMe.ts загружает /me, если токен есть; слушает armka:unauthorized и сбрасывает профиль с текстом «Сессия истекла»
src/auth/AuthGate.tsx корневая обёртка: loading → спиннер; нет профиля → экран входа; есть → MeContext.Provider
src/auth/MeContext.ts useCurrentUser() — профиль из контекста (без повторных запросов /me); вне AuthGate бросает
src/auth/DevLogin.tsx форма «Вход для разработки» → POST /dev/token → setToken → reload. В production-сборке вырезана по process.env.NODE_ENV; вместо неё заглушка «Вход выполняется через РАИСА» с кнопкой «Повторить»
src/auth/permissions.ts константы кодов прав для UI
src/widgets/Sidebar/Sidebar.tsx пункты меню с полем permission фильтруются по me.permissions
src/layouts/Admin.tsx без USERS_ADMIN показывает «Нет доступа» и не делает запросов к /admin/*

Окружение фронта: REACT_APP_API_URL (адрес бэкенда, в dev http://localhost:8000) и REACT_APP_ROOT_PATH (= ROOT_PATH бэкенда, в dev /); обе обязательны при npm run start. Для проверки входа используйте npm run start, а не npm run build: в production-сборке dev-формы нет.

7. Чек-лист для нового экрана или клиента

  1. Все запросы — через единый HTTP-клиент, который сам подставляет Bearer и глобально обрабатывает 401.
  2. После получения токена и при старте с сохранённым токеном — один GET /me; профиль — в контексте, не в каждом экране.
  3. Видимость элементов — по me.permissions, константы — из одного модуля; роли не проверять.
  4. 403 — показать причину из detail; 404 NOT_FOUND — «не найдено», без ветвления по тексту; 409/422 — по code.
  5. Деньги в ответах — строки "100.00", не парсить во float (общее правило контрактов).
  6. Токен не писать в URL, localStorage и логи; на сервере он тоже не логируется.
  7. Слой получения токена (dev-форма сейчас, SSO потом) изолирован от слоя использования.

8. Проверка руками

# токен
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
  -d '{"email":"ka_rf@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
# профиль
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
# 401: без заголовка
curl -s -i localhost:8000/api/v1/me | head -5
# 403 по праву: KA_RF без USERS_ADMIN
curl -s localhost:8000/api/v1/admin/users -H "Authorization: Bearer $TOKEN"
# 404 dev-ручки в проде — так же выглядит и несуществующий путь
curl -s localhost:8000/api/v1/no-such-route -H "Authorization: Bearer $TOKEN"

В Swagger (/docs, только при DEBUG=true): Authorize (замок справа вверху) → вставить токен без слова Bearer → все защищённые ручки получают заголовок. Кнопка есть с arm-core #29 (схема bearerAuth в openapi.json, components.securitySchemes); на стенде старше этого PR кнопки нет — проверяйте curl. Публичные ручки без замка: POST /dev/token, GET /healthcheck.

9. Открытые вопросы, которые затронут фронт

# Вопрос Что менять на фронте, когда решится
OQ-54 Параметры JWT РАИСА и способ получения токена SPA (Keycloak? raisa-fastapi-protected-web?) только слой получения токена: заменить DevLogin/ProdNotice на реальный SSO-поток; редирект на вход при 401
— Обновление токена (refresh) и его срок в проде если SSO даст refresh — обновлять в HTTP-клиенте перед 401
— Выход (LOGOUT в аудит не пишется, сессий нет) кнопка «Выйти» = clearToken() + редирект на SSO logout, если он будет
— Перечитывание /me после смены ролей администратором сейчас — перезагрузка страницы

Аудит входа (OQ-64, закрыт): сервер пишет LOGIN при выдаче dev-токена и при первом успешном запросе пользователя после старта процесса, LOGIN_FAILED — при 401 с предъявленным токеном и при 403 аутентификации. Фронт для этого ничего не делает.