fetch_ml/internal/api/audit/handlers.go
Jeremie Fraeys c18a8619fe
feat(api): add structured error package and refactor handlers
New error handling:
- Add internal/api/errors/errors.go with structured API error types
- Standardize error codes across all API endpoints
- Add user-facing error messages vs internal error details separation

Handler improvements:
- jupyter/handlers.go: better workspace lifecycle and error handling
- plugins/handlers.go: plugin management with validation
- groups/handlers.go: group CRUD with capability metadata
- jobs/handlers.go: job submission and monitoring improvements
- datasets/handlers.go: dataset upload/download with progress
- validate/handlers.go: manifest validation with detailed errors
- audit/handlers.go: audit log querying with filters

Server configuration:
- server_config.go: refined config loading with validation
- server_gen.go: improved code generation for OpenAPI specs
2026-03-12 12:04:46 -04:00

210 lines
6.2 KiB
Go

// Package audit provides HTTP handlers for audit log management
package audit
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/jfraeys/fetch_ml/internal/api/errors"
"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") {
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
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 {
errors.WriteHTTPError(w, http.StatusInternalServerError, errors.CodeUnknownError, "Failed to query events", err.Error())
return
}
response := AuditEventList{
Events: events,
Total: total,
Limit: limit,
Offset: offset,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Warn("failed to encode audit events", "error", err)
}
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")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Warn("failed to encode empty audit events", "error", err)
}
}
// 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") {
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
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")
if err := json.NewEncoder(w).Encode(result); err != nil {
h.logger.Warn("failed to encode verification result", "error", err)
}
}
// 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") {
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
return
}
// Get chain root (placeholder implementation)
response := ChainRootResponse{
RootHash: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
Timestamp: time.Now().UTC(),
TotalEvents: 0,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Warn("failed to encode chain root", "error", err)
}
}
// 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
}