internal/control/audit.go

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

88 lines · 2395 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	for i := 0; i < len(args); i++ {
29		if i+1 >= len(args) {
30			return c.fail(protocol.ExitUsage, auditUsage)
31		}
32		v := args[i+1]
33		switch args[i] {
34		case "--limit":
35			n, err := strconv.Atoi(v)
36			if err != nil || n < 1 || n > 10000 {
37				return c.fail(protocol.ExitUsage, "--limit must be 1 to 10000")
38			}
39			f.Limit = n
40		case "--actor":
41			f.Actor = v
42		case "--action":
43			f.ActionPrefix = v
44		case "--since":
45			t, ok := parseSince(v, time.Now())
46			if !ok {
47				return c.fail(protocol.ExitUsage, "--since takes a duration (30m, 24h, 7d) or a date (2026-09-01, RFC 3339)")
48			}
49			f.Since = t.UTC().Format("2006-01-02T15:04:05.000Z")
50		default:
51			return c.fail(protocol.ExitUsage, auditUsage)
52		}
53		i++
54	}
55	entries, err := c.Store.AuditEntries(f)
56	if err != nil {
57		return c.fail(protocol.ExitFailure, "%v", err)
58	}
59	return c.emit(entries, func(w io.Writer) {
60		for _, e := range entries {
61			actor := e.Actor
62			if actor == "" {
63				actor = "-"
64			}
65			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.CreatedAt, actor, e.Action, e.Data)
66		}
67	})
68}
69
70// parseSince reads --since as a duration back from now (with a d suffix
71// for days, which time.ParseDuration lacks) or as a date or RFC 3339
72// timestamp.
73func parseSince(v string, now time.Time) (time.Time, bool) {
74	if strings.HasSuffix(v, "d") {
75		if n, err := strconv.Atoi(strings.TrimSuffix(v, "d")); err == nil && n >= 0 {
76			return now.Add(-time.Duration(n) * 24 * time.Hour), true
77		}
78	}
79	if d, err := time.ParseDuration(v); err == nil && d >= 0 {
80		return now.Add(-d), true
81	}
82	for _, layout := range []string{time.RFC3339, "2006-01-02"} {
83		if t, err := time.Parse(layout, v); err == nil {
84			return t, true
85		}
86	}
87	return time.Time{}, false
88}