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

7.6 KiB
Raw Blame History

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, без редиректа на //