audit logs
This commit is contained in:
@ -12,6 +12,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/audit"
|
||||
"github.com/kms/api-key-service/internal/config"
|
||||
"github.com/kms/api-key-service/internal/database"
|
||||
"github.com/kms/api-key-service/internal/handlers"
|
||||
@ -65,6 +66,9 @@ func main() {
|
||||
grantRepo := postgres.NewGrantedPermissionRepository(db)
|
||||
auditRepo := postgres.NewAuditRepository(db)
|
||||
|
||||
// Initialize audit logger
|
||||
auditLogger := audit.NewAuditLogger(cfg, logger, auditRepo)
|
||||
|
||||
// Initialize services
|
||||
appService := services.NewApplicationService(appRepo, auditRepo, logger)
|
||||
tokenService := services.NewTokenService(tokenRepo, appRepo, permRepo, grantRepo, cfg.GetString("INTERNAL_HMAC_KEY"), cfg, logger)
|
||||
@ -75,9 +79,10 @@ func main() {
|
||||
appHandler := handlers.NewApplicationHandler(appService, authService, logger)
|
||||
tokenHandler := handlers.NewTokenHandler(tokenService, authService, logger)
|
||||
authHandler := handlers.NewAuthHandler(authService, tokenService, cfg, logger)
|
||||
auditHandler := handlers.NewAuditHandler(auditLogger, authService, logger)
|
||||
|
||||
// Set up router
|
||||
router := setupRouter(cfg, logger, healthHandler, appHandler, tokenHandler, authHandler)
|
||||
router := setupRouter(cfg, logger, healthHandler, appHandler, tokenHandler, authHandler, auditHandler)
|
||||
|
||||
// Create HTTP server
|
||||
srv := &http.Server{
|
||||
@ -151,7 +156,7 @@ func initLogger(cfg config.ConfigProvider) *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, appHandler *handlers.ApplicationHandler, tokenHandler *handlers.TokenHandler, authHandler *handlers.AuthHandler) *gin.Engine {
|
||||
func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *handlers.HealthHandler, appHandler *handlers.ApplicationHandler, tokenHandler *handlers.TokenHandler, authHandler *handlers.AuthHandler, auditHandler *handlers.AuditHandler) *gin.Engine {
|
||||
// Set Gin mode based on environment
|
||||
if cfg.IsProduction() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@ -199,6 +204,11 @@ func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *h
|
||||
protected.POST("/applications/:id/tokens", tokenHandler.Create)
|
||||
protected.DELETE("/tokens/:id", tokenHandler.Delete)
|
||||
|
||||
// Audit management
|
||||
protected.GET("/audit/events", auditHandler.ListEvents)
|
||||
protected.GET("/audit/events/:id", auditHandler.GetEvent)
|
||||
protected.GET("/audit/stats", auditHandler.GetStats)
|
||||
|
||||
// Documentation endpoint
|
||||
protected.GET("/docs", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@ -223,6 +233,11 @@ func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *h
|
||||
"POST /api/applications/:id/tokens",
|
||||
"DELETE /api/tokens/:id",
|
||||
},
|
||||
"audit": []string{
|
||||
"GET /api/audit/events",
|
||||
"GET /api/audit/events/:id",
|
||||
"GET /api/audit/stats",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Card,
|
||||
@ -28,6 +28,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { apiService, AuditEvent, AuditQueryParams } from '../services/apiService';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@ -35,103 +36,10 @@ const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
interface AuditLogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
user_id: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
status: 'success' | 'failure' | 'warning';
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
details: Record<string, any>;
|
||||
}
|
||||
|
||||
// Mock audit data for demonstration
|
||||
const mockAuditData: AuditLogEntry[] = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: dayjs().subtract(1, 'hour').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'CREATE_APPLICATION',
|
||||
resource_type: 'application',
|
||||
resource_id: 'com.example.newapp',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
app_link: 'https://newapp.example.com',
|
||||
owner: 'Development Team'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: dayjs().subtract(2, 'hours').toISOString(),
|
||||
user_id: 'user@example.com',
|
||||
action: 'CREATE_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-abc123',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.101',
|
||||
user_agent: 'curl/7.68.0',
|
||||
details: {
|
||||
app_id: 'com.example.app',
|
||||
permissions: ['repo.read', 'repo.write']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: dayjs().subtract(3, 'hours').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'DELETE_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-xyz789',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
app_id: 'com.example.oldapp',
|
||||
reason: 'Token compromised'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: dayjs().subtract(4, 'hours').toISOString(),
|
||||
user_id: 'user@example.com',
|
||||
action: 'VERIFY_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-def456',
|
||||
status: 'failure',
|
||||
ip_address: '192.168.1.102',
|
||||
user_agent: 'PostmanRuntime/7.28.4',
|
||||
details: {
|
||||
app_id: 'com.example.app',
|
||||
error: 'Token expired'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: dayjs().subtract(6, 'hours').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'UPDATE_APPLICATION',
|
||||
resource_type: 'application',
|
||||
resource_id: 'com.example.app',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
changes: {
|
||||
callback_url: 'https://updated.example.com/callback'
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const Audit: React.FC = () => {
|
||||
const [auditData, setAuditData] = useState<AuditLogEntry[]>(mockAuditData);
|
||||
const [filteredData, setFilteredData] = useState<AuditLogEntry[]>(mockAuditData);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [auditData, setAuditData] = useState<AuditEvent[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<AuditEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState({
|
||||
dateRange: null as any,
|
||||
action: '',
|
||||
@ -140,36 +48,70 @@ const Audit: React.FC = () => {
|
||||
resourceType: '',
|
||||
});
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...auditData];
|
||||
// Load audit data on component mount
|
||||
useEffect(() => {
|
||||
loadAuditData();
|
||||
}, []);
|
||||
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
const [start, end] = filters.dateRange;
|
||||
filtered = filtered.filter(entry => {
|
||||
const entryDate = dayjs(entry.timestamp);
|
||||
return entryDate.isAfter(start) && entryDate.isBefore(end);
|
||||
const loadAuditData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiService.getAuditEvents({
|
||||
limit: 100,
|
||||
order_by: 'timestamp',
|
||||
order_desc: true,
|
||||
});
|
||||
setAuditData(response.events);
|
||||
setFilteredData(response.events);
|
||||
} catch (error) {
|
||||
console.error('Failed to load audit data:', error);
|
||||
// Keep empty arrays on error
|
||||
setAuditData([]);
|
||||
setFilteredData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (filters.action) {
|
||||
filtered = filtered.filter(entry => entry.action === filters.action);
|
||||
const applyFilters = async () => {
|
||||
// For real-time filtering, we'll use the API with filters
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: AuditQueryParams = {
|
||||
limit: 100,
|
||||
order_by: 'timestamp',
|
||||
order_desc: true,
|
||||
};
|
||||
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
const [start, end] = filters.dateRange;
|
||||
params.start_time = start.toISOString();
|
||||
params.end_time = end.toISOString();
|
||||
}
|
||||
|
||||
if (filters.action) {
|
||||
params.event_types = [filters.action];
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
params.statuses = [filters.status];
|
||||
}
|
||||
|
||||
if (filters.user) {
|
||||
params.actor_id = filters.user;
|
||||
}
|
||||
|
||||
if (filters.resourceType) {
|
||||
params.resource_type = filters.resourceType;
|
||||
}
|
||||
|
||||
const response = await apiService.getAuditEvents(params);
|
||||
setFilteredData(response.events);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply filters:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
filtered = filtered.filter(entry => entry.status === filters.status);
|
||||
}
|
||||
|
||||
if (filters.user) {
|
||||
filtered = filtered.filter(entry =>
|
||||
entry.user_id.toLowerCase().includes(filters.user.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.resourceType) {
|
||||
filtered = filtered.filter(entry => entry.resource_type === filters.resourceType);
|
||||
}
|
||||
|
||||
setFilteredData(filtered);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
@ -180,7 +122,7 @@ const Audit: React.FC = () => {
|
||||
user: '',
|
||||
resourceType: '',
|
||||
});
|
||||
setFilteredData(auditData);
|
||||
loadAuditData(); // Reload original data
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
@ -216,42 +158,42 @@ const Audit: React.FC = () => {
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
sorter: (a: AuditLogEntry, b: AuditLogEntry) =>
|
||||
sorter: (a: AuditEvent, b: AuditEvent) =>
|
||||
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
|
||||
defaultSortOrder: 'descend' as const,
|
||||
},
|
||||
{
|
||||
title: 'User',
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
render: (userId: string) => (
|
||||
dataIndex: 'actor_id',
|
||||
key: 'actor_id',
|
||||
render: (actorId: string) => (
|
||||
<div>
|
||||
<UserOutlined style={{ marginRight: '8px' }} />
|
||||
{userId}
|
||||
{actorId || 'System'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (action: string) => (
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => (
|
||||
<div>
|
||||
{getActionIcon(action)}
|
||||
<span style={{ marginLeft: '8px' }}>{action.replace(/_/g, ' ')}</span>
|
||||
{getActionIcon(type)}
|
||||
<span style={{ marginLeft: '8px' }}>{type.replace(/_/g, ' ').replace(/\./g, ' ')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Resource',
|
||||
key: 'resource',
|
||||
render: (_: any, record: AuditLogEntry) => (
|
||||
render: (_: any, record: AuditEvent) => (
|
||||
<div>
|
||||
<div>
|
||||
<Tag color="blue">{record.resource_type.toUpperCase()}</Tag>
|
||||
<Tag color="blue">{record.resource_type?.toUpperCase() || 'N/A'}</Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{record.resource_id}
|
||||
{record.resource_id || 'N/A'}
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
@ -271,13 +213,13 @@ const Audit: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: 'IP Address',
|
||||
dataIndex: 'ip_address',
|
||||
key: 'ip_address',
|
||||
render: (ip: string) => <Text code>{ip}</Text>,
|
||||
dataIndex: 'actor_ip',
|
||||
key: 'actor_ip',
|
||||
render: (ip: string) => <Text code>{ip || 'N/A'}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
const expandedRowRender = (record: AuditLogEntry) => (
|
||||
const expandedRowRender = (record: AuditEvent) => (
|
||||
<Card size="small" title="Event Details">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
@ -360,7 +302,7 @@ const Audit: React.FC = () => {
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<UserOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||
{new Set(filteredData.map(e => e.user_id)).size}
|
||||
{new Set(filteredData.map(e => e.actor_id).filter(id => id)).size}
|
||||
</div>
|
||||
<div>Unique Users</div>
|
||||
</div>
|
||||
@ -401,12 +343,14 @@ const Audit: React.FC = () => {
|
||||
onChange={(value) => setFilters({ ...filters, action: value })}
|
||||
allowClear
|
||||
>
|
||||
<Option value="CREATE_APPLICATION">Create Application</Option>
|
||||
<Option value="UPDATE_APPLICATION">Update Application</Option>
|
||||
<Option value="DELETE_APPLICATION">Delete Application</Option>
|
||||
<Option value="CREATE_TOKEN">Create Token</Option>
|
||||
<Option value="DELETE_TOKEN">Delete Token</Option>
|
||||
<Option value="VERIFY_TOKEN">Verify Token</Option>
|
||||
<Option value="app.created">Application Created</Option>
|
||||
<Option value="app.updated">Application Updated</Option>
|
||||
<Option value="app.deleted">Application Deleted</Option>
|
||||
<Option value="auth.token_created">Token Created</Option>
|
||||
<Option value="auth.token_revoked">Token Revoked</Option>
|
||||
<Option value="auth.token_validated">Token Validated</Option>
|
||||
<Option value="auth.login">Login</Option>
|
||||
<Option value="auth.login_failed">Login Failed</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
@ -465,16 +409,16 @@ const Audit: React.FC = () => {
|
||||
color={entry.status === 'success' ? 'green' : entry.status === 'failure' ? 'red' : 'orange'}
|
||||
>
|
||||
<div>
|
||||
<Text strong>{entry.action.replace(/_/g, ' ')}</Text>
|
||||
<Text strong>{entry.type.replace(/_/g, ' ').replace(/\./g, ' ')}</Text>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
{entry.user_id} • {dayjs(entry.timestamp).fromNow()}
|
||||
{entry.actor_id || 'System'} • {dayjs(entry.timestamp).fromNow()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag>{entry.resource_type}</Tag>
|
||||
<Tag>{entry.resource_type || 'N/A'}</Tag>
|
||||
<Text type="secondary" style={{ marginLeft: '8px' }}>
|
||||
{entry.resource_id}
|
||||
{entry.resource_id || 'N/A'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
@ -485,13 +429,15 @@ const Audit: React.FC = () => {
|
||||
|
||||
{/* Audit Log Table */}
|
||||
<Card title="Audit Log Entries">
|
||||
<Alert
|
||||
message="Demo Data"
|
||||
description="This audit log shows simulated data for demonstration purposes. In production, this would display real audit events from your KMS system."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
{filteredData.length === 0 && !loading && (
|
||||
<Alert
|
||||
message="No Audit Events Found"
|
||||
description="No audit events match your current filters. Try adjusting the filters or check if any events have been logged to the system."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
|
||||
@ -90,6 +90,59 @@ export interface VerifyResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
actor_id?: string;
|
||||
actor_ip?: string;
|
||||
user_agent?: string;
|
||||
resource_id?: string;
|
||||
resource_type?: string;
|
||||
action: string;
|
||||
description: string;
|
||||
details?: Record<string, any>;
|
||||
request_id?: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export interface AuditQueryParams {
|
||||
event_types?: string[];
|
||||
statuses?: string[];
|
||||
actor_id?: string;
|
||||
resource_id?: string;
|
||||
resource_type?: string;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by?: string;
|
||||
order_desc?: boolean;
|
||||
}
|
||||
|
||||
export interface AuditResponse {
|
||||
events: AuditEvent[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface AuditStats {
|
||||
total_events: number;
|
||||
by_type: Record<string, number>;
|
||||
by_severity: Record<string, number>;
|
||||
by_status: Record<string, number>;
|
||||
by_time?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface AuditStatsParams {
|
||||
event_types?: string[];
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
group_by?: string;
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
private api: AxiosInstance;
|
||||
private baseURL: string;
|
||||
@ -204,6 +257,55 @@ class ApiService {
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Audit endpoints
|
||||
async getAuditEvents(params?: AuditQueryParams): Promise<AuditResponse> {
|
||||
const queryString = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
if (params.event_types?.length) {
|
||||
params.event_types.forEach(type => queryString.append('event_types', type));
|
||||
}
|
||||
if (params.statuses?.length) {
|
||||
params.statuses.forEach(status => queryString.append('statuses', status));
|
||||
}
|
||||
if (params.actor_id) queryString.set('actor_id', params.actor_id);
|
||||
if (params.resource_id) queryString.set('resource_id', params.resource_id);
|
||||
if (params.resource_type) queryString.set('resource_type', params.resource_type);
|
||||
if (params.start_time) queryString.set('start_time', params.start_time);
|
||||
if (params.end_time) queryString.set('end_time', params.end_time);
|
||||
if (params.limit) queryString.set('limit', params.limit.toString());
|
||||
if (params.offset) queryString.set('offset', params.offset.toString());
|
||||
if (params.order_by) queryString.set('order_by', params.order_by);
|
||||
if (params.order_desc !== undefined) queryString.set('order_desc', params.order_desc.toString());
|
||||
}
|
||||
|
||||
const url = `/api/audit/events${queryString.toString() ? '?' + queryString.toString() : ''}`;
|
||||
const response = await this.api.get(url);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getAuditEvent(eventId: string): Promise<AuditEvent> {
|
||||
const response = await this.api.get(`/api/audit/events/${eventId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getAuditStats(params?: AuditStatsParams): Promise<AuditStats> {
|
||||
const queryString = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
if (params.event_types?.length) {
|
||||
params.event_types.forEach(type => queryString.append('event_types', type));
|
||||
}
|
||||
if (params.start_time) queryString.set('start_time', params.start_time);
|
||||
if (params.end_time) queryString.set('end_time', params.end_time);
|
||||
if (params.group_by) queryString.set('group_by', params.group_by);
|
||||
}
|
||||
|
||||
const url = `/api/audit/stats${queryString.toString() ? '?' + queryString.toString() : ''}`;
|
||||
const response = await this.api.get(url);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
||||
Reference in New Issue
Block a user