Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
10 KiB
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)