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
This commit is contained in:
parent
7b28554817
commit
8bc76c69a0
@ -10,7 +10,8 @@
|
|||||||
Сервис аудита SHALL предоставлять метод записи события с полями `action` (из enum `audit_action`), `section`
|
Сервис аудита SHALL предоставлять метод записи события с полями `action` (из enum `audit_action`), `section`
|
||||||
(из `audit_section`), `object_type`, `object_id`, `user_id` (NULL для системных действий), `consolidation_version_id`,
|
(из `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`;
|
`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+.
|
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_CHANGE` | `USERS` | `app_user` |
|
||||||
| роли пользователя | `USER_ROLE_CHANGE` | `USERS` | `app_user` |
|
| роли пользователя | `USER_ROLE_CHANGE` | `USERS` | `app_user` |
|
||||||
| права роли | `ROLE_PERMISSION_CHANGE` | `ACCESS` | `role` |
|
| права роли | `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` —
|
`before_value`/`after_value` SHALL содержать изменённые поля (для списков — весь набор до и после); `changed_field` —
|
||||||
имя поля при изменении одного поля.
|
имя поля при изменении одного поля.
|
||||||
@ -55,3 +61,35 @@ ORM-модель `audit_event` MUST NOT использоваться для `UPD
|
|||||||
#### Scenario: Откат мутации справочника
|
#### Scenario: Откат мутации справочника
|
||||||
- **WHEN** транзакция перевода ЮЛ откатывается
|
- **WHEN** транзакция перевода ЮЛ откатывается
|
||||||
- **THEN** строки `LEGAL_ENTITY_CHANGE` и `MEMBERSHIP_CHANGE` в `audit_event` отсутствуют
|
- **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` нет
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
настроенным проверяющим (dev HS256 при `AUTH_DEV_ISSUER=true`, иначе JWKS/публичный ключ по `AUTH_*`), извлечь email из
|
настроенным проверяющим (dev HS256 при `AUTH_DEV_ISSUER=true`, иначе JWKS/публичный ключ по `AUTH_*`), извлечь email из
|
||||||
claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_user` с этим `email` и `is_active = true`. Нет или невалидный
|
claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_user` с этим `email` и `is_active = true`. Нет или невалидный
|
||||||
токен → 401; пользователь не найден или неактивен → 403. Тело ошибки — `{"detail": "<причина по-русски>"}`.
|
токен → 401; пользователь не найден или неактивен → 403. Тело ошибки — `{"detail": "<причина по-русски>"}`.
|
||||||
|
Отказы в аутентификации (401 при предъявленном токене, 403 «не найден / отключён / неоднозначен») и вход SHALL
|
||||||
|
фиксироваться в журнале по требованию «События входа» спеки `audit-trail` (`LOGIN_FAILED` / `LOGIN`).
|
||||||
|
|
||||||
#### Scenario: Валидный токен активного пользователя
|
#### Scenario: Валидный токен активного пользователя
|
||||||
- **WHEN** запрос несёт корректный токен с `email = ka_rf@armka.local`, и такой активный пользователь есть
|
- **WHEN** запрос несёт корректный токен с `email = ka_rf@armka.local`, и такой активный пользователь есть
|
||||||
@ -64,3 +66,32 @@ claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_us
|
|||||||
#### Scenario: Прод-конфигурация
|
#### Scenario: Прод-конфигурация
|
||||||
- **WHEN** `DEBUG=false`
|
- **WHEN** `DEBUG=false`
|
||||||
- **THEN** `POST /api/v1/dev/token` возвращает 404, токен, подписанный dev-секретом, отвергается с 401
|
- **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`, без редиректа на `//`
|
||||||
|
|||||||
@ -14,6 +14,12 @@ dev-данные загружаются из файлов репозитория
|
|||||||
`elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`.
|
`elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`.
|
||||||
Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться.
|
Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться.
|
||||||
|
|
||||||
|
Исключение — матрица `role_permission` (ей после первичной загрузки владеет администратор, issue #9): в ПУСТУЮ таблицу
|
||||||
|
матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право
|
||||||
|
вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право,
|
||||||
|
снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что
|
||||||
|
матрица пропущена (или сколько добавлено пар новых ролей и прав).
|
||||||
|
|
||||||
#### Scenario: Первый запуск на пустой схеме
|
#### Scenario: Первый запуск на пустой схеме
|
||||||
- **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed`
|
- **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed`
|
||||||
- **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10,
|
- **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10,
|
||||||
@ -22,7 +28,21 @@ dev-данные загружаются из файлов репозитория
|
|||||||
|
|
||||||
#### Scenario: Повторный запуск
|
#### Scenario: Повторный запуск
|
||||||
- **WHEN** `python -m scripts.seed` выполняется второй раз без изменения файлов
|
- **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: Изменение названия роли в файле
|
#### Scenario: Изменение названия роли в файле
|
||||||
- **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова
|
- **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова
|
||||||
@ -47,7 +67,9 @@ dev-данные загружаются из файлов репозитория
|
|||||||
### Requirement: Dev-данные только по явному флагу
|
### Requirement: Dev-данные только по явному флагу
|
||||||
`python -m scripts.seed --dev` SHALL дополнительно загрузить из `api/seed/dev/`: примеры региональных филиалов и по одному
|
`python -m scripts.seed --dev` SHALL дополнительно загрузить из `api/seed/dev/`: примеры региональных филиалов и по одному
|
||||||
`app_user` на каждую системную роль (`<role>@armka.local`, активные, с назначенной ролью через `user_role`). Без флага
|
`app_user` на каждую системную роль (`<role>@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-пользователи
|
#### Scenario: Dev-пользователи
|
||||||
- **WHEN** выполнен `python -m scripts.seed --dev`
|
- **WHEN** выполнен `python -m scripts.seed --dev`
|
||||||
@ -56,3 +78,7 @@ dev-данные загружаются из файлов репозитория
|
|||||||
#### Scenario: Прод-режим
|
#### Scenario: Прод-режим
|
||||||
- **WHEN** выполнен `python -m scripts.seed` без `--dev`
|
- **WHEN** выполнен `python -m scripts.seed` без `--dev`
|
||||||
- **THEN** таблица `app_user` пуста, `regional_office` содержит только Головной офис
|
- **THEN** таблица `app_user` пуста, `regional_office` содержит только Головной офис
|
||||||
|
|
||||||
|
#### Scenario: `--dev` без DEBUG
|
||||||
|
- **WHEN** при `DEBUG=false` выполнен `python -m scripts.seed --dev`
|
||||||
|
- **THEN** код возврата 2, в stderr — сообщение об отказе, БД не затронута
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user