Модель (JWT без сессий, права как данные, порядок проверок право → видимость → статус), режимы dev/прод,
POST /dev/token и GET /me с семантикой 401/403/404, сводка прав по маршрутам, каталог прав и ролей,
policy-слой, эталонный клиент web/src/{api,auth}, чек-лист, проверка руками, открытые вопросы (OQ-54).
Ссылка добавлена в dev/README.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
23 KiB
Аутентификация и авторизация 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. Модель в трёх предложениях
- Сессий на сервере нет. Каждый запрос к
/api/v1/*несётAuthorization: Bearer <JWT>; сервер проверяет подпись и срок, берёт из токена email, находит пользователя вapp_userи его права. Нет cookie, нет refresh, нет logout-ручки: «выйти» = забыть токен на клиенте. - Права — данные, роли — нет. Что можно пользователю, решает множество кодов прав из
GET /me → permissions(объединение прав всех его ролей). Код роли (KA_RF,ADMIN…) в бизнес-логике сервера не проверяется и фронтом использоваться для ветвления не должен: матрица «роль → право» — черновик, администратор правит её в UI. - Клиент только прячет, сервер решает. Скрывать пункты меню и кнопки по
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), по одному на роль:
| Роль | Филиал | Видимость | |
|---|---|---|---|
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, /files/{id}, /documents/{id} |
DOCUMENT_VIEW |
DOCUMENT_EDIT (загрузка, удаление, подтверждение) |
|
/consolidations, /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 — 404NOT_FOUND, как несуществующий. - Редактируемость версии консолидации. Правки — только в статусах «в работе» (
INITIATED,DOCUMENTS_PROCESSING,DATA_CORRECTION) и при своей активной блокировке; иначе 409VERSION_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. Чек-лист для нового экрана или клиента
- Все запросы — через единый HTTP-клиент, который сам подставляет
Bearerи глобально обрабатывает 401. - После получения токена и при старте с сохранённым токеном — один
GET /me; профиль — в контексте, не в каждом экране. - Видимость элементов — по
me.permissions, константы — из одного модуля; роли не проверять. - 403 — показать причину из
detail; 404NOT_FOUND— «не найдено», без ветвления по тексту; 409/422 — поcode. - Деньги в ответах — строки
"100.00", не парсить воfloat(общее правило контрактов). - Токен не писать в URL,
localStorageи логи; на сервере он тоже не логируется. - Слой получения токена (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.
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 аутентификации. Фронт для этого
ничего не делает.