Перейти к содержанию

Аудит завершённых действий организации

Когда участник спрашивает, кто изменил проект или пригласил Веру, технического журнала недостаточно. В нём могут быть сбои, повторы и диагностические детали, но не обязательно целостная история продукта. Для «Пространства» добавим аудит: записи о завершённых предметных действиях в конкретной организации.

Аудит содержит инициатора, действие, объект, момент и небольшой набор значимых данных. Он сохраняется в одной транзакции с изменением. API чтения использует тот же tenant-контекст и RLS, что проекты. Полный код AuditAsync в Database.cs и маршрут истории находятся в архиве. Получение событий при подготовке не выполнялось.

Событие подтверждает commit

Если Анна переименовала проект, история должна содержать project.renamed. Если UPDATE получил конфликт версии, такого события нет: имя не изменилось. Попытку можно зарегистрировать в диагностике, но она не должна выглядеть как завершённая правка в пользовательской истории.

Следующий листинг — полный SQL записи аудита. Поле data передаётся как JSON с явным типом jsonb. В архиве helper добавляет организацию и инициатора из текущего сеанса, поэтому обработчик не может незаметно взять их из JSON клиента.

INSERT INTO audit_events(organization_id,id,actor,action,entity_id,data)
VALUES (@organization,@id,@actor,@action,@entity,@data::jsonb);

После UPDATE вызывается INSERT аудита, затем общий commit. Если INSERT не удался, предметное изменение откатывается. Это делает гарантию истории проверяемой: завершённое изменение не существует без соответствующей записи. Если продукт допускает потерю отдельных событий ради доступности, это уже другой контракт, который нельзя обещать как полный аудит.

У событий есть случайный идентификатор и created_at базы. Время помогает отображению, но не всегда задаёт строгий глобальный порядок между всеми транзакциями. Два события могут иметь близкие либо одинаковые моменты, а транзакция с ранним временем завершиться позже. Для списка добавляется вторичный порядок по идентификатору; для строгой последовательности потребуется отдельная модель номера события.

Содержимое события выбирается по задаче

Для переименования достаточно старого и нового имени вместе с версиями. Для изменения роли — идентификатор участника и роли до и после. Для принятия приглашения — приглашение и результат членства. Не следует автоматически сериализовать весь DTO или весь объект базы в аудит: при появлении нового секретного свойства оно тогда начнёт копироваться без отдельного решения.

Особенно опасны токены приглашения и строки подключения. Аудиту не нужно уметь заново принять старую ссылку или открыть соединение. Поэтому эти значения вообще не записываются. Даже хеш токена обычно не требуется пользовательской истории; достаточно идентификатора приглашения. Меньший объём также упрощает сроки хранения и доступ владельца.

Похожее правило действует для персональных сведений. Иногда email нужен владельцу, чтобы различить приглашения, но идентификатор участника может быть достаточен для технического расследования. Наш пример сохраняет минимальные поля конкретного действия. Универсальное «сохраним всё на всякий случай» создаёт долгую обязанность защищать и удалять ненужные копии.

История тоже принадлежит компании

Чтение истории ограничивается organization_id. Если защитить проекты, но показать общий аудит, Борис увидит названия северных проектов и действия Анны. Такая утечка часто менее заметна, потому что страницу истории считают административной. Однако owner «Юга» не является администратором всего сервиса.

Для таблицы включается RLS и выдаются приложению только SELECT и INSERT. UPDATE и DELETE аудитa обычному HTTP-процессу не нужны. Это не делает историю криптографически неизменяемой: владелец базы всё ещё может её изменить. Зато случайный маршрут редактирования не сможет переписать старые записи через штатные права приложения.

У worker также есть право добавлять события своей ограниченной операции. Его actor записывается как system:export, а исходный инициатор сохраняется отдельным полем. Подставлять anna как будто Анна сама выполнила фоновое вычисление неточно. Различие помогает объяснить, почему действие завершилось позднее и какой компонент его выполнял.

Отдельная привилегированная процедура удаления компании может удалить историю согласно выбранному сроку. Это исключение относится к жизненному циклу, а не к обычному редактированию. Права worker и административной роли должны объяснять такую возможность явно. Иначе утверждение «аудит никогда нельзя удалить» разойдётся с фактической процедурой очищения данных.

Аудит и внешние уведомления

Запись аудита в базе не заменяет доставку события внешнему потребителю. Если после commit отправлять вебхук без сохранённой очереди, процесс может остановиться до отправки. Если отправлять перед commit, внешний потребитель узнает о действии, которое затем откатится. Для надёжной доставки нужна outbox-запись в общей транзакции и отдельная повторяемая доставка.

В нашем курсе внешняя доставка не реализуется, поэтому аудит не называется универсальной шиной событий. Он отвечает на вопросы истории внутри продукта. Следующий курс платежей будет использовать inbox для входящих уведомлений; сходство названий не означает, что все три таблицы имеют один контракт и одни права.

Для ручного изучения переименуйте проект с правильной версией, затем повторите старую версию. Ожидаемо появляется одна запись о переименовании и один ответ о конфликте. Затем прочитайте историю «Юга» Анной: северное событие там отсутствует, несмотря на то что тот же человек имеет северное членство. Область ответа определяется текущей компанией.

Когда событие помогает восстановить состояние, храните значения осознанно. Одного текста «проект изменён» недостаточно для объяснения версии; полной копии всех задач избыточно. Наше событие содержит ровно те поля, которые понадобятся владельцу для понимания действия. Это предметный выбор, а не автоматический эффект использования JSON.

Формат действия тоже развивается

При изменении JSON-состава события сохраняйте смысл уже записанных данных. Новый экран истории должен уметь показать старое переименование, где отсутствует дополнительное поле. Для крупных изменений можно ввести версию формата внутри data. Не переписывайте все прежние события только ради нового интерфейса: это изменит историческое основание. Словарь action полезно держать небольшим и предметным, чтобы запрос истории мог различать изменение роли и обычное переименование без разбора русского описания.

Теперь история связана с завершёнными транзакциями и своей компанией. Следующий урок разберёт выгрузку данных и доступ к её результату. Экспорт является ещё одной копией информации, поэтому его принадлежность, срок и право скачивания должны быть столь же явными, как у основной строки.

Оглавление серии