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.go +100−4

@@ -1,14 +1,110 @@
1 1 package web
2 2
3 // Instance administration is split across the surfaces it belongs to: accounts
4 // live on their own Accounts page in the top navigation, and a repository's access
5 // rules are part of that repository's Settings page. An instance admin has the
6 // Admin role on every repository, so they can administer Settings there too.
3 // Instance administration is one page with a tab for each part of it: how the
4 // instance is doing, what it stores, which runners are connected, the effective
5 // configuration, the accounts roster, and the audit log. A repository's access
6 // rules stay on that repository's Settings page, because they belong to the
7 // repository; an instance admin has the Admin role there too.
7 8
8 9 import (
9 10 "net/http"
11 "time"
10 12 )
11 13
14 // adminTab is one destination in the Admin page's tab row. The key is the value
15 // of ?tab= and, apart from the default, the URL the tab links to.
16 type adminTab struct {
17 Key string
18 Label string
19 Icon string
20 }
21
22 // adminTabs is the tab order: the instance's state first, then the people, then
23 // the record of what they did.
24 var adminTabs = []adminTab{
25 {Key: "overview", Label: "Overview", Icon: "layout-dashboard"},
26 {Key: "storage", Label: "Storage", Icon: "hard-drive"},
27 {Key: "runners", Label: "Runners", Icon: "server"},
28 {Key: "configuration", Label: "Configuration", Icon: "sliders-horizontal"},
29 {Key: "accounts", Label: "Accounts", Icon: "users"},
30 {Key: "audit", Label: "Audit log", Icon: "history"},
31 }
32
33 func adminTabFrom(raw string) string {
34 for _, t := range adminTabs {
35 if t.Key == raw {
36 return t.Key
37 }
38 }
39 return "overview"
40 }
41
42 // adminPageView is the whole Admin page. Every tab reads the fields it needs and
43 // leaves the rest at zero, so one template can own the header, the tab row and
44 // the footer while each tab stays a section of its own.
45 type adminPageView struct {
46 Page
47 Tab string
48 Tabs []adminTab
49
50 // Overview, storage, runners, and configuration.
51 Accounts int
52 Repos int
53 Queued int
54 Running int
55 Runners int
56 Started time.Time
57 Storage statusStorageView
58 LFS statusLFSView
59 Views statusViewsView
60 RunnerErr string
61 RunnerSessions []runnerView
62 RunnerQueue []queueView
63 RunnerWindow string
64 Config *statusConfigView
65
66 // Accounts.
67 Users []adminUserRow
68
69 // Audit log.
70 Audit auditPageView
71 }
72
73 // AdminPage is the instance-admin surface. The gate runs before anything is
74 // read, so a non-admin cannot use the tab a request names to learn what exists.
75 func (w *Web) AdminPage(rw http.ResponseWriter, r *http.Request) {
76 if _, ok := w.requireInstanceAdmin(rw, r); !ok {
77 return
78 }
79 tab := adminTabFrom(r.URL.Query().Get("tab"))
80 if tab == "audit" {
81 w.noStore(rw)
82 }
83 w.render(rw, "admin", w.adminView(r, tab, ""), 0)
84 }
85
86 // adminView fills the fields the current tab renders. A message is the result of
87 // a form POST that failed on this tab.
88 func (w *Web) adminView(r *http.Request, tab, msg string) adminPageView {
89 v := adminPageView{Page: w.page(r, "Admin"), Tab: tab, Tabs: adminTabs, Started: w.Started}
90 v.Page.Error = msg
91 switch tab {
92 case "accounts":
93 v.Users = w.adminUserRows()
94 case "audit":
95 v.Audit = w.auditView(r)
96 default:
97 w.fillStatusView(&v)
98 }
99 return v
100 }
101
102 // renderAdminTab answers a write that failed on a tab. It renders the page the
103 // form came from with the error, rather than redirecting and losing the message.
104 func (w *Web) renderAdminTab(rw http.ResponseWriter, r *http.Request, tab, msg string, status int) {
105 w.render(rw, "admin", w.adminView(r, tab, msg), status)
106 }
107
12 108 // requireInstanceAdmin redirects an anonymous visitor to login and hides the
13 109 // admin surface from an authenticated non-admin.
14 110 func (w *Web) requireInstanceAdmin(rw http.ResponseWriter, r *http.Request) (*principal, bool) {