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) { | |