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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Blame · server/web/admin_audit.go

Line attribution on main · 118 line(s)

43e681d8
Keenan Nemetz
4d ago
1 package web
2
3 // The audit log tab: a read-only, paginated view of the instance's record of
4 // privileged actions. It shows the columns internal/audit writes and nothing
5 // else, newest first, because the file is the record and this page must not
6 // become a second one. A damaged line is skipped by the reader rather than
7 // failing the page; the file remains the authority.
8
9 import (
10 "net/http"
11 "strconv"
12 "time"
13
d37d301f
Keenan Nemetz
19h ago
14 "rodent.software/vole/internal/audit"
43e681d8
Keenan Nemetz
4d ago
15 )
16
17 // auditPerPage is how many records one page shows. An audit line is short, and
18 // the page is read for a specific action rather than skimmed, so a hundred rows
19 // is a screenful that still pages.
20 const auditPerPage = 100
21
22 // auditMaxPage bounds the page number so a hand-written query cannot overflow
23 // the offset arithmetic. A page past the end is simply empty.
24 const auditMaxPage = 1_000_000
25
26 type auditEntryView struct {
27 Time time.Time
28 Action string
29 Actor string
30 Subject string
31 Detail string
32 }
33
34 type auditPageView struct {
35 Entries []auditEntryView
36 Page int
37 HasNewer bool
38 HasOlder bool
39 NewerURL string
40 OlderURL string
41 Err string
42 }
43
0db980ee
Keenan Nemetz
4d ago
44 // adminOverviewAudit is how many recent records the overview shows before it
45 // hands the reader to the full log. It is a glance, not a second audit page.
46 const adminOverviewAudit = 6
47
48 // adminAuditSummary is the overview's short look at the log.
49 type adminAuditSummary struct {
50 Entries []auditEntryView
51 Err string
52 }
53
54 func (w *Web) auditPath() string {
55 if w.Cfg == nil {
56 return ""
57 }
58 return w.Cfg.AuditLog()
59 }
60
43e681d8
Keenan Nemetz
4d ago
61 func (w *Web) auditView(r *http.Request) auditPageView {
62 v := auditPageView{Page: auditPageNumber(r.URL.Query().Get("page"))}
0db980ee
Keenan Nemetz
4d ago
63 entries, older, err := audit.Tail(w.auditPath(), (v.Page-1)*auditPerPage, auditPerPage)
43e681d8
Keenan Nemetz
4d ago
64 if err != nil {
65 v.Err = err.Error()
66 return v
67 }
0db980ee
Keenan Nemetz
4d ago
68 v.Entries = auditEntryViews(entries)
43e681d8
Keenan Nemetz
4d ago
69 v.HasOlder = older
70 v.HasNewer = v.Page > 1
71 if v.HasNewer {
72 v.NewerURL = auditURL(v.Page - 1)
73 }
74 if v.HasOlder {
75 v.OlderURL = auditURL(v.Page + 1)
76 }
77 return v
78 }
79
0db980ee
Keenan Nemetz
4d ago
80 func (w *Web) recentAudit() adminAuditSummary {
81 var v adminAuditSummary
82 entries, _, err := audit.Tail(w.auditPath(), 0, adminOverviewAudit)
83 if err != nil {
84 v.Err = err.Error()
85 return v
86 }
87 v.Entries = auditEntryViews(entries)
88 return v
89 }
90
91 func auditEntryViews(entries []audit.Entry) []auditEntryView {
92 views := make([]auditEntryView, 0, len(entries))
93 for _, e := range entries {
94 views = append(views, auditEntryView{
95 Time: e.Time, Action: e.Action, Actor: e.Actor,
96 Subject: e.Subject, Detail: e.Detail,
97 })
98 }
99 return views
100 }
101
43e681d8
Keenan Nemetz
4d ago
102 func auditPageNumber(raw string) int {
103 n, err := strconv.Atoi(raw)
104 if err != nil || n < 1 {
105 return 1
106 }
107 if n > auditMaxPage {
108 return auditMaxPage
109 }
110 return n
111 }
112
113 func auditURL(page int) string {
114 if page <= 1 {
ed03b3d1
Keenan Nemetz
6h ago
115 return "/-/admin?tab=audit"
43e681d8
Keenan Nemetz
4d ago
116 }
ed03b3d1
Keenan Nemetz
6h ago
117 return "/-/admin?tab=audit&page=" + strconv.Itoa(page)
43e681d8
Keenan Nemetz
4d ago
118 }