This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
ID string
|
||||
Actor string
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
Result string
|
||||
OccurredAt time.Time
|
||||
CorrelationID string
|
||||
Before map[string]any
|
||||
After map[string]any
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Append(context.Context, Event) error
|
||||
}
|
||||
|
||||
type MemoryStore struct {
|
||||
Events []Event
|
||||
}
|
||||
|
||||
func (store *MemoryStore) Append(_ context.Context, event Event) error {
|
||||
if event.ID == "" {
|
||||
event.ID = newID()
|
||||
}
|
||||
if event.OccurredAt.IsZero() {
|
||||
event.OccurredAt = time.Now().UTC()
|
||||
}
|
||||
store.Events = append(store.Events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
type PostgresStore struct{ Pool *pgxpool.Pool }
|
||||
|
||||
func (store PostgresStore) Append(ctx context.Context, event Event) error {
|
||||
if store.Pool == nil {
|
||||
return errors.New("audit database pool is nil")
|
||||
}
|
||||
if event.ID == "" {
|
||||
event.ID = newID()
|
||||
}
|
||||
if event.OccurredAt.IsZero() {
|
||||
event.OccurredAt = time.Now().UTC()
|
||||
}
|
||||
before, err := json.Marshal(event.Before)
|
||||
if err != nil {
|
||||
return errors.New("marshal audit before diff")
|
||||
}
|
||||
after, err := json.Marshal(event.After)
|
||||
if err != nil {
|
||||
return errors.New("marshal audit after diff")
|
||||
}
|
||||
_, err = store.Pool.Exec(ctx, `INSERT INTO audit_events (id, actor, action, resource_type, resource_id, result, occurred_at, correlation_id, before_diff, after_diff) VALUES ($1::uuid, $2, $3, $4, NULLIF($5, '')::uuid, $6, $7, $8, $9::jsonb, $10::jsonb)`, event.ID, event.Actor, event.Action, event.ResourceType, event.ResourceID, event.Result, event.OccurredAt, event.CorrelationID, before, after)
|
||||
if err != nil {
|
||||
return errors.New("write audit event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newID() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "00000000-0000-4000-8000-000000000000"
|
||||
}
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16])
|
||||
}
|
||||
|
||||
func RecordSecurityAction(ctx context.Context, store Store, actor, action, result, correlationID string) error {
|
||||
return store.Append(ctx, Event{Actor: actor, Action: action, ResourceType: "security", Result: result, CorrelationID: correlationID})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSecurityActionsAreStoredWithCorrelation(t *testing.T) {
|
||||
store := &MemoryStore{}
|
||||
if err := RecordSecurityAction(context.Background(), store, "user-1", "config.update", "success", "corr-1234"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(store.Events) != 1 || store.Events[0].CorrelationID != "corr-1234" || store.Events[0].Action != "config.update" {
|
||||
t.Fatalf("unexpected audit event: %#v", store.Events)
|
||||
}
|
||||
if store.Events[0].ID == "" || store.Events[0].OccurredAt.IsZero() {
|
||||
t.Fatal("audit event lacks identity or timestamp")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user