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.

Add one Admin page with status, accounts, and the audit log

43e681d8a5d94567aa9530d32b2c89223c1a17f9 · Keenan Nemetz · 5d ago · parent af018ce8

Add one Admin page with status, accounts, and the audit log

Instance administration was split between the Status and Accounts pages,
and the audit log had no browser view at all. The new /admin page gathers
the status tabs, the account roster, and a read-only audit viewer into one
place with a tab for each, and /status, /runners, and /users redirect to
the tab they named so old links keep working.

The audit reader walks the file backwards a chunk at a time, returns the
newest records first, and shows one extra record to know whether an older
page exists. It skips a line it cannot parse rather than failing the page
and refuses a symlink or a non-regular file, as startup already does.

Refs #198 (01M6MK0NNZ2XR1D5TQKNWYT4HN).

server/web/admin_audit.go +88

@@ -0,0 +1,88 @@
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
14 "github.com/rodent-software/vole/internal/audit"
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
44 func (w *Web) auditView(r *http.Request) auditPageView {
45 v := auditPageView{Page: auditPageNumber(r.URL.Query().Get("page"))}
46 path := ""
47 if w.Cfg != nil {
48 path = w.Cfg.AuditLog()
49 }
50 entries, older, err := audit.Tail(path, (v.Page-1)*auditPerPage, auditPerPage)
51 if err != nil {
52 v.Err = err.Error()
53 return v
54 }
55 for _, e := range entries {
56 v.Entries = append(v.Entries, auditEntryView{
57 Time: e.Time, Action: e.Action, Actor: e.Actor,
58 Subject: e.Subject, Detail: e.Detail,
59 })
60 }
61 v.HasOlder = older
62 v.HasNewer = v.Page > 1
63 if v.HasNewer {
64 v.NewerURL = auditURL(v.Page - 1)
65 }
66 if v.HasOlder {
67 v.OlderURL = auditURL(v.Page + 1)
68 }
69 return v
70 }
71
72 func auditPageNumber(raw string) int {
73 n, err := strconv.Atoi(raw)
74 if err != nil || n < 1 {
75 return 1
76 }
77 if n > auditMaxPage {
78 return auditMaxPage
79 }
80 return n
81 }
82
83 func auditURL(page int) string {
84 if page <= 1 {
85 return "/admin?tab=audit"
86 }
87 return "/admin?tab=audit&page=" + strconv.Itoa(page)
88 }