feat(audit): route every LogAudit call through a best-effort WriteAudit helper

Audit writes stay best-effort — a LogAudit failure must never fail or abort
the request — but a failed write must no longer be silently discarded. Add
db.WriteAudit(auditor, actor, action, targetType, targetID, detail), which
logs a failed write with actor/action/target context (never the detail
string, which may be sensitive) and never propagates the error.

The Auditor interface is satisfied structurally by both *db.DB and the
service-layer Store, so api/admin/ws/service all reach the helper without an
import cycle. Converts all ~26 call sites from `_ = LogAudit(...)` (and the
two backup handlers' inline `if err` blocks) to db.WriteAudit. Pinned by
db/audit_test.go: failure logged and not propagated, success logs nothing,
detail never leaks.

Resolves the repo-wide LogAudit policy question flagged by the D8 note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-20 10:48:05 +02:00
co-authored by Claude Fable 5
parent 5de92b9510
commit b60bc8d04b
14 changed files with 139 additions and 35 deletions
+3 -7
View File
@@ -52,10 +52,8 @@ func handleBackup(database *db.DB) http.Handler {
actor := actorFromContext(r)
backupName := filepath.Base(backupPath)
slog.Info("database backup created", "actor_id", actor, "name", backupName)
if err := database.LogAudit(actor, "backup_create", "server", 0,
fmt.Sprintf("backup saved: %s", backupName)); err != nil {
slog.Error("audit log write failed", "action", "backup_create", "actor_id", actor, "error", err)
}
db.WriteAudit(database, actor, "backup_create", "server", 0,
fmt.Sprintf("backup saved: %s", backupName))
writeJSON(w, http.StatusOK, map[string]string{
"path": filepath.Base(backupPath),
@@ -138,9 +136,7 @@ func handleDeleteBackup(database *db.DB) http.Handler {
actor := actorFromContext(r)
slog.Info("backup deleted", "actor_id", actor, "name", name)
if err := database.LogAudit(actor, "backup_delete", "server", 0, "deleted backup "+name); err != nil {
slog.Error("audit log write failed", "action", "backup_delete", "actor_id", actor, "error", err)
}
db.WriteAudit(database, actor, "backup_delete", "server", 0, "deleted backup "+name)
w.WriteHeader(http.StatusNoContent)
})
+2 -2
View File
@@ -108,7 +108,7 @@ func handlePutChannelPermission(database *db.DB, hub HubBroadcaster, permInvalid
actor := actorFromContext(r)
slog.Info("channel permissions updated", "actor_id", actor, "channel_id", ch.ID,
"role_id", roleID, "allow", allow, "deny", deny)
_ = database.LogAudit(actor, "channel_perms_update", "channel", ch.ID,
db.WriteAudit(database, actor, "channel_perms_update", "channel", ch.ID,
fmt.Sprintf("set overrides for role %s on #%s (allow=%#x deny=%#x)", role.Name, ch.Name, allow, deny))
if permInvalidator != nil {
@@ -147,7 +147,7 @@ func handleDeleteChannelPermission(database *db.DB, hub HubBroadcaster, permInva
actor := actorFromContext(r)
slog.Info("channel permissions cleared", "actor_id", actor, "channel_id", ch.ID, "role_id", roleID)
_ = database.LogAudit(actor, "channel_perms_clear", "channel", ch.ID,
db.WriteAudit(database, actor, "channel_perms_clear", "channel", ch.ID,
fmt.Sprintf("cleared overrides for role %d on #%s", roleID, ch.Name))
if permInvalidator != nil {
+3 -3
View File
@@ -115,7 +115,7 @@ func handleCreateChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
}
actor := actorFromContext(r)
slog.Info("channel created", "actor_id", actor, "channel", req.Name, "type", req.Type)
_ = database.LogAudit(actor, "channel_create", "channel", id,
db.WriteAudit(database, actor, "channel_create", "channel", id,
fmt.Sprintf("created #%s (%s)", req.Name, req.Type))
if hub != nil {
hub.BroadcastChannelCreate(ch)
@@ -171,7 +171,7 @@ func handlePatchChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
actor := actorFromContext(r)
slog.Info("channel updated", "actor_id", actor, "channel_id", id, "name", req.Name)
_ = database.LogAudit(actor, "channel_update", "channel", id,
db.WriteAudit(database, actor, "channel_update", "channel", id,
fmt.Sprintf("updated #%s", req.Name))
updated, err := database.GetChannel(id)
@@ -210,7 +210,7 @@ func handleDeleteChannel(database *db.DB, hub HubBroadcaster) http.HandlerFunc {
}
actor := actorFromContext(r)
slog.Warn("channel deleted", "actor_id", actor, "channel_id", id, "name", existing.Name)
_ = database.LogAudit(actor, "channel_delete", "channel", id,
db.WriteAudit(database, actor, "channel_delete", "channel", id,
fmt.Sprintf("deleted #%s", existing.Name))
if hub != nil {
hub.BroadcastChannelDelete(id)
+1 -1
View File
@@ -79,7 +79,7 @@ func handlePatchSettings(database *db.DB) http.HandlerFunc {
}
for key := range normalizedUpdates {
slog.Info("setting changed", "actor_id", actor, "key", key)
_ = database.LogAudit(actor, "setting_change", "setting", 0,
db.WriteAudit(database, actor, "setting_change", "setting", 0,
fmt.Sprintf("%s updated", key))
}
+2 -2
View File
@@ -139,7 +139,7 @@ func handlePatchUser(database *db.DB, hub HubBroadcaster, permInvalidator Permis
if permInvalidator != nil {
permInvalidator.InvalidateUser(id)
}
_ = database.LogAudit(actor, "role_change", "user", id,
db.WriteAudit(database, actor, "role_change", "user", id,
fmt.Sprintf("changed %s role to %d", user.Username, *req.RoleID))
if role, err := database.GetRoleByID(*req.RoleID); err == nil && role != nil {
if hub != nil {
@@ -171,7 +171,7 @@ func handleForceLogout(database *db.DB) http.HandlerFunc {
}
actor := actorFromContext(r)
slog.Info("force logout", "actor_id", actor, "target_user_id", id)
_ = database.LogAudit(actor, "force_logout", "user", id, "all sessions terminated")
db.WriteAudit(database, actor, "force_logout", "user", id, "all sessions terminated")
w.WriteHeader(http.StatusNoContent)
}
}
+1 -1
View File
@@ -152,7 +152,7 @@ func handleSetup(database *db.DB, limiter *auth.RateLimiter, allowedOrigins []st
}
slog.Info("server setup completed", "owner", req.Username, "user_id", uid)
_ = database.LogAudit(uid, "server_setup", "server", 0,
db.WriteAudit(database, uid, "server_setup", "server", 0,
"initial setup: owner account created, default channel and invite generated")
writeJSON(w, http.StatusCreated, setupResponse{