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.go

Line attribution on main · 188 line(s)

2973f727
Keenan Nemetz
15d ago
1 package web
2
43e681d8
Keenan Nemetz
4d ago
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.
2973f727
Keenan Nemetz
15d ago
8
9 import (
10 "net/http"
9a2a270b
Keenan Nemetz
4d ago
11 "strings"
43e681d8
Keenan Nemetz
4d ago
12 "time"
2973f727
Keenan Nemetz
15d ago
13 )
14
43e681d8
Keenan Nemetz
4d ago
15 // adminTab is one destination in the Admin page's tab row. The key is the value
16 // of ?tab= and, apart from the default, the URL the tab links to.
17 type adminTab struct {
18 Key string
19 Label string
20 Icon string
21 }
22
23 // adminTabs is the tab order: the instance's state first, then the people, then
24 // the record of what they did.
25 var adminTabs = []adminTab{
26 {Key: "overview", Label: "Overview", Icon: "layout-dashboard"},
27 {Key: "storage", Label: "Storage", Icon: "hard-drive"},
28 {Key: "runners", Label: "Runners", Icon: "server"},
29 {Key: "configuration", Label: "Configuration", Icon: "sliders-horizontal"},
30 {Key: "accounts", Label: "Accounts", Icon: "users"},
31 {Key: "audit", Label: "Audit log", Icon: "history"},
32 }
33
34 func adminTabFrom(raw string) string {
35 for _, t := range adminTabs {
36 if t.Key == raw {
37 return t.Key
38 }
39 }
40 return "overview"
41 }
42
43 // adminPageView is the whole Admin page. Every tab reads the fields it needs and
44 // leaves the rest at zero, so one template can own the header, the tab row and
45 // the footer while each tab stays a section of its own.
46 type adminPageView struct {
47 Page
48 Tab string
49 Tabs []adminTab
50
6ce635a8
Keenan Nemetz
1d ago
51 // Overview, storage, runners, configuration, and landing page.
43e681d8
Keenan Nemetz
4d ago
52 Accounts int
53 Repos int
54 Started time.Time
55 Storage statusStorageView
56 LFS statusLFSView
57 Views statusViewsView
58 RunnerErr string
59 RunnerSessions []runnerView
60 RunnerQueue []queueView
61 RunnerWindow string
62 Config *statusConfigView
63
6ce635a8
Keenan Nemetz
1d ago
64 // InstanceRepo is the Configuration tab's Instance repository panel.
65 InstanceRepo instanceRepoView
66
ea21f923
Keenan Nemetz
4d ago
67 // AccountSummary is the overview's counts, derived from the roster the users
68 // directory lists.
0db980ee
Keenan Nemetz
4d ago
69 AccountSummary adminAccountSummary
43e681d8
Keenan Nemetz
4d ago
70
71 // Audit log.
0db980ee
Keenan Nemetz
4d ago
72 Audit auditPageView
73 RecentAudit adminAuditSummary
74
75 // Configuration attention: the warnings from the configuration tab, repeated
76 // on the overview so a development setting is visible without a click.
77 ConfigWarnings []statusSetting
78 }
79
80 // adminAccountSummary is the roster at a glance: who can act, and who has been
81 // retired. A retired account keeps its name, so it is counted separately rather
82 // than hidden.
83 type adminAccountSummary struct {
84 People int
85 Bots int
86 Admins int
87 Retired int
88 }
89
90 func (w *Web) accountSummary() adminAccountSummary {
91 var s adminAccountSummary
92 if w.Ident == nil {
93 return s
94 }
95 for _, a := range w.Ident.All() {
96 if !a.Active() {
97 s.Retired++
98 continue
99 }
100 if a.Bot() {
101 s.Bots++
102 } else {
103 s.People++
104 }
105 if a.Admin {
106 s.Admins++
107 }
108 }
109 return s
43e681d8
Keenan Nemetz
4d ago
110 }
111
9a2a270b
Keenan Nemetz
4d ago
112 // UsersNote is the Users card's one-line breakdown. Zero counts are left out,
113 // so a clean instance reads as "3 people" rather than a row of zeros, and each
114 // number is spelled for its count.
115 func (s adminAccountSummary) UsersNote() string {
116 parts := []string{countLabel(s.People, "person", "people")}
117 if s.Bots > 0 {
118 parts = append(parts, countLabel(s.Bots, "bot", "bots"))
119 }
120 if s.Admins > 0 {
121 parts = append(parts, countLabel(s.Admins, "admin", "admins"))
122 }
123 if s.Retired > 0 {
124 parts = append(parts, countLabel(s.Retired, "retired", "retired"))
125 }
126 return strings.Join(parts, " · ")
127 }
128
43e681d8
Keenan Nemetz
4d ago
129 // AdminPage is the instance-admin surface. The gate runs before anything is
130 // read, so a non-admin cannot use the tab a request names to learn what exists.
131 func (w *Web) AdminPage(rw http.ResponseWriter, r *http.Request) {
132 if _, ok := w.requireInstanceAdmin(rw, r); !ok {
133 return
134 }
135 tab := adminTabFrom(r.URL.Query().Get("tab"))
136 if tab == "audit" {
137 w.noStore(rw)
138 }
139 w.render(rw, "admin", w.adminView(r, tab, ""), 0)
140 }
141
142 // adminView fills the fields the current tab renders. A message is the result of
143 // a form POST that failed on this tab.
144 func (w *Web) adminView(r *http.Request, tab, msg string) adminPageView {
145 v := adminPageView{Page: w.page(r, "Admin"), Tab: tab, Tabs: adminTabs, Started: w.Started}
146 v.Page.Error = msg
147 switch tab {
148 case "accounts":
ea21f923
Keenan Nemetz
4d ago
149 // The tab is the creation forms; an existing account is managed from the
150 // users directory, which is already paginated and searchable.
43e681d8
Keenan Nemetz
4d ago
151 case "audit":
152 v.Audit = w.auditView(r)
153 default:
6ce635a8
Keenan Nemetz
1d ago
154 w.fillStatusView(r, &v)
43e681d8
Keenan Nemetz
4d ago
155 }
156 return v
157 }
158
159 // renderAdminTab answers a write that failed on a tab. It renders the page the
160 // form came from with the error, rather than redirecting and losing the message.
161 func (w *Web) renderAdminTab(rw http.ResponseWriter, r *http.Request, tab, msg string, status int) {
162 w.render(rw, "admin", w.adminView(r, tab, msg), status)
163 }
164
2973f727
Keenan Nemetz
15d ago
165 // requireInstanceAdmin redirects an anonymous visitor to login and hides the
166 // admin surface from an authenticated non-admin.
167 func (w *Web) requireInstanceAdmin(rw http.ResponseWriter, r *http.Request) (*principal, bool) {
168 pr, ok := w.requireHTMLUser(rw, r)
169 if !ok {
170 return nil, false
171 }
62576fa8
Keenan Nemetz
14d ago
172 if w.Ident == nil || !w.Ident.IsAdmin(pr.account.Name) {
2973f727
Keenan Nemetz
15d ago
173 w.notFound(rw, r)
174 return nil, false
175 }
176 return pr, true
177 }
178
5b814e99
Keenan Nemetz
14d ago
179 func (w *Web) accountStatus(name string) string {
180 if w.Ident == nil {
181 return "missing"
182 }
183 acct, err := w.Ident.Find(name)
184 if err != nil {
185 return "missing"
186 }
187 return acct.Status
188 }