# Аутентификация и авторизация 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 `; сервер проверяет подпись и срок, берёт из токена 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": "", "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`), по одному на роль: | 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` — кто я и что мне можно Нужен любой действующий токен; прав не требует. Единственная ручка, которую вызывают «просто с токеном». ```json { "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 — 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. Проверка руками ```bash # токен 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 аутентификации. Фронт для этого ничего не делает.