2026-10-08 18:11:56 +04:00

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

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_id NULL, 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