1package storage
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "net/netip"
9 "slices"
10 "strings"
11 "time"
12)
13
14const (
15 AuditPageSize = 50
16 maxAuditPageSize = 200
17)
18
19type AuditCursor struct {
20 OccurredAt time.Time
21 ID int64
22}
23
24type AuditPageFilter struct {
25 Actor string
26 Action string
27 Subject string
28 Address netip.Addr
29 After *time.Time
30 Before *time.Time
31 Older *AuditCursor
32 Newer *AuditCursor
33 Limit int
34}
35
36type AuditPage struct {
37 Events []AuditEvent
38 Older *AuditCursor
39 Newer *AuditCursor
40}
41
42func auditPageQuery(filter AuditPageFilter, limit int) (string, []any) {
43 query := strings.Builder{}
44 query.WriteString(`SELECT id, actor, action, subject, occurred_at, detail, address FROM audit_log WHERE 1 = 1`)
45 var args []any
46 action := `action = ?`
47 if filter.Actor != "" {
48 action = `+action = ?`
49 }
50 for _, eq := range []struct{ term, value string }{
51 {`actor = ?`, filter.Actor}, {action, filter.Action}, {`subject = ?`, filter.Subject},
52 } {
53 if eq.value != "" {
54 query.WriteString(` AND ` + eq.term)
55 args = append(args, eq.value)
56 }
57 }
58 if filter.Address.IsValid() {
59 query.WriteString(` AND address = ?`)
60 args = append(args, filter.Address.Unmap().String())
61 }
62 if filter.After != nil {
63 query.WriteString(` AND occurred_at >= ?`)
64 args = append(args, timeToInt(*filter.After))
65 }
66 if filter.Before != nil {
67 query.WriteString(` AND occurred_at < ?`)
68 args = append(args, timeToInt(*filter.Before))
69 }
70 order := `DESC`
71 switch {
72 case filter.Older != nil:
73 query.WriteString(` AND (occurred_at, id) < (?, ?)`)
74 args = append(args, timeToInt(filter.Older.OccurredAt), filter.Older.ID)
75 case filter.Newer != nil:
76 query.WriteString(` AND (occurred_at, id) > (?, ?)`)
77 args = append(args, timeToInt(filter.Newer.OccurredAt), filter.Newer.ID)
78 order = `ASC`
79 }
80 query.WriteString(` ORDER BY occurred_at ` + order + `, id ` + order + ` LIMIT ?`)
81 args = append(args, limit+1)
82 return query.String(), args
83}
84
85func (s *Store) ListAuditPage(ctx context.Context, filter AuditPageFilter) (AuditPage, error) {
86 const operation = "list audit page"
87 if err := s.checkOpen(operation); err != nil {
88 return AuditPage{}, err
89 }
90 limit := filter.Limit
91 if limit == 0 {
92 limit = AuditPageSize
93 }
94 if limit < 1 || limit > maxAuditPageSize {
95 return AuditPage{}, invalid(operation, fmt.Errorf("limit must be between 1 and %d", maxAuditPageSize))
96 }
97 if filter.Older != nil && filter.Newer != nil {
98 return AuditPage{}, invalid(operation, errors.New("a page is older or newer than one cursor, not both"))
99 }
100 if filter.Address.Zone() != "" {
101 return AuditPage{}, invalid(operation, errors.New("an address filter has no zone"))
102 }
103 query, args := auditPageQuery(filter, limit)
104 rows, err := s.sql.QueryContext(ctx, query, args...)
105 if err != nil {
106 return AuditPage{}, classify(operation, err)
107 }
108 defer rows.Close()
109 var events []AuditEvent
110 for rows.Next() {
111 var e AuditEvent
112 var occurredAt int64
113 var address sql.NullString
114 if err := rows.Scan(&e.ID, &e.Actor, &e.Action, &e.Subject, &occurredAt, &e.Detail, &address); err != nil {
115 return AuditPage{}, classify(operation, err)
116 }
117 e.OccurredAt = intToTime(occurredAt)
118 e.Address = scanAddress(address)
119 events = append(events, e)
120 }
121 if err := rows.Err(); err != nil {
122 return AuditPage{}, classify(operation, err)
123 }
124 more := len(events) > limit
125 events = events[:min(len(events), limit)]
126 backward := filter.Newer != nil
127 if backward {
128 slices.Reverse(events)
129 }
130 page := AuditPage{Events: events}
131 if len(events) == 0 {
132 return page, nil
133 }
134 first, last := auditCursorOf(events[0]), auditCursorOf(events[len(events)-1])
135 if backward {
136 page.Older = &last
137 if more {
138 page.Newer = &first
139 }
140 return page, nil
141 }
142 if more {
143 page.Older = &last
144 }
145 if filter.Older != nil {
146 page.Newer = &first
147 }
148 return page, nil
149}
150
151func auditCursorOf(e AuditEvent) AuditCursor {
152 return AuditCursor{OccurredAt: e.OccurredAt, ID: e.ID}
153}