// Package audit provides HTTP handlers for audit log management package audit import ( "encoding/json" "net/http" "strconv" "time" "github.com/jfraeys/fetch_ml/internal/auth" "github.com/jfraeys/fetch_ml/internal/logging" ) // Handler provides audit-related HTTP API handlers type Handler struct { logger *logging.Logger store AuditStore // Optional: separate store for querying } // AuditStore interface for querying audit events type AuditStore interface { QueryEvents(from, to time.Time, eventType, userID string, limit, offset int) ([]AuditEvent, int, error) } // AuditEvent represents an audit event for API responses type AuditEvent struct { Timestamp time.Time `json:"timestamp"` EventType string `json:"event_type"` UserID string `json:"user_id,omitempty"` Resource string `json:"resource,omitempty"` Action string `json:"action,omitempty"` Success bool `json:"success"` IPAddress string `json:"ip_address,omitempty"` Error string `json:"error,omitempty"` PrevHash string `json:"prev_hash,omitempty"` EventHash string `json:"event_hash,omitempty"` SequenceNum int `json:"sequence_num,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` } // AuditEventList represents a list of audit events type AuditEventList struct { Events []AuditEvent `json:"events"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } // VerificationResult represents the result of audit chain verification type VerificationResult struct { Valid bool `json:"valid"` TotalEvents int `json:"total_events"` FirstTampered int `json:"first_tampered,omitempty"` ChainRootHash string `json:"chain_root_hash,omitempty"` VerifiedAt time.Time `json:"verified_at"` } // ChainRootResponse represents the chain root hash response type ChainRootResponse struct { RootHash string `json:"root_hash"` Timestamp time.Time `json:"timestamp"` TotalEvents int `json:"total_events"` } // NewHandler creates a new audit API handler func NewHandler(logger *logging.Logger, store AuditStore) *Handler { return &Handler{ logger: logger, store: store, } } // GetV1AuditEvents handles GET /v1/audit/events func (h *Handler) GetV1AuditEvents(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "audit:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } // Parse query parameters fromStr := r.URL.Query().Get("from") toStr := r.URL.Query().Get("to") eventType := r.URL.Query().Get("event_type") userID := r.URL.Query().Get("user_id") limit := parseIntQueryParam(r, "limit", 100) offset := parseIntQueryParam(r, "offset", 0) // Validate limit if limit > 1000 { limit = 1000 } // Parse timestamps var from, to time.Time if fromStr != "" { from, _ = time.Parse(time.RFC3339, fromStr) } if toStr != "" { to, _ = time.Parse(time.RFC3339, toStr) } // If store is available, query from it if h.store != nil { events, total, err := h.store.QueryEvents(from, to, eventType, userID, limit, offset) if err != nil { http.Error(w, `{"error":"Failed to query events","code":"INTERNAL_ERROR"}`, http.StatusInternalServerError) return } response := AuditEventList{ Events: events, Total: total, Limit: limit, Offset: offset, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } // Return empty list if no store configured response := AuditEventList{ Events: []AuditEvent{}, Total: 0, Limit: limit, Offset: offset, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // PostV1AuditVerify handles POST /v1/audit/verify func (h *Handler) PostV1AuditVerify(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "audit:verify") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } h.logger.Info("verifying audit chain", "user", user.Name) // Perform verification (placeholder implementation) result := VerificationResult{ Valid: true, TotalEvents: 0, // Would be populated from actual verification VerifiedAt: time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // GetV1AuditChainRoot handles GET /v1/audit/chain-root func (h *Handler) GetV1AuditChainRoot(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "audit:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } // Get chain root (placeholder implementation) response := ChainRootResponse{ RootHash: "sha256:0000000000000000000000000000000000000000000000000000000000000000", Timestamp: time.Now().UTC(), TotalEvents: 0, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // checkPermission checks if the user has the required permission func (h *Handler) checkPermission(user *auth.User, permission string) bool { if user == nil { return false } // Admin has all permissions if user.Admin { return true } // Check specific permission return user.HasPermission(permission) } // parseIntQueryParam parses an integer query parameter func parseIntQueryParam(r *http.Request, name string, defaultVal int) int { str := r.URL.Query().Get(name) if str == "" { return defaultVal } val, err := strconv.Atoi(str) if err != nil { return defaultVal } return val }