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

8.6 KiB
Raw Blame History

audit-trail Specification

Purpose

Все действия из enum audit_action пишутся в audit_event одним способом — через сервис аудита; журнал только дополняется, записи неизменяемы (ТЗ 8.2).

Requirements

Requirement: Единая точка записи

Сервис аудита 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 выполняться в той же транзакции, что и изменение данных; единственное исключение — события входа раздела SESSION (требование «События входа»).

NOTE: В Срезе 0 обмен через recognition_job аудит не пишет (рулинг R-7, design #20); сервис готов к использованию Срезом 2+.

Scenario: Системное событие

  • WHEN системный процесс (без пользователя) вызывает сервис аудита
  • THEN в audit_event появляется строка с user_id = NULL, before_value/after_value содержат старый и новый статус

Scenario: Откат транзакции

  • WHEN изменение данных и запись аудита выполняются в одной транзакции, и она откатывается
  • THEN строка audit_event не сохраняется

Requirement: Журнал только дополняется

ORM-модель audit_event MUST NOT использоваться для UPDATE или DELETE в коде ядра; тест SHALL проверять, что в src/ нет обращений, изменяющих или удаляющих audit_event, а сервис аудита не имеет методов изменения.

Scenario: Попытка изменить запись

  • WHEN код вызывает обновление строки audit_event через сервис аудита
  • THEN такого метода нет (ошибка на этапе типизации/теста), запись остаётся прежней

Requirement: События справочников, импорта и администрирования

Каждая мутация справочников, импорт и каждое действие администратора SHALL писать audit_event через сервис аудита в той же транзакции с user_id инициатора, ip_address и user_agent запроса:

Действие action section object_type
создание/изменение/архив ГК GROUP_CHANGE DIRECTORIES company_group
смена филиала ГК ASSIGNMENT_CHANGE DIRECTORIES company_group
создание/изменение/перевод/закрытие ЮЛ, алиасы LEGAL_ENTITY_CHANGE DIRECTORIES legal_entity / legal_entity_alias
ГСК/ГСЗ и их состав MEMBERSHIP_CHANGE DIRECTORIES related_group / related_group_member
импорт выгрузки DIRECTORY_IMPORT DIRECTORIES directory_import
пользователь создан/изменён/деактивирован 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 — имя поля при изменении одного поля.

Scenario: Изменение имени ГК

  • WHEN имя ГК меняется с «А» на «Б»
  • THEN в audit_event строка GROUP_CHANGE с changed_field = "name", before_value = {"name": "А"}, after_value = {"name": "Б"}, user_id инициатора

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 нет