Журнал аудита
Коротко: Непрерывная запись значимых для безопасности и администрирования действий (кто что и когда сделал и с кем) — служит последующей прослеживаемости, например при назначении ролей или других вмешательствах администратора.
Подробнее: Журнал аудита нужно писать намеренно отказоустойчиво — ошибка журналирования никогда не должна блокировать или обрушивать само действие. Типичная структура: кто действовал (актор), что сделано (действие), над каким объектом (цель), плюс необязательные структурированные детали.
Наш контекст: app/utils/auditLog.ts в Emzett, виден в панели администратора в разделе «Журнал аудита» (о контроле доступа к нему см. Динамическая ролевая система (гранулярный RBAC)). Запись защищена try/catch, чтобы ошибка БД при журналировании не помешала, скажем, назначению роли.
Подробности
Журнал аудита отличается от обычного журналирования приложения (вывод ошибок/отладки, который в какой-то момент ротируется/удаляется) назначением и требованиями: он должен быть полным, постоянным и защищённым от подделки, потому что при сомнении служит доказательством — кто когда менял какое право, кто получал доступ к каким данным. Поэтому журнал аудита обычно оказывается в отдельной, как правило, только добавляющей таблице (записи никогда не меняются и не удаляются, только добавляются), часто со стро́же ограниченными правами доступа, чем остальная база данных — в идеале даже администратор не может позже удалить отдельные записи, чтобы это в свою очередь не было записано.
Структура записи
Типичная запись журнала аудита следует схеме актор–действие–цель:
- Актор — кто действовал (ID пользователя, но системные процессы, например cron-задача, тоже считаются акторами)
- Действие — что сделано, обычно как описательная строка (
role.grant,user.delete,settings.update) - Цель — над каким объектом (ID затронутой записи)
- Детали — необязательная структурированная дополнительная информация (например, старое и новое значение при изменении)
- Метка времени и часто также IP-адрес актора или ID сессии, чтобы инциденты можно было позже отнести к конкретной сессии
Для критичных для безопасности изменений (например, смены прав) стоит хранить и старое, и новое значение ({ from: "user", to: "admin" }), а не только новое — иначе потом нельзя восстановить, каким было прежнее состояние.
Отказоустойчивость как основной принцип
Принцип «писать отказоустойчиво» решающий: запись в журнал аудита — побочный эффект самого действия, а не оно само. Если бы ошибка журналирования блокировала основное действие, сломанная система журналирования могла бы случайно парализовать всё приложение — классический пример «контроль не должен стать важнее контролируемого». В то же время неудавшаяся запись в журнал аудита не должна просто молча пропасть — принято хотя бы записать ошибку в обычное журналирование ошибок (например, Sentry), чтобы пробелы в журнале аудита замечались, даже если они не блокируют основное действие.
async function grantRole(userId: string, role: string) {
await db.update(users).set({ role }).where(eq(users.id, userId))
try {
await logAudit({ actor: currentUser.id, action: "role.grant", target: userId, details: { role } })
} catch (err) {
console.error("Audit log failed:", err) // назначение роли всё равно прошло
}
}Отличие от родственных понятий
Журнал аудита часто путают с журналом событий или лентой активности, но он служит другой цели: лента активности (например, «X прокомментировал Y») предназначена для пользователей и вполне может быть неполной или сокращённой; журнал аудита предназначен для прослеживаемости/соответствия и должен быть полным. Журнал изменений базы данных (например, через расширение Postgres pg_audit или таблицы истории на триггерах) автоматически фиксирует КАЖДОЕ изменение на уровне базы данных независимо от логики приложения — это полнее, но и значительно шумнее, потому что не даёт семантической классификации вроде «это было назначение роли», только сырые изменения строк.
С юридической точки зрения журнал аудита важен и в контексте GDPR: он документирует, кто имел доступ к персональным данным, что может быть решающим при запросе доступа или утечке данных (ст. 33 GDPR, обязанность сообщить в течение 72 часов), чтобы вообще суметь очертить масштаб инцидента.
См. также: Динамическая ролевая система (гранулярный RBAC), Протокол, GDPR