1package storage
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "strings"
8 "time"
9)
10
11func (s *Store) AppendAudit(ctx context.Context, input AppendAuditInput) (AuditEvent, error) {
12 const operation = "append audit event"
13 if err := s.checkOpen(operation); err != nil {
14 return AuditEvent{}, err
15 }
16 return appendAudit(ctx, s.sql, operation, input, canonicalTime(s.now()))
17}
18
19func appendAudit(ctx context.Context, q execer, operation string, input AppendAuditInput, occurredAt time.Time) (AuditEvent, error) {
20 if err := requireValue("actor", input.Actor); err != nil {
21 return AuditEvent{}, invalid(operation, err)
22 }
23 if err := requireValue("action", input.Action); err != nil {
24 return AuditEvent{}, invalid(operation, err)
25 }
26 if err := requireValue("subject", input.Subject); err != nil {
27 return AuditEvent{}, invalid(operation, err)
28 }
29 address := auditAddress(ctx, input.Address)
30 detail := input.Detail
31 if detail == "" {
32 detail = "{}"
33 }
34 result, err := q.ExecContext(ctx, `
35INSERT INTO audit_log (actor, action, subject, occurred_at, detail, address)
36VALUES (?, ?, ?, ?, ?, ?)`, input.Actor, input.Action, input.Subject,
37 timeToInt(occurredAt), detail, nullAddress(address))
38 if err != nil {
39 return AuditEvent{}, classify(operation, err)
40 }
41 id, err := result.LastInsertId()
42 if err != nil {
43 return AuditEvent{}, classify(operation+" id", err)
44 }
45 return AuditEvent{ID: id, Actor: input.Actor, Action: input.Action, Subject: input.Subject, OccurredAt: occurredAt, Detail: detail, Address: address}, nil
46}
47
48func (s *Store) ListAudit(ctx context.Context, filter AuditFilter) ([]AuditEvent, error) {
49 const operation = "list audit events"
50 if err := s.checkOpen(operation); err != nil {
51 return nil, err
52 }
53 limit := filter.Limit
54 if limit == 0 {
55 limit = 100
56 }
57 if limit < 1 || limit > 1000 {
58 return nil, invalid(operation, fmt.Errorf("limit must be between 1 and 1000"))
59 }
60 query := strings.Builder{}
61 query.WriteString(`SELECT id, actor, action, subject, occurred_at, detail, address FROM audit_log WHERE 1 = 1`)
62 var args []any
63 if filter.Actor != "" {
64 query.WriteString(` AND actor = ?`)
65 args = append(args, filter.Actor)
66 }
67 if filter.Action != "" {
68 query.WriteString(` AND action = ?`)
69 args = append(args, filter.Action)
70 }
71 if filter.Subject != "" {
72 query.WriteString(` AND subject = ?`)
73 args = append(args, filter.Subject)
74 }
75 if filter.After != nil {
76 query.WriteString(` AND occurred_at >= ?`)
77 args = append(args, timeToInt(*filter.After))
78 }
79 if filter.Before != nil {
80 query.WriteString(` AND occurred_at < ?`)
81 args = append(args, timeToInt(*filter.Before))
82 }
83 query.WriteString(` ORDER BY occurred_at DESC, id DESC LIMIT ?`)
84 args = append(args, limit)
85 rows, err := s.sql.QueryContext(ctx, query.String(), args...)
86 if err != nil {
87 return nil, classify(operation, err)
88 }
89 defer rows.Close()
90 var events []AuditEvent
91 for rows.Next() {
92 var event AuditEvent
93 var occurredAt int64
94 var address sql.NullString
95 if err := rows.Scan(&event.ID, &event.Actor, &event.Action, &event.Subject, &occurredAt, &event.Detail, &address); err != nil {
96 return nil, classify(operation, err)
97 }
98 event.OccurredAt = intToTime(occurredAt)
99 event.Address = scanAddress(address)
100 events = append(events, event)
101 }
102 if err := rows.Err(); err != nil {
103 return nil, classify(operation, err)
104 }
105 return events, nil
106}