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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 25m ago 32290114
history blame raw 5.4 KiB
1package web
2
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.
8
9import (
10 "net/http"
11 "strings"
12 "time"
13)
14
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.
17type 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.
25var 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
34func 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.
46type adminPageView struct {
47 Page
48 Tab string
49 Tabs []adminTab
50
51 // Overview, storage, runners, configuration, and landing page.
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
64 // InstanceRepo is the Configuration tab's Instance repository panel.
65 InstanceRepo instanceRepoView
66
67 // AccountSummary is the overview's counts, derived from the roster the users
68 // directory lists.
69 AccountSummary adminAccountSummary
70
71 // Audit log.
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.
83type adminAccountSummary struct {
84 People int
85 Bots int
86 Admins int
87 Retired int
88}
89
90func (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
110}
111
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.
115func (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
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.
131func (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.
144func (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":
149 // The tab is the creation forms; an existing account is managed from the
150 // users directory, which is already paginated and searchable.
151 case "audit":
152 v.Audit = w.auditView(r)
153 default:
154 w.fillStatusView(r, &v)
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.
161func (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
165// requireInstanceAdmin redirects an anonymous visitor to login and hides the
166// admin surface from an authenticated non-admin.
167func (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 }
172 if w.Ident == nil || !w.Ident.IsAdmin(pr.account.Name) {
173 w.notFound(rw, r)
174 return nil, false
175 }
176 return pr, true
177}
178
179func (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}
189