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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 21m ago 4b8c9e1d
history blame raw 2.8 KiB
1package 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
9import (
10 "net/http"
11 "strconv"
12 "time"
13
14 "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.
20const 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.
24const auditMaxPage = 1_000_000
25
26type auditEntryView struct {
27 Time time.Time
28 Action string
29 Actor string
30 Subject string
31 Detail string
32}
33
34type 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// 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.
46const adminOverviewAudit = 6
47
48// adminAuditSummary is the overview's short look at the log.
49type adminAuditSummary struct {
50 Entries []auditEntryView
51 Err string
52}
53
54func (w *Web) auditPath() string {
55 if w.Cfg == nil {
56 return ""
57 }
58 return w.Cfg.AuditLog()
59}
60
61func (w *Web) auditView(r *http.Request) auditPageView {
62 v := auditPageView{Page: auditPageNumber(r.URL.Query().Get("page"))}
63 entries, older, err := audit.Tail(w.auditPath(), (v.Page-1)*auditPerPage, auditPerPage)
64 if err != nil {
65 v.Err = err.Error()
66 return v
67 }
68 v.Entries = auditEntryViews(entries)
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
80func (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
91func 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
102func 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
113func auditURL(page int) string {
114 if page <= 1 {
115 return "/-/admin?tab=audit"
116 }
117 return "/-/admin?tab=audit&page=" + strconv.Itoa(page)
118}
119