ACLMiddleware now slog.Debug's the configured email-header name, the observed value at that name, and the full r.Header map on every request. Off at the default INFO log level; enable per-pod with ZDDC_LOG_LEVEL=debug. Motivated by debugging the X-Auth-Request-Email passthrough chain — when access logs show email=anonymous, /.admin/whoami is unreachable (the admin gate requires a non-empty email, which is the chicken-and-egg). The debug log line dumps headers without the gate, so an operator can identify whichever header name the upstream proxy is actually setting (X-Forwarded-User, X-Forwarded-Email, Remote-User, X-Authentik-Email, etc.) and adjust ZDDC_EMAIL_HEADER accordingly. The debug-level dump captures auth tokens and cookies along with everything else; safe in dev clusters, not appropriate for production unless the operator is comfortable with the trade-off. README documents the trade-off in the Admin Debug Page section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
|
|
"log/slog"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
// EmailKey is the context key for the authenticated user's email.
|
|
const EmailKey contextKey = "email"
|
|
|
|
// ACLMiddleware extracts the user email from the configured header and stores
|
|
// it in the request context. It does NOT enforce ACL itself — each handler
|
|
// performs its own ACL check via zddc.EffectivePolicy / zddc.AllowedWithChain.
|
|
func ACLMiddleware(cfg config.Config, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
email := r.Header.Get(cfg.EmailHeader)
|
|
// DEBUG-level header dump for diagnosing proxy / SSO header
|
|
// passthrough. Off by default (LogLevel info); enable with
|
|
// ZDDC_LOG_LEVEL=debug. Logs the configured header name, the
|
|
// observed value at that name, and the full request header
|
|
// map so an operator can see exactly what reached the binary.
|
|
// Note: at debug level this also captures auth tokens, cookies,
|
|
// and anything else upstream proxies forward — only enable in
|
|
// trusted environments.
|
|
slog.Debug("request headers",
|
|
"configured", cfg.EmailHeader,
|
|
"observed", email,
|
|
"headers", r.Header)
|
|
ctx := context.WithValue(r.Context(), EmailKey, email)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// EmailFromContext extracts the user email from the request context.
|
|
func EmailFromContext(r *http.Request) string {
|
|
if v, ok := r.Context().Value(EmailKey).(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// responseWriter wraps http.ResponseWriter to capture status code and bytes written.
|
|
type responseWriter struct {
|
|
http.ResponseWriter
|
|
status int
|
|
bytes int
|
|
wrote bool
|
|
}
|
|
|
|
// WriteHeader records the status code and writes it to the underlying ResponseWriter.
|
|
func (rw *responseWriter) WriteHeader(code int) {
|
|
rw.status = code
|
|
rw.wrote = true
|
|
rw.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// Write records the bytes written and writes to the underlying ResponseWriter.
|
|
func (rw *responseWriter) Write(b []byte) (int, error) {
|
|
n, err := rw.ResponseWriter.Write(b)
|
|
rw.bytes += n
|
|
return n, err
|
|
}
|
|
|
|
// AccessLogMiddleware logs a structured line per HTTP request after the response is written.
|
|
func AccessLogMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Capture request start time
|
|
start := time.Now()
|
|
|
|
// Wrap the ResponseWriter
|
|
wrapped := &responseWriter{ResponseWriter: w, status: 200}
|
|
|
|
// Serve the request
|
|
next.ServeHTTP(wrapped, r)
|
|
|
|
// Calculate duration
|
|
durationMs := int(time.Since(start).Milliseconds())
|
|
|
|
// Get email from context
|
|
email := EmailFromContext(r)
|
|
if email == "" {
|
|
email = "anonymous"
|
|
}
|
|
|
|
// Log access
|
|
slog.Info("access",
|
|
"ts", start.Format(time.RFC3339),
|
|
"email", email,
|
|
"method", r.Method,
|
|
"path", r.URL.Path,
|
|
"status", wrapped.status,
|
|
"bytes", wrapped.bytes,
|
|
"duration_ms", durationMs,
|
|
)
|
|
})
|
|
}
|