Files
storage-appliance/internal/audit/audit.go

120 lines
3.0 KiB
Go

package audit
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"log"
"time"
"github.com/google/uuid"
)
type Event struct {
ID string
Timestamp time.Time
UserID string
Action string
ResourceType string
ResourceID string
Success bool
Details map[string]any
// Enhanced fields
Actor string // Username or user identifier
Resource string // Full resource identifier (e.g., "pool:my-pool")
PayloadHash string // SHA256 hash of request payload
Result string // Success/failure message or status
ClientIP string // Client IP address
}
type AuditLogger interface {
Record(ctx context.Context, e Event) error
}
type SQLAuditLogger struct {
DB *sql.DB
}
func NewSQLAuditLogger(db *sql.DB) *SQLAuditLogger {
return &SQLAuditLogger{DB: db}
}
func (l *SQLAuditLogger) Record(ctx context.Context, e Event) error {
if e.ID == "" {
e.ID = uuid.New().String()
}
if e.Timestamp.IsZero() {
e.Timestamp = time.Now()
}
// Set actor from UserID if not provided
if e.Actor == "" {
e.Actor = e.UserID
}
// Build resource string from ResourceType and ResourceID
if e.Resource == "" {
if e.ResourceID != "" {
e.Resource = e.ResourceType + ":" + e.ResourceID
} else {
e.Resource = e.ResourceType
}
}
// Set result from Success if not provided
if e.Result == "" {
if e.Success {
e.Result = "success"
} else {
e.Result = "failure"
}
}
detailsJSON, _ := json.Marshal(e.Details)
// Try to insert with all columns, fallback to basic columns if enhanced columns don't exist
_, err := l.DB.ExecContext(ctx,
`INSERT INTO audit_events (id, ts, user_id, action, resource_type, resource_id, success, details, actor, resource, payload_hash, result, client_ip)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.ID, e.Timestamp, e.UserID, e.Action, e.ResourceType, e.ResourceID, boolToInt(e.Success), string(detailsJSON),
e.Actor, e.Resource, e.PayloadHash, e.Result, e.ClientIP)
if err != nil {
// Fallback to basic insert if enhanced columns don't exist yet
_, err2 := l.DB.ExecContext(ctx,
`INSERT INTO audit_events (id, ts, user_id, action, resource_type, resource_id, success, details)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
e.ID, e.Timestamp, e.UserID, e.Action, e.ResourceType, e.ResourceID, boolToInt(e.Success), string(detailsJSON))
if err2 != nil {
log.Printf("audit record failed: %v (fallback also failed: %v)", err, err2)
return err2
}
log.Printf("audit record inserted with fallback (enhanced columns may not exist): %v", err)
}
return nil
}
// HashPayload computes SHA256 hash of a payload (JSON string or bytes)
func HashPayload(payload interface{}) string {
var data []byte
switch v := payload.(type) {
case []byte:
data = v
case string:
data = []byte(v)
default:
jsonData, _ := json.Marshal(payload)
data = jsonData
}
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}