Raw
  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}