From 8bc76c69a06fd1def82d133e47b5b8bb543475ad Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 5 Oct 2026 13:49:46 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20=D1=81=D0=BF=D0=B5=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=81=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D0=BE=D0=BC=20issue=20#9=20(=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=8C=D1=8E=20R-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- openspec/specs/audit-trail/spec.md | 40 +++++++++++++++++++++- openspec/specs/authn-authz/spec.md | 31 +++++++++++++++++ openspec/specs/reference-data-seed/spec.md | 30 ++++++++++++++-- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/openspec/specs/audit-trail/spec.md b/openspec/specs/audit-trail/spec.md index 74ab0e0..edc2db9 100644 --- a/openspec/specs/audit-trail/spec.md +++ b/openspec/specs/audit-trail/spec.md @@ -10,7 +10,8 @@ Сервис аудита SHALL предоставлять метод записи события с полями `action` (из enum `audit_action`), `section` (из `audit_section`), `object_type`, `object_id`, `user_id` (NULL для системных действий), `consolidation_version_id`, `description`, `before_value`, `after_value` (jsonb), `comment`, `result`, `changed_field`, `ip_address`, `user_agent`; -`occurred_at` SHALL проставляться сервером (UTC). Запись SHALL выполняться в той же транзакции, что и изменение данных. +`occurred_at` SHALL проставляться сервером (UTC). Запись SHALL выполняться в той же транзакции, что и изменение данных; +единственное исключение — события входа раздела `SESSION` (требование «События входа»). NOTE: В Срезе 0 обмен через recognition_job аудит не пишет (рулинг R-7, design #20); сервис готов к использованию Срезом 2+. @@ -44,6 +45,11 @@ ORM-модель `audit_event` MUST NOT использоваться для `UPD | пользователь создан/изменён/деактивирован | `USER_CHANGE` | `USERS` | `app_user` | | роли пользователя | `USER_ROLE_CHANGE` | `USERS` | `app_user` | | права роли | `ROLE_PERMISSION_CHANGE` | `ACCESS` | `role` | +| вход: выдача dev-токена, первый запрос с токеном | `LOGIN` | `SESSION` | `app_user` | +| отказ во входе (401 токена, 403 аутентификации) | `LOGIN_FAILED` | `SESSION` | `app_user` | + +Строки раздела `SESSION` подчиняются требованию «События входа» (отдельная транзакция), остальные — пишутся в той же +транзакции, что и мутация. `before_value`/`after_value` SHALL содержать изменённые поля (для списков — весь набор до и после); `changed_field` — имя поля при изменении одного поля. @@ -55,3 +61,35 @@ ORM-модель `audit_event` MUST NOT использоваться для `UPD #### Scenario: Откат мутации справочника - **WHEN** транзакция перевода ЮЛ откатывается - **THEN** строки `LEGAL_ENTITY_CHANGE` и `MEMBERSHIP_CHANGE` в `audit_event` отсутствуют + +### Requirement: События входа +Ядро SHALL писать через сервис аудита события входа раздела `SESSION` с `object_type = "app_user"`, +`object_id = user_id`, `ip_address` и `user_agent` запроса (OQ-64): +- `LOGIN` — при выдаче dev-токена (`POST /api/v1/dev/token`, в транзакции запроса) и при первом успешном запросе + пользователя с токеном после старта процесса (учёт — в памяти процесса; после рестарта первый запрос пишется снова); +- `LOGIN_FAILED` — при 401 (токен предъявлен, но не принят: `user_id = NULL`, в `description` причина, сам токен MUST NOT + попадать в журнал) и при 403 аутентификации (пользователь не зарегистрирован, отключён или неоднозначен: email в + `description`, `user_id` — если пользователь найден). + +Запрос без заголовка `Authorization` или со схемой не `Bearer` и 403 «недостаточно прав» событиями входа MUST NOT +считаться. `LOGIN_FAILED` и `LOGIN` первого запроса SHALL записываться в ОТДЕЛЬНОЙ короткой транзакции и фиксироваться, +даже если транзакция запроса откатывается: при 401/403 она откатывается всегда, а отказ во входе — то событие, ради +которого журнал ведётся. `user_agent` SHALL обрезаться до 512 символов, `description` — до 1000. Ошибка записи события +входа MUST NOT менять ответ запроса (только лог). `ip_address` достоверен только за доверенным прокси: +`X-Forwarded-For` принимается лишь от адресов `FORWARDED_ALLOW_IPS` (по умолчанию `127.0.0.1`). + +#### Scenario: Первый запрос пользователя +- **WHEN** пользователь трижды запрашивает `/api/v1/me` с валидным токеном после старта процесса +- **THEN** в `audit_event` одна строка `LOGIN` с его `user_id` и описанием «первый запрос с токеном» + +#### Scenario: Невалидный токен +- **WHEN** запрос несёт подделанный токен +- **THEN** ответ 401, в `audit_event` строка `LOGIN_FAILED` с `user_id = NULL` и причиной отказа без токена + +#### Scenario: Отказ переживает откат запроса +- **WHEN** запрос с токеном незарегистрированного пользователя получает 403, и транзакция запроса откатывается +- **THEN** строка `LOGIN_FAILED` с email в `description` зафиксирована в `audit_event` + +#### Scenario: Не попытка входа +- **WHEN** запрос пришёл без заголовка `Authorization` или пользователь получил 403 «недостаточно прав» +- **THEN** строки `LOGIN_FAILED` нет diff --git a/openspec/specs/authn-authz/spec.md b/openspec/specs/authn-authz/spec.md index 6d6c6ec..74b1267 100644 --- a/openspec/specs/authn-authz/spec.md +++ b/openspec/specs/authn-authz/spec.md @@ -12,6 +12,8 @@ настроенным проверяющим (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`, и такой активный пользователь есть @@ -64,3 +66,32 @@ claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_us #### 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`, без редиректа на `//` diff --git a/openspec/specs/reference-data-seed/spec.md b/openspec/specs/reference-data-seed/spec.md index d4b9d92..7699fbd 100644 --- a/openspec/specs/reference-data-seed/spec.md +++ b/openspec/specs/reference-data-seed/spec.md @@ -14,6 +14,12 @@ dev-данные загружаются из файлов репозитория `elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`. Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться. +Исключение — матрица `role_permission` (ей после первичной загрузки владеет администратор, issue #9): в ПУСТУЮ таблицу +матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право +вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право, +снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что +матрица пропущена (или сколько добавлено пар новых ролей и прав). + #### Scenario: Первый запуск на пустой схеме - **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed` - **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10, @@ -22,7 +28,21 @@ dev-данные загружаются из файлов репозитория #### Scenario: Повторный запуск - **WHEN** `python -m scripts.seed` выполняется второй раз без изменения файлов -- **THEN** число строк во всех затронутых таблицах не меняется, команда завершается с кодом 0 +- **THEN** число строк во всех затронутых таблицах не меняется, команда завершается с кодом 0, в отчёте + `role_permission` помечена как пропущенная + +#### Scenario: Право, снятое администратором +- **WHEN** администратор снял с роли `EXPERT` право `EX_VIEW`, после чего seed запущен снова +- **THEN** связь `EXPERT × EX_VIEW` не восстановлена, остальная матрица не изменилась + +#### Scenario: Новые роль и право в файлах seed +- **WHEN** матрица заполнена, а в `permissions.yaml` добавлено новое право, в `roles.yaml` — новая роль, и в + `role_permissions.yaml` — их пары (новое право существующей роли, существующее право новой роли) +- **THEN** seed вставляет ровно эти пары; снятые ранее администратором связи существующих ролей и прав не возвращаются + +#### Scenario: Администратор снял все связи +- **WHEN** таблица `role_permission` пуста и seed запущен снова +- **THEN** матрица файла загружается целиком, как при первом запуске #### Scenario: Изменение названия роли в файле - **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова @@ -47,7 +67,9 @@ dev-данные загружаются из файлов репозитория ### Requirement: Dev-данные только по явному флагу `python -m scripts.seed --dev` SHALL дополнительно загрузить из `api/seed/dev/`: примеры региональных филиалов и по одному `app_user` на каждую системную роль (`@armka.local`, активные, с назначенной ролью через `user_role`). Без флага -`--dev` эти данные MUST NOT загружаться. +`--dev` эти данные MUST NOT загружаться. Флаг `--dev` SHALL приниматься только при `DEBUG=true`: при `DEBUG=false` +команда MUST завершиться с кодом 2 и сообщением «--dev доступен только при DEBUG=true (dev-пользователи не для прода)» +до чтения файлов seed и обращения к БД (OQ-60). #### Scenario: Dev-пользователи - **WHEN** выполнен `python -m scripts.seed --dev` @@ -56,3 +78,7 @@ dev-данные загружаются из файлов репозитория #### Scenario: Прод-режим - **WHEN** выполнен `python -m scripts.seed` без `--dev` - **THEN** таблица `app_user` пуста, `regional_office` содержит только Головной офис + +#### Scenario: `--dev` без DEBUG +- **WHEN** при `DEBUG=false` выполнен `python -m scripts.seed --dev` +- **THEN** код возврата 2, в stderr — сообщение об отказе, БД не затронута