Andrey Epifancev 8bc76c69a0 docs(openspec): спеки синхронизированы с кодом issue #9 (ревью R-4)
- reference-data-seed: матрица role_permission целиком — только в пустую
  таблицу, в заполненную — лишь пары ролей/прав, вставленных тем же запуском
  (сценарии: снятое право, новые роль и право, все связи сняты); `--dev` только
  при DEBUG=true (код 2 до обращения к БД).
- audit-trail: требование «События входа» (LOGIN/LOGIN_FAILED, раздел SESSION)
  — исключение из «той же транзакции» с обоснованием, обрезка полей, доверенные
  прокси; строки в таблице событий.
- authn-authz: ссылка на аудит входа; требование «Прод-конфигурация
  HTTP-поверхности» — /docs, /redoc, /openapi.json только при DEBUG, CORS по
  CORS_ORIGINS, 405 на неверный метод, 404 под /api/v1, GET / без редиректа.

`openspec validate --specs`: 9 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:49:46 +04:00

98 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# authn-authz Specification
## Purpose
Вход в ядро по JWT внешнего SSO (РАИСА): пользователь находится по email в `app_user`, его эффективные права — объединение
прав его ролей из БД; любой защищённый маршрут объявляет требуемое право одной зависимостью. Для разработки — эмитент
токенов за флагом.
## Requirements
### Requirement: Личность из JWT, пользователь из БД
Защищённый маршрут SHALL требовать заголовок `Authorization: Bearer <jwt>`. Ядро SHALL проверить подпись и срок токена
настроенным проверяющим (dev HS256 при `AUTH_DEV_ISSUER=true`, иначе JWKS/публичный ключ по `AUTH_*`), извлечь email из
claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_user` с этим `email` и `is_active = true`. Нет или невалидный
токен → 401; пользователь не найден или неактивен → 403. Тело ошибки — `{"detail": "<причина по-русски>"}`.
Отказы в аутентификации (401 при предъявленном токене, 403 «не найден / отключён / неоднозначен») и вход SHALL
фиксироваться в журнале по требованию «События входа» спеки `audit-trail` (`LOGIN_FAILED` / `LOGIN`).
#### Scenario: Валидный токен активного пользователя
- **WHEN** запрос несёт корректный токен с `email = ka_rf@armka.local`, и такой активный пользователь есть
- **THEN** маршрут выполняется от имени этого `app_user`
#### Scenario: Пользователя нет в системе
- **WHEN** токен валиден, но `app_user` с таким email отсутствует
- **THEN** ответ 403, `detail` сообщает, что пользователь не зарегистрирован в АРМ
#### Scenario: Истёкший или подделанный токен
- **WHEN** срок токена истёк или подпись не проходит
- **THEN** ответ 401, тело `{"detail": …}`
### Requirement: Эффективные права и зависимость `require`
`effective_permissions(user)` SHALL быть объединением `permission.code` по всем ролям пользователя
(`user_role → role_permission → permission`); код роли в проверках MUST NOT использоваться. Зависимость
`require("<PERMISSION_CODE>")` SHALL возвращать 403, если кода нет в эффективных правах, и MUST NOT обращаться к БД
повторно в рамках одного запроса.
#### Scenario: Право есть
- **WHEN** пользователь с ролью `KA_GO` вызывает маршрут с `require("KFO_VERIFY")`
- **THEN** маршрут выполняется
#### Scenario: Права нет
- **WHEN** пользователь с ролью `EXPERT` вызывает маршрут с `require("DOCUMENT_EDIT")`
- **THEN** ответ 403, `detail` называет недостающее право
#### Scenario: Две роли
- **WHEN** пользователю назначены роли `KA_RF` и `CURATOR_GO`
- **THEN** его эффективные права — объединение прав обеих ролей без дубликатов
### Requirement: `GET /api/v1/me`
Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`,
`business_segment`, `roles` (список `code`), `permissions` (отсортированный список `code`). Это единственный маршрут
Среза 0, доступный любому аутентифицированному пользователю без `require`.
#### Scenario: Ответ для dev-пользователя
- **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me`
- **THEN** ответ 200 содержит `roles = ["KA_RF"]` и `permissions`, равные правам `KA_RF` из матрицы §10
### Requirement: Dev-эмитент токенов только в режиме разработки
При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`,
возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов
маршрут MUST NOT существовать (404), а dev-проверяющий MUST NOT принимать токены.
#### Scenario: Получение dev-токена
- **WHEN** `AUTH_DEV_ISSUER=true`, `DEBUG=true` и запрошен токен для `admin@armka.local`
- **THEN** ответ 200 `{access_token, token_type: "bearer"}`, токен принимается `/api/v1/me`
#### Scenario: Прод-конфигурация
- **WHEN** `DEBUG=false`
- **THEN** `POST /api/v1/dev/token` возвращает 404, токен, подписанный dev-секретом, отвергается с 401
### Requirement: Прод-конфигурация HTTP-поверхности
Документация API (`/docs`, `/redoc`, `/openapi.json`) SHALL существовать только при `DEBUG=true`; при `DEBUG=false`
эти пути MUST отвечать 404 (не схемой и не страницей SPA) (OQ-66). CORS при `DEBUG=true` SHALL разрешать любой источник
`*` без `allow_credentials`; при `DEBUG=false` — только источники из `CORS_ORIGINS` (JSON-список или строка через
запятую), пустой список SHALL запрещать кросс-доменные запросы, `allow_credentials` — только для явного списка без `*`
(OQ-65). Под `/api/v1` SPA MUST NOT отвечать: неверный метод на существующий маршрут SHALL давать 405 с заголовком
`Allow` и `detail` по-русски, несуществующий путь (в том числе dev-маршрут при выключенных флагах) — 404 (OQ-61);
`GET /` SHALL отдавать `index.html` SPA без редиректа при любом `ROOT_PATH` (OQ-62).
#### Scenario: Документация в проде
- **WHEN** `DEBUG=false` и запрошен `/openapi.json`
- **THEN** ответ 404
#### Scenario: Кросс-доменный запрос в проде без настроенных источников
- **WHEN** `DEBUG=false`, `CORS_ORIGINS` пуст, браузер шлёт preflight с `Origin: https://other.example`
- **THEN** в ответе нет `Access-Control-Allow-Origin`
#### Scenario: Неверный метод
- **WHEN** выполнен `POST /api/v1/me`
- **THEN** ответ 405, заголовок `Allow: GET`
#### Scenario: Несуществующий путь API
- **WHEN** выполнен `GET /api/v1/no-such-route`
- **THEN** ответ 404, не `index.html`
#### Scenario: Корень приложения
- **WHEN** при `ROOT_PATH=/` выполнен `GET /`
- **THEN** ответ 200 с `index.html`, без редиректа на `//`