Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
114 lines
10 KiB
Markdown
114 lines
10 KiB
Markdown
# 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` нет
|
||
|
||
### Requirement: События файлов и документов
|
||
Загрузка, автозамена, удаление файла и подтверждение документа SHALL писать `audit_event` через сервис аудита:
|
||
|
||
| Действие | `action` | `section` | `object_type` | `user_id` |
|
||
|---|---|---|---|---|
|
||
| файл принят | `FILE_UPLOAD` | `PRIMARY_DOCUMENTS` | `stored_file` | инициатор |
|
||
| прежний файл заменён новым (автозамена после маппинга) | `FILE_REPLACE` | `PRIMARY_DOCUMENTS` | `stored_file` (заменённый) | NULL (система; инициатор загрузки — в `description`) |
|
||
| файл удалён | `FILE_DELETE` | `PRIMARY_DOCUMENTS` | `stored_file` | инициатор |
|
||
| документ подтверждён | `DOCUMENT_CONFIRM` | `PRIMARY_DOCUMENTS` | `document` | инициатор |
|
||
|
||
`FILE_REPLACE` SHALL содержать в `after_value` новый `file_id`, в `description` — перечень заменённых документов и
|
||
`confirmed=true`, если среди них был `CONFIRMED`. Смена статусов задания и файла в обмене и в цикле маппинга
|
||
`audit_event` не пишет (`ASSUMPTION(OQ-55)`).
|
||
|
||
#### Scenario: Загрузка двух файлов
|
||
- **WHEN** принято два файла одним запросом
|
||
- **THEN** два события `FILE_UPLOAD` с `object_id` каждого файла и `after_value` (`original_name`, `content_hash`, `job_id`)
|