internal/control/audit.go

69d8aa4ff9115d6ca1116e09a232e7a6416e4645
gitbay/internal/control/audit.go history · blame · raw

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