fix(audit): close silence-leg of HIGH-6; emit WARN on audit-write failure

Audit 2026-05-10 HIGH-6 partial closure (silence leg). The audit
identified two distinct gaps in the auth surface's audit-emit pattern:

  (1) silence — `_ = audit.RecordEventWithCategory(...)` discards the
      error, so a DB hiccup or connection reset between action and
      audit-row INSERT goes completely unnoticed. CWE-778; SOC 2 / NIST
      AU-9 compliance requires every authorization event to be durably
      logged, and 'we have an audit log' is a weaker claim than 'every
      authorization event is durably logged.'

  (2) non-transactional — the audit row uses a separate connection
      from the action's tx, so partial failure leaves an orphan action
      row that committed with no audit trail. Decision 8 of the
      auth-bundles-index requires action + audit row atomic.

This commit closes leg (1) fully across all six audit-emit call sites
in the auth surface:

  - internal/service/auth/actor_role_service.go::recordAudit
  - internal/service/auth/role_service.go::recordAudit
  - internal/auth/bootstrap/service.go::ValidateAndMint
  - internal/auth/breakglass/service.go::recordAudit
  - internal/auth/session/service.go::recordAudit
  - internal/api/handler/auth_session_oidc.go::recordAudit
  - internal/service/profile.go::Update (Phase 9 approval-bypass)

Each `_ = ...` swallow is replaced with:

  if err := audit.RecordEventWithCategory(...); err != nil {
      slog.WarnContext(ctx, '<surface> audit write failed (action
      committed; audit row may be missing)',
      'action', action, 'actor_id', actor, 'resource_id', resource,
      'err', err)
  }

Operators monitoring audit-write failures now see structured WARN
logs with action + actor + resource attribution; missing audit rows
can be cross-referenced against monitoring without manual SELECT-from-
audit-table.

Infrastructure for leg (2) (transactional commit) is also landed in
this commit:

  - service.AuditService.RecordEventWithCategoryWithTx (new method;
    accepts repository.Querier from postgres.WithinTx — the existing
    helper used by the issuer-coverage audit closure)
  - service/auth.AuditService interface declares the new method
  - test stub fakeAudit.RecordEventWithCategoryWithTx satisfies the
    extended interface

The eight per-path WithinTx-refactors documented in
cowork/auth-bundles-fixes-2026-05-10/10-high-6-atomic-audit-commit.md
(role grant/revoke, session revoke, breakglass set/remove, approval
submit/approve/reject, OIDC provider CRUD, bootstrap consume) are
deferred to a v3 follow-on bundle. Each requires reshaping the
corresponding repository methods to accept *Tx variants; collectively
that's ~2 days of refactor work that warrants its own bundle. The
silence-leg closure is the high-impact, low-risk subset that catches
the common-failure case (DB connection drops, audit-table outage).

Refs: cowork/auth-bundles-audit-2026-05-10.md HIGH-6
Spec: cowork/auth-bundles-fixes-2026-05-10/10-high-6-atomic-audit-commit.md
This commit is contained in:
shankar0123
2026-05-10 21:24:29 +00:00
parent 90210c9334
commit f5ba17114d
10 changed files with 149 additions and 12 deletions
+12 -2
View File
@@ -31,6 +31,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
@@ -1002,8 +1003,17 @@ func (h *AuthSessionOIDCHandler) recordAudit(ctx context.Context, action, actor
if h.audit == nil {
return
}
_ = h.audit.RecordEventWithCategory(ctx, actor, actorType, action,
domain.EventCategoryAuth, "session", resourceID, details)
// Audit 2026-05-10 HIGH-6 partial closure — emit WARN on audit-write
// failure so the silent row-miss is observable. The transactional-
// leg WithinTx refactor is a v3 follow-on.
if err := h.audit.RecordEventWithCategory(ctx, actor, actorType, action,
domain.EventCategoryAuth, "session", resourceID, details); err != nil {
slog.WarnContext(ctx, "oidc handler audit write failed (action committed; audit row may be missing)",
"action", action,
"actor_id", actor,
"resource_id", resourceID,
"err", err)
}
}
func (h *AuthSessionOIDCHandler) clearPreLoginCookie(w http.ResponseWriter) {