Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/audit_test.go

Line attribution on main · 86 line(s)

b1499c72
Keenan Nemetz
12d ago
1 package web
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
d37d301f
Keenan Nemetz
15h ago
10 "rodent.software/vole/forge/config"
b1499c72
Keenan Nemetz
12d ago
11 )
12
13 // auditWeb returns a Web whose only wired parts are the ones the audit log
14 // needs: a data directory and a collector for the server log.
15 func auditWeb(t *testing.T) (*Web, *[]string) {
16 t.Helper()
17 cfg, err := config.Load("")
18 if err != nil {
19 t.Fatal(err)
20 }
21 cfg.DataDir = t.TempDir()
22 var lines []string
23 w := &Web{Cfg: cfg}
24 w.Logger = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) }
25 return w, &lines
26 }
27
28 func TestAuditWritesOneRecordInFiveColumns(t *testing.T) {
29 w, lines := auditWeb(t)
30 w.audit("web.repo.delete", "keenan", "demo", "")
31 if len(*lines) != 0 {
32 t.Fatalf("a successful write logged an error: %v", *lines)
33 }
34 raw, err := os.ReadFile(w.Cfg.AuditLog())
35 if err != nil {
36 t.Fatal(err)
37 }
38 cols := strings.Split(strings.TrimSuffix(string(raw), "\n"), "\t")
39 if len(cols) != 5 {
40 t.Fatalf("got %d columns, want 5: %q", len(cols), raw)
41 }
42 if cols[1] != "web.repo.delete" || cols[2] != "keenan" || cols[3] != "demo" || cols[4] != "" {
43 t.Fatalf("record = %q", raw)
44 }
45 }
46
47 // A repository name, a branch name and a commit message reach this code straight
48 // from a form. None of them may forge a column or a second record.
49 func TestAuditEscapesInjectedFields(t *testing.T) {
50 w, _ := auditWeb(t)
51 evil := "demo\tadmin\troot\n2026-01-01T00:00:00Z\tweb.admin.acl\troot"
52 w.audit("web.issue.create", evil, evil, evil)
53 raw, err := os.ReadFile(w.Cfg.AuditLog())
54 if err != nil {
55 t.Fatal(err)
56 }
57 if n := strings.Count(string(raw), "\n"); n != 1 {
58 t.Fatalf("injected field wrote %d lines: %q", n, raw)
59 }
60 if n := len(strings.Split(strings.TrimSuffix(string(raw), "\n"), "\t")); n != 5 {
61 t.Fatalf("injected field wrote %d columns: %q", n, raw)
62 }
63 }
64
65 // The web cannot undo an action it has already taken, so a log that cannot be
66 // written has to be loud. It must never look like success to an operator.
67 func TestAuditReportsWriteFailure(t *testing.T) {
68 w, lines := auditWeb(t)
69 blocker := filepath.Join(t.TempDir(), "blocked")
70 if err := os.WriteFile(blocker, []byte("x"), 0o600); err != nil {
71 t.Fatal(err)
72 }
73 if err := os.RemoveAll(w.Cfg.AuditLog()); err != nil {
74 t.Fatal(err)
75 }
76 if err := os.Symlink(filepath.Join(blocker, "audit.log"), w.Cfg.AuditLog()); err != nil {
77 t.Fatalf("symlink in place of the log: %v", err)
78 }
79 w.audit("web.repo.delete", "keenan", "demo", "")
80 if len(*lines) != 1 || !strings.Contains((*lines)[0], "audit log write failed") {
81 t.Fatalf("the failure was not reported: %v", *lines)
82 }
83 if !strings.Contains((*lines)[0], "web.repo.delete") {
84 t.Fatalf("the failure does not name the action: %v", *lines)
85 }
86 }