Commit graph

2 commits

Author SHA1 Message Date
411f49169b feat(server): tee access log to a rotated file for on-disk audit trail
Add --access-log <path> (env ZDDC_ACCESS_LOG). When set, every access-
log record is written as a JSON line to the configured file in
addition to the existing slog.Default() stderr output. Empty (default)
keeps the prior behavior — stderr only.

Rotation via gopkg.in/natefinch/lumberjack.v2:
  100 MB per file, 10 backups, 90-day max age, gzip rotated files.

Operator usage (e.g. behind a Caddy/quadlet stack):
  zddc-server --access-log /srv/.zddc.d/logs/access.log ...

Architecture:
  AccessLogMiddleware now takes an optional *slog.Logger. main.go wires
  it via setupAccessAuditLog() which builds a slog.JSONHandler over a
  lumberjack rotator. Stderr emission stays via slog.Default(); the
  audit logger gets the same fields in line-delimited JSON, the format
  every standard log shipper (Vector, Loki, fluentbit, journalbeat)
  parses natively.

Tests cover the audit logger receiving the same email/path/status
fields as the stderr stream.
2026-05-04 07:49:17 -05:00
fedc3650b5 fix(zddc-server): access log was always email=anonymous (middleware ordering)
The middleware chain in main.go was:

  AccessLogMiddleware ( CORSMiddleware ( ACLMiddleware ( dispatch ) ) )

ACLMiddleware extracts the user email from the configured header and
stores it in the request context via r.WithContext. But Go's context
propagates DOWN the chain (to handlers further in) — not back UP. The
new context-bearing request only exists inside the call to
next.ServeHTTP; once that returns, the outer middleware still has the
ORIGINAL request without the email. So AccessLogMiddleware's
EmailFromContext(r) call (which runs after next.ServeHTTP returns to
log the request) read from the original context and got an empty
string, falling through to "anonymous".

The /.profile/ page worked correctly because it reads the email
directly inside the handler — at that depth the context-bearing
request is the one in scope.

Fix: invert the chain so ACL is OUTERMOST.

  ACLMiddleware ( AccessLogMiddleware ( CORSMiddleware ( dispatch ) ) )

Now ACL extracts the email and the new request flows down through
AccessLog (which sees the email-bearing context), CORS, and dispatch.

Add three regression tests in middleware_test.go:

  TestAccessLogReadsEmailFromACLContext
    The fix: with ACL outer, AccessLog logs email=alice@example.com
    when X-Auth-Request-Email is set.

  TestAccessLogAnonymousWhenNoEmail
    The unchanged path: no header → email=anonymous (correct fallback).

  TestAccessLogOuterDoesNotSeeInnerContext
    Locks down Go's actual context-propagation behavior. Builds the
    INVERTED (buggy) chain and asserts that AccessLog (outer) does NOT
    see the email ACL (inner) set. If this ever fails, Go's context
    propagation has changed in a way that lets inner-set context flow
    upward — which would mean the reordering fix could be reverted.

All zddc-server tests pass via `go test ./...` (run in podman against
golang:1.24-alpine since this dev host doesn't have Go installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:46:49 -05:00