20 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 |
инициатор |
| документ подтверждён автоматически при маппинге (чистый) | DOCUMENT_CONFIRM |
PRIMARY_DOCUMENTS |
document |
NULL (система; description = «автоподтверждение: без замечаний») |
FILE_REPLACE SHALL содержать в after_value новый file_id, в description — перечень заменённых документов и
confirmed=true, если среди них был CONFIRMED. FILE_DELETE SHALL содержать в after_value.unbound_version_ids
версии, где сняты привязки удалённых автоподтверждённых документов. Смена статусов задания и файла в обмене и в цикле
маппинга audit_event не пишет (ASSUMPTION(OQ-55)).
Scenario: Загрузка двух файлов
- WHEN принято два файла одним запросом
- THEN два события
FILE_UPLOADсobject_idкаждого файла иafter_value(original_name,content_hash,job_id)
Scenario: Автоподтверждение
- WHEN смаплен чистый результат ОСВ
- THEN одно событие
DOCUMENT_CONFIRMсobject_idдокумента,user_idNULL,before_value.status = RECOGNIZED,after_value.status = CONFIRMED,description = «автоподтверждение: без замечаний»
Requirement: События консолидации
Действия с консолидацией SHALL писать audit_event через сервис аудита с заполненным consolidation_version_id:
| Действие | action |
section |
object_type |
before_value / after_value |
|---|---|---|---|---|
| консолидация создана | CONSOLIDATION_CREATE |
CONSOLIDATION |
consolidation_version |
— / {consolidation_id, year, quarter, members} |
| периметр изменён | MEMBERSHIP_CHANGE |
CONSOLIDATION |
consolidation_version |
списки ЮЛ до / после |
| взята в работу, снята | CONSOLIDATION_LOCK |
CONSOLIDATION |
consolidation_version |
— / {locked_by, released} |
| перехват | CONSOLIDATION_LOCK_TAKEOVER |
CONSOLIDATION |
consolidation_version |
{locked_by: прежний} / {locked_by: новый}, comment = причина |
| смена статуса (ручная и автоматическая) | CONSOLIDATION_STATUS_CHANGE |
CONSOLIDATION |
consolidation_version |
{status} / {status}, comment |
| контрагент сопоставлен вручную | OSV_COUNTERPARTY_MAP |
CONSOLIDATION |
osv_entry |
{counterparty_id, match_kind} / {counterparty_id, match_kind, propagated_entry_ids, alias_created, alias_moved_from} |
| экспорт свода | EXPORT |
EXPORT |
consolidation_version |
— / {report: "consolidated_osv", filters, rows} |
Автоматические переходы статуса SHALL писаться с user_id инициатора события (загрузивший, подтвердивший,
stored_file.uploaded_by). Создание и удаление алиасов при ручном сопоставлении SHALL дополнительно писать
LEGAL_ENTITY_CHANGE (раздел DIRECTORIES, как в Срезе 1). FILE_REPLACE SHALL добавлять в after_value
unbound_version_ids.
Scenario: Перехват
- WHEN Б перехватывает блокировку у А с причиной «Отпуск»
- THEN одно событие
CONSOLIDATION_LOCK_TAKEOVERсbefore_value.locked_by = A,after_value.locked_by = B,comment = "Отпуск",consolidation_version_idверсии
Scenario: Автоматический переход
- WHEN подтверждение документа перевело версию в
DATA_CORRECTION - THEN событие
CONSOLIDATION_STATUS_CHANGEсuser_idподтвердившего,before_value.status = INITIATED
Requirement: Аудит экспорта шахматки
Каждый успешный GET …/intercompany-matrix/all/export, POST …/intercompany-matrix/manual/export и
GET …/intercompany-matrix/preset/{rule_id}/export
SHALL добавлять отдельное неизменяемое событие audit_event с action = EXPORT, section = EXPORT,
object_type = consolidation_version, object_id = consolidation_version_id, user_id инициатора,
before_value = null и after_value = {report: "intercompany_matrix", mode, debit_account_ids, credit_account_ids, cells}; для PRESET в after_value SHALL быть дополнительно rule_id, rule_no и
rule_version_no (фактическая версия применённого правила, в том числе при незафиксированной версии правил). mode — ALL,
MANUAL или PRESET, массивы счетов — ID из запроса (в ALL и PRESET пустые), cells — число ячеек. Выгрузка
сводной ОСВ продолжает писать собственное событие EXPORT.
Scenario: Две выгрузки
- WHEN пользователь дважды выгружает ту же шахматку
- THEN в
audit_eventдве разные записиEXPORT, каждая со своим payload и числом ячеек
Scenario: Выгрузка пресета
- WHEN пользователь выгружает
GET …/intercompany-matrix/preset/{rule_id}/export - THEN событие
EXPORTсодержитmode = PRESET,rule_id,rule_no,rule_version_no, пустые массивы счетов и число ячеек
Requirement: Аудит экспорта результирующей таблицы ВГО
Каждый успешный GET …/intercompany-matrix/result-table/export SHALL добавлять отдельное неизменяемое событие
audit_event с action = EXPORT, section = EXPORT, object_type = consolidation_version,
object_id = consolidation_version_id, consolidation_version_id версии, user_id инициатора, before_value = null
и after_value = {report: "intercompany_result_table", articles}, где articles — число статей в книге. Неуспешная
выгрузка (404, 422, ошибка сборки книги) события не пишет.
Scenario: Выгрузка таблицы
- WHEN пользователь выгружает результирующую таблицу версии
- THEN в
audit_eventодна записьEXPORTсafter_value = {report: "intercompany_result_table", articles: 5}
Scenario: Нет счёта статьи
- WHEN выгрузка отвечает 422
RESULT_ACCOUNTS_MISSING - THEN событие
EXPORTне записано
Requirement: Аудит ручной повторной обработки файла
Каждая принятая ручная попытка SHALL писать одно audit_event с action = FILE_REPROCESS, section = PRIMARY_DOCUMENTS, object_type = stored_file, object_id = file_id, user_id инициатора, IP и User-Agent запроса. before_value SHALL содержать прежние job_id, attempt, status задания, error_code; after_value — новые job_id, attempt, а при пакетном повторе также consolidation_id. Событие SHALL фиксироваться в транзакции создания попытки.
Scenario: Пакетный повтор
- WHEN пакет принял два файла ОСВ РБ и пропустил третий
- THEN два FILE_REPROCESS с разными object_id и прежними/новыми job_id, с consolidation_id в after_value; before_value.status — статус прежнего задания (timeout или dlq), error_code — TIMEOUT или NO_ACK; для skipped события нет
Scenario: Отказ или откат
- WHEN повтор отклонён либо транзакция создания задания откатывается
- THEN FILE_REPROCESS для него отсутствует
Scenario: Автоматическая повторная публикация
- WHEN outbox повторно публикует прежнее задание без ack, не создавая новую попытку
- THEN FILE_REPROCESS не пишется
Requirement: Аудит уточнения шапки ОСВ
Фактический PATCH шапки ОСВ SHALL писать одно RECOGNIZED_DATA_EDIT раздела RECOGNITION, object_type = document, object_id документа, user_id инициатора. before/after SHALL содержать изменённые поля и при сбросе подтверждения status, confirmed_by/at; after_value.unbound_version_ids SHALL содержать версии снятых привязок. Запрос без изменений MUST NOT писать аудит.
Scenario: Подтверждённая ОСВ исправлена
- WHEN меняется счёт CONFIRMED ОСВ РБ, привязанной к версии DATA_CORRECTION
- THEN одно RECOGNIZED_DATA_EDIT с прежним/новым счётом и подтверждением, after_value содержит unbound_version_ids, пользователь, IP и User-Agent сохранены
Scenario: Возврат к исходному
- WHEN аналитик возвращает исходные значения после предыдущей правки
- THEN новое RECOGNIZED_DATA_EDIT отражает эту правку; старые события не меняются