Files
2026-08-09 05:33:21 +08:00

69 lines
1.9 KiB
Go

package server
import (
"context"
"net"
"net/http"
"strings"
"time"
"vocat/internal/store"
)
// recordAudit writes one security-relevant event to the audit trail. Failures
// are logged but never block the request being audited.
func (s *Server) recordAudit(
ctx context.Context,
actor string,
action string,
entityType string,
entityID string,
outcome string,
remoteAddr string,
) {
if s.store == nil {
return
}
_, err := s.store.AppendAuditEvent(ctx, store.AuditEvent{
Actor: actor,
Action: action,
EntityType: entityType,
EntityID: entityID,
Outcome: outcome,
RemoteAddr: remoteAddr,
CreatedAt: time.Now().UTC(),
})
if err != nil {
s.logger.Warn("write audit event failed", "action", action, "error", err)
}
}
// audit records an event for an already-authenticated request, resolving the
// actor from the session and the source address from the raw connection (proxy
// headers are deliberately not trusted for the audit trail).
func (s *Server) audit(r *http.Request, action string, entityType string, entityID string, outcome string) {
actor := ""
if s.auth != nil {
if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
if session, authErr := s.auth.Authenticate(r.Context(), cookie.Value); authErr == nil {
actor = session.Principal.Username
}
}
}
s.recordAudit(r.Context(), actor, action, entityType, entityID, outcome, requestRemoteHost(r))
}
// auditAuth records an authentication event where no session exists yet (the
// actor is the username that was attempted).
func (s *Server) auditAuth(r *http.Request, username string, outcome string) {
s.recordAudit(r.Context(), username, "auth.login", "session", username, outcome, requestRemoteHost(r))
}
func requestRemoteHost(r *http.Request) string {
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
if err != nil {
return strings.TrimSpace(r.RemoteAddr)
}
return host
}