audit logs
This commit is contained in:
254
internal/handlers/audit.go
Normal file
254
internal/handlers/audit.go
Normal file
@ -0,0 +1,254 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/audit"
|
||||
"github.com/kms/api-key-service/internal/errors"
|
||||
"github.com/kms/api-key-service/internal/services"
|
||||
"github.com/kms/api-key-service/internal/validation"
|
||||
)
|
||||
|
||||
// AuditHandler handles audit-related HTTP requests
|
||||
type AuditHandler struct {
|
||||
auditLogger audit.AuditLogger
|
||||
authService services.AuthenticationService
|
||||
validator *validation.Validator
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAuditHandler creates a new audit handler
|
||||
func NewAuditHandler(
|
||||
auditLogger audit.AuditLogger,
|
||||
authService services.AuthenticationService,
|
||||
logger *zap.Logger,
|
||||
) *AuditHandler {
|
||||
return &AuditHandler{
|
||||
auditLogger: auditLogger,
|
||||
authService: authService,
|
||||
validator: validation.NewValidator(logger),
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AuditQueryRequest represents the request for querying audit events
|
||||
type AuditQueryRequest struct {
|
||||
EventTypes []string `json:"event_types,omitempty" form:"event_types"`
|
||||
Statuses []string `json:"statuses,omitempty" form:"statuses"`
|
||||
ActorID string `json:"actor_id,omitempty" form:"actor_id"`
|
||||
ResourceID string `json:"resource_id,omitempty" form:"resource_id"`
|
||||
ResourceType string `json:"resource_type,omitempty" form:"resource_type"`
|
||||
StartTime *string `json:"start_time,omitempty" form:"start_time"`
|
||||
EndTime *string `json:"end_time,omitempty" form:"end_time"`
|
||||
Limit int `json:"limit,omitempty" form:"limit"`
|
||||
Offset int `json:"offset,omitempty" form:"offset"`
|
||||
OrderBy string `json:"order_by,omitempty" form:"order_by"`
|
||||
OrderDesc *bool `json:"order_desc,omitempty" form:"order_desc"`
|
||||
}
|
||||
|
||||
// AuditStatsRequest represents the request for audit statistics
|
||||
type AuditStatsRequest struct {
|
||||
EventTypes []string `json:"event_types,omitempty" form:"event_types"`
|
||||
StartTime *string `json:"start_time,omitempty" form:"start_time"`
|
||||
EndTime *string `json:"end_time,omitempty" form:"end_time"`
|
||||
GroupBy string `json:"group_by,omitempty" form:"group_by"`
|
||||
}
|
||||
|
||||
// AuditResponse represents the response structure for audit queries
|
||||
type AuditResponse struct {
|
||||
Events []AuditEventResponse `json:"events"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
|
||||
// AuditEventResponse represents a single audit event in API responses
|
||||
type AuditEventResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
ActorIP string `json:"actor_ip,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ResourceType string `json:"resource_type,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
}
|
||||
|
||||
// ListEvents handles GET /audit/events
|
||||
func (h *AuditHandler) ListEvents(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
var req AuditQueryRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if req.Limit <= 0 || req.Limit > 1000 {
|
||||
req.Limit = 100
|
||||
}
|
||||
if req.Offset < 0 {
|
||||
req.Offset = 0
|
||||
}
|
||||
if req.OrderBy == "" {
|
||||
req.OrderBy = "timestamp"
|
||||
}
|
||||
if req.OrderDesc == nil {
|
||||
orderDesc := true
|
||||
req.OrderDesc = &orderDesc
|
||||
}
|
||||
|
||||
// Convert request to audit filter
|
||||
filter := &audit.AuditFilter{
|
||||
ActorID: req.ActorID,
|
||||
ResourceID: req.ResourceID,
|
||||
ResourceType: req.ResourceType,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
OrderBy: req.OrderBy,
|
||||
OrderDesc: *req.OrderDesc,
|
||||
}
|
||||
|
||||
// Convert event types
|
||||
for _, et := range req.EventTypes {
|
||||
filter.EventTypes = append(filter.EventTypes, audit.EventType(et))
|
||||
}
|
||||
|
||||
// Convert statuses
|
||||
for _, st := range req.Statuses {
|
||||
filter.Statuses = append(filter.Statuses, audit.EventStatus(st))
|
||||
}
|
||||
|
||||
// Parse time filters
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil {
|
||||
filter.StartTime = &startTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil {
|
||||
filter.EndTime = &endTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Query audit events
|
||||
events, err := h.auditLogger.QueryEvents(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to query audit events", zap.Error(err))
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
response := &AuditResponse{
|
||||
Events: make([]AuditEventResponse, len(events)),
|
||||
Total: len(events), // Note: This is just the count of returned events, not total matching
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}
|
||||
|
||||
for i, event := range events {
|
||||
response.Events[i] = AuditEventResponse{
|
||||
ID: event.ID.String(),
|
||||
Type: string(event.Type),
|
||||
Status: string(event.Status),
|
||||
Timestamp: event.Timestamp.Format(time.RFC3339),
|
||||
ActorID: event.ActorID,
|
||||
ActorIP: event.ActorIP,
|
||||
UserAgent: event.UserAgent,
|
||||
ResourceID: event.ResourceID,
|
||||
ResourceType: event.ResourceType,
|
||||
Action: event.Action,
|
||||
Description: event.Description,
|
||||
Details: event.Details,
|
||||
RequestID: event.RequestID,
|
||||
SessionID: event.SessionID,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetEvent handles GET /audit/events/:id
|
||||
func (h *AuditHandler) GetEvent(c *gin.Context) {
|
||||
eventIDStr := c.Param("id")
|
||||
_, err := uuid.Parse(eventIDStr)
|
||||
if err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "id", "Invalid event ID format")
|
||||
return
|
||||
}
|
||||
|
||||
// Single event retrieval not yet implemented
|
||||
c.JSON(http.StatusNotImplemented, gin.H{
|
||||
"error": "Single event retrieval not yet implemented",
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats handles GET /audit/stats
|
||||
func (h *AuditHandler) GetStats(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
var req AuditStatsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert request to audit stats filter
|
||||
filter := &audit.AuditStatsFilter{
|
||||
GroupBy: req.GroupBy,
|
||||
}
|
||||
|
||||
// Convert event types
|
||||
for _, et := range req.EventTypes {
|
||||
filter.EventTypes = append(filter.EventTypes, audit.EventType(et))
|
||||
}
|
||||
|
||||
// Parse time filters
|
||||
if req.StartTime != nil && *req.StartTime != "" {
|
||||
if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil {
|
||||
filter.StartTime = &startTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.EndTime != nil && *req.EndTime != "" {
|
||||
if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil {
|
||||
filter.EndTime = &endTime
|
||||
} else {
|
||||
h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get audit statistics
|
||||
stats, err := h.auditLogger.GetEventStats(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get audit statistics", zap.Error(err))
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
Reference in New Issue
Block a user