internal/control/audit.go

dbec077010394f2955c9b20cfd81700ef6b2bdb9
gitbay/internal/control/audit.go history · blame · raw

80 lines · 2413 bytes

 1package control
 2
 3import (
 4	"fmt"
 5	"io"
 6	"strconv"
 7	"strings"
 8	"time"
 9
10	"gitbay.org/gitbay/internal/protocol"
11	"gitbay.org/gitbay/internal/store"
12)
13
14func init() {
15	register(Command{Path: []string{"audit"},
16		Summary:  "instance audit log (admins)",
17		Usage:    "audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]",
18		ReadOnly: true, SSHOnly: true, Run: runAudit})
19}
20
21const auditUsage = "usage: audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]"
22
23func runAudit(c *Ctx, args []string) int {
24	if !c.User.IsAdmin {
25		return c.fail(protocol.ExitDenied, "the audit log is for instance admins")
26	}
27	f := store.AuditFilter{Limit: 100}
28	fl, err := parseFlags(args, flagSpec{Values: []string{"--limit", "--actor", "--action", "--since"}, MaxPos: 0, Usage: auditUsage})
29	if err != nil {
30		return c.fail(protocol.ExitUsage, "%v", err)
31	}
32	if fl.Has("--limit") {
33		n, err := strconv.Atoi(fl.Value("--limit"))
34		if err != nil || n < 1 || n > 10000 {
35			return c.fail(protocol.ExitUsage, "--limit must be 1 to 10000")
36		}
37		f.Limit = n
38	}
39	f.Actor, f.ActionPrefix = fl.Value("--actor"), fl.Value("--action")
40	if fl.Has("--since") {
41		t, ok := parseSince(fl.Value("--since"), time.Now())
42		if !ok {
43			return c.fail(protocol.ExitUsage, "--since takes a duration (30m, 24h, 7d) or a date (2026-09-01, RFC 3339)")
44		}
45		f.Since = t.UTC().Format("2006-01-02T15:04:05.000Z")
46	}
47	entries, err := c.Store.AuditEntries(f)
48	if err != nil {
49		return c.fail(protocol.ExitFailure, "%v", err)
50	}
51	return c.emit(entries, func(w io.Writer) {
52		for _, e := range entries {
53			actor := e.Actor
54			if actor == "" {
55				actor = "-"
56			}
57			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.CreatedAt, actor, e.Action, e.Data)
58		}
59	})
60}
61
62// parseSince reads --since as a duration back from now (with a d suffix
63// for days, which time.ParseDuration lacks) or as a date or RFC 3339
64// timestamp.
65func parseSince(v string, now time.Time) (time.Time, bool) {
66	if strings.HasSuffix(v, "d") {
67		if n, err := strconv.Atoi(strings.TrimSuffix(v, "d")); err == nil && n >= 0 {
68			return now.Add(-time.Duration(n) * 24 * time.Hour), true
69		}
70	}
71	if d, err := time.ParseDuration(v); err == nil && d >= 0 {
72		return now.Add(-d), true
73	}
74	for _, layout := range []string{time.RFC3339, "2006-01-02"} {
75		if t, err := time.Parse(layout, v); err == nil {
76			return t, true
77		}
78	}
79	return time.Time{}, false
80}