audit.go 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. package server
  2. import (
  3. "context"
  4. "net"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "vocat/internal/store"
  9. )
  10. // recordAudit writes one security-relevant event to the audit trail. Failures
  11. // are logged but never block the request being audited.
  12. func (s *Server) recordAudit(
  13. ctx context.Context,
  14. actor string,
  15. action string,
  16. entityType string,
  17. entityID string,
  18. outcome string,
  19. remoteAddr string,
  20. ) {
  21. if s.store == nil {
  22. return
  23. }
  24. _, err := s.store.AppendAuditEvent(ctx, store.AuditEvent{
  25. Actor: actor,
  26. Action: action,
  27. EntityType: entityType,
  28. EntityID: entityID,
  29. Outcome: outcome,
  30. RemoteAddr: remoteAddr,
  31. CreatedAt: time.Now().UTC(),
  32. })
  33. if err != nil {
  34. s.logger.Warn("write audit event failed", "action", action, "error", err)
  35. }
  36. }
  37. // audit records an event for an already-authenticated request, resolving the
  38. // actor from the session and the source address from the raw connection (proxy
  39. // headers are deliberately not trusted for the audit trail).
  40. func (s *Server) audit(r *http.Request, action string, entityType string, entityID string, outcome string) {
  41. actor := ""
  42. if s.auth != nil {
  43. if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
  44. if session, authErr := s.auth.Authenticate(r.Context(), cookie.Value); authErr == nil {
  45. actor = session.Principal.Username
  46. }
  47. }
  48. }
  49. s.recordAudit(r.Context(), actor, action, entityType, entityID, outcome, requestRemoteHost(r))
  50. }
  51. // auditAuth records an authentication event where no session exists yet (the
  52. // actor is the username that was attempted).
  53. func (s *Server) auditAuth(r *http.Request, username string, outcome string) {
  54. s.recordAudit(r.Context(), username, "auth.login", "session", username, outcome, requestRemoteHost(r))
  55. }
  56. func requestRemoteHost(r *http.Request) string {
  57. host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
  58. if err != nil {
  59. return strings.TrimSpace(r.RemoteAddr)
  60. }
  61. return host
  62. }