internal/control/audit.go
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}