b25e074e1d
Implements Phase 4 of v0.1 Foundation: - internal/store/migrations/0003_audit_log.sql: audit_log table with indexes - internal/store/audit_repo.go: AuditRepo (Append + List) - internal/store/audit_repo_test.go: 2 tests for audit persistence - internal/engine/audit.go: Audit wrapper that persists to SQLite AND logs via slog - internal/cli/audit.go: orca audit list command (text + JSON) - Registry now records every join/leave/forget with actor/action/resource/result Verified: audit entries persist across restarts, JSON output includes metadata, node operations emit audit records. All tests pass with -race. ---ci--- project: orca phase: 4 milestone: v0.1 status: execute req_covered: - REQ-005 - REQ-006 - REQ-008 - REQ-017 - REQ-018 ---/ci---
82 lines
2.2 KiB
Go
82 lines
2.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type AuditEntry struct {
|
|
ID int64 `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Actor string `json:"actor"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
Result string `json:"result"`
|
|
Error string `json:"error,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type AuditRepo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewAuditRepo(db *sql.DB) *AuditRepo {
|
|
return &AuditRepo{db: db}
|
|
}
|
|
|
|
func (r *AuditRepo) Append(ctx context.Context, e *AuditEntry) error {
|
|
if e.Timestamp.IsZero() {
|
|
e.Timestamp = time.Now().UTC()
|
|
}
|
|
if e.Actor == "" {
|
|
e.Actor = "system"
|
|
}
|
|
metaJSON, _ := json.Marshal(e.Metadata)
|
|
if e.Error == "" {
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO audit_log (timestamp, actor, action, resource, result, metadata) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
e.Timestamp, e.Actor, e.Action, e.Resource, e.Result, string(metaJSON))
|
|
if err != nil {
|
|
return fmt.Errorf("insert audit: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
_, err := r.db.ExecContext(ctx,
|
|
`INSERT INTO audit_log (timestamp, actor, action, resource, result, error, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
e.Timestamp, e.Actor, e.Action, e.Resource, e.Result, e.Error, string(metaJSON))
|
|
if err != nil {
|
|
return fmt.Errorf("insert audit (with error): %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *AuditRepo) List(ctx context.Context, limit int) ([]*AuditEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.db.QueryContext(ctx,
|
|
`SELECT id, timestamp, actor, action, resource, result, COALESCE(error, ''), COALESCE(metadata, '') FROM audit_log ORDER BY id DESC LIMIT ?`, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list audit: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var entries []*AuditEntry
|
|
for rows.Next() {
|
|
var (
|
|
e AuditEntry
|
|
metaJSON string
|
|
)
|
|
if err := rows.Scan(&e.ID, &e.Timestamp, &e.Actor, &e.Action, &e.Resource, &e.Result, &e.Error, &metaJSON); err != nil {
|
|
return nil, fmt.Errorf("scan audit: %w", err)
|
|
}
|
|
if metaJSON != "" {
|
|
_ = json.Unmarshal([]byte(metaJSON), &e.Metadata)
|
|
}
|
|
entries = append(entries, &e)
|
|
}
|
|
return entries, rows.Err()
|
|
}
|