- 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
98 lines
7.6 KiB
Markdown
98 lines
7.6 KiB
Markdown
# 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`, без редиректа на `//`
|