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

Line attribution on main · 229 line(s)

395554f4
Keenan Nemetz
15d ago
1 package web
2
2e024cc9
Keenan Nemetz
14d ago
3 // Instance admin accounts: the operations that used to need `vole admin account`.
395554f4
Keenan Nemetz
15d ago
4 // Account creation goes through CreateWithInvite, so the browser never asks an
5 // operator to paste somebody else's SSH key; the one-time enrollment link is the
6 // credential and is shown once.
7
8 import (
9 "net/http"
10 "strings"
11 "time"
12
d37d301f
Keenan Nemetz
19h ago
13 "rodent.software/vole/forge/access"
14 "rodent.software/vole/forge/identity"
15 "rodent.software/vole/forge/session"
395554f4
Keenan Nemetz
15d ago
16 )
17
18 type adminUserView struct {
19 Page
20 Account *identity.Account
21 Sessions []session.Session
8c72543b
Keenan Nemetz
5d ago
22 // RoleScope and RoleHelp keep the role picker honest for a bot: it may hold
23 // an instance role, but not the instance-admin role.
24 RoleScope string
25 RoleHelp string
26 Error string
395554f4
Keenan Nemetz
15d ago
27 }
28
29 type adminInviteView struct {
30 Page
31 Account *identity.Account
32 Link string
33 Expires time.Time
34 BrowserLogin bool
35 }
36
37 func (w *Web) adminUserView(r *http.Request, acct *identity.Account, msg string) adminUserView {
8c72543b
Keenan Nemetz
5d ago
38 v := adminUserView{
39 Page: w.page(r, acct.Name), Account: acct, Error: msg,
40 RoleScope: "instance",
41 RoleHelp: "Instance roles apply to public repositories. A repository-specific role can grant more on one repository. admin is the exception: it applies to every repository, private ones included.",
42 }
43 if acct.Bot() {
44 v.RoleScope = "bot"
45 v.RoleHelp = "Instance roles apply to public repositories. A repository-specific role can grant more on one repository. A bot cannot be an instance admin."
46 }
395554f4
Keenan Nemetz
15d ago
47 if w.Sessions != nil {
62576fa8
Keenan Nemetz
14d ago
48 v.Sessions, _ = w.Sessions.List(acct.Name)
395554f4
Keenan Nemetz
15d ago
49 }
50 return v
51 }
52
53 func (w *Web) adminInviteView(r *http.Request, acct *identity.Account, inv *identity.Invite) adminInviteView {
edf9db6e
Keenan Nemetz
13d ago
54 // The token is in the fragment, so it is not sent to the server, not written to
55 // an access log in front of it, and not carried in a Referer when the page
56 // links anywhere.
ed03b3d1
Keenan Nemetz
6h ago
57 link := "/-/invite#" + inv.Token
395554f4
Keenan Nemetz
15d ago
58 if base := w.Cfg.BaseURL(); base != "" {
59 link = base + link
60 }
61 return adminInviteView{
ab326d3a
Keenan Nemetz
14d ago
62 Page: w.page(r, "Enrollment link for "+acct.Name), Account: acct,
395554f4
Keenan Nemetz
15d ago
63 Link: link, Expires: inv.Expires, BrowserLogin: w.enabled(),
64 }
65 }
66
67 // adminUserPost is the shared gate for the action endpoints: instance admin,
68 // existing account, CSRF, and a parsed form.
69 func (w *Web) adminUserPost(rw http.ResponseWriter, r *http.Request) (*identity.Account, *principal, bool) {
c717d4eb
Keenan Nemetz
13d ago
70 pr, ok := w.beginAdminPost(rw, r, maxSmallForm)
395554f4
Keenan Nemetz
15d ago
71 if !ok {
72 return nil, nil, false
73 }
62576fa8
Keenan Nemetz
14d ago
74 acct, err := w.Ident.Find(r.PathValue("name"))
395554f4
Keenan Nemetz
15d ago
75 if err != nil {
76 w.notFound(rw, r)
77 return nil, nil, false
78 }
79 return acct, pr, true
80 }
81
43e681d8
Keenan Nemetz
4d ago
82 // AdminUsersPage keeps the old roster URL working. The roster is the Accounts tab
83 // on the Admin page now; the gate still runs here so an anonymous visitor is sent
84 // to login and a non-admin still gets the same 404 a missing page gets.
395554f4
Keenan Nemetz
15d ago
85 func (w *Web) AdminUsersPage(rw http.ResponseWriter, r *http.Request) {
86 if _, ok := w.requireInstanceAdmin(rw, r); !ok {
87 return
88 }
ed03b3d1
Keenan Nemetz
6h ago
89 http.Redirect(rw, r, "/-/admin?tab=accounts", http.StatusMovedPermanently)
395554f4
Keenan Nemetz
15d ago
90 }
91
92 func (w *Web) CreateAdminUser(rw http.ResponseWriter, r *http.Request) {
c717d4eb
Keenan Nemetz
13d ago
93 if _, ok := w.beginAdminPost(rw, r, maxSmallForm); !ok {
395554f4
Keenan Nemetz
15d ago
94 return
95 }
96 name := strings.TrimSpace(r.PostFormValue("name"))
5b814e99
Keenan Nemetz
14d ago
97 role, err := access.ParseRole(r.PostFormValue("role"))
98 if err != nil {
43e681d8
Keenan Nemetz
4d ago
99 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
5b814e99
Keenan Nemetz
14d ago
100 return
101 }
102 acct, inv, err := w.Ident.CreateWithInviteRole(name, role, w.accountName(r), 0)
395554f4
Keenan Nemetz
15d ago
103 if err != nil {
43e681d8
Keenan Nemetz
4d ago
104 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
395554f4
Keenan Nemetz
15d ago
105 return
106 }
edf9db6e
Keenan Nemetz
13d ago
107 w.noStore(rw)
395554f4
Keenan Nemetz
15d ago
108 w.render(rw, "admin_invite", w.adminInviteView(r, acct, inv), 0)
109 }
110
8c72543b
Keenan Nemetz
5d ago
111 // CreateAdminBot creates an automation account. A bot has no browser sign-in, so
112 // an enrollment link would be a secret it could never use: the form collects the
113 // SSH key it acts with instead. The identity layer records the same audit line
114 // the CLI does, with the kind and the role.
115 func (w *Web) CreateAdminBot(rw http.ResponseWriter, r *http.Request) {
116 pr, ok := w.beginAdminPost(rw, r, maxSmallForm)
117 if !ok {
118 return
119 }
120 name := strings.TrimSpace(r.PostFormValue("name"))
121 role, err := access.ParseRole(r.PostFormValue("role"))
122 if err != nil {
43e681d8
Keenan Nemetz
4d ago
123 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
8c72543b
Keenan Nemetz
5d ago
124 return
125 }
126 key := strings.TrimSpace(r.PostFormValue("key"))
127 if key == "" {
43e681d8
Keenan Nemetz
4d ago
128 w.renderAdminTab(rw, r, "accounts", "Paste a public key for the bot. A bot signs in with a key, not a browser.", http.StatusBadRequest)
8c72543b
Keenan Nemetz
5d ago
129 return
130 }
131 acct, err := w.Ident.CreateBot(name, role, key, pr.account.Name)
132 if err != nil {
43e681d8
Keenan Nemetz
4d ago
133 w.renderAdminTab(rw, r, "accounts", err.Error(), http.StatusBadRequest)
8c72543b
Keenan Nemetz
5d ago
134 return
135 }
ed03b3d1
Keenan Nemetz
6h ago
136 http.Redirect(rw, r, "/-/users/"+acct.Name, http.StatusSeeOther)
8c72543b
Keenan Nemetz
5d ago
137 }
138
395554f4
Keenan Nemetz
15d ago
139 func (w *Web) AdminUserPage(rw http.ResponseWriter, r *http.Request) {
140 if _, ok := w.requireInstanceAdmin(rw, r); !ok {
141 return
142 }
62576fa8
Keenan Nemetz
14d ago
143 acct, err := w.Ident.Find(r.PathValue("name"))
395554f4
Keenan Nemetz
15d ago
144 if err != nil {
145 w.notFound(rw, r)
146 return
147 }
148 w.render(rw, "admin_user", w.adminUserView(r, acct, ""), 0)
149 }
150
5b814e99
Keenan Nemetz
14d ago
151 func (w *Web) SetAdminUserRole(rw http.ResponseWriter, r *http.Request) {
395554f4
Keenan Nemetz
15d ago
152 acct, pr, ok := w.adminUserPost(rw, r)
153 if !ok {
154 return
155 }
5b814e99
Keenan Nemetz
14d ago
156 role, err := access.ParseRole(r.PostFormValue("role"))
157 if err != nil {
158 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
159 return
160 }
161 if _, err := w.Ident.SetInstanceRole(acct.Name, role, pr.account.Name); err != nil {
395554f4
Keenan Nemetz
15d ago
162 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
163 return
164 }
ed03b3d1
Keenan Nemetz
6h ago
165 http.Redirect(rw, r, "/-/users/"+acct.Name, http.StatusSeeOther)
395554f4
Keenan Nemetz
15d ago
166 }
167
168 func (w *Web) RetireAdminUser(rw http.ResponseWriter, r *http.Request) {
169 acct, pr, ok := w.adminUserPost(rw, r)
170 if !ok {
171 return
172 }
173 if got := strings.TrimSpace(r.PostFormValue("confirm")); got != acct.Name {
174 w.render(rw, "admin_user", w.adminUserView(r, acct, "Type the account name exactly to confirm retirement."), http.StatusBadRequest)
175 return
176 }
62576fa8
Keenan Nemetz
14d ago
177 if _, err := w.Ident.Retire(acct.Name, pr.account.Name); err != nil {
395554f4
Keenan Nemetz
15d ago
178 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
179 return
180 }
ed03b3d1
Keenan Nemetz
6h ago
181 http.Redirect(rw, r, "/-/admin?tab=accounts", http.StatusSeeOther)
395554f4
Keenan Nemetz
15d ago
182 }
183
184 func (w *Web) InviteAdminUser(rw http.ResponseWriter, r *http.Request) {
185 acct, pr, ok := w.adminUserPost(rw, r)
186 if !ok {
187 return
188 }
62576fa8
Keenan Nemetz
14d ago
189 inv, err := w.Ident.Invite(acct.Name, pr.account.Name, 0)
395554f4
Keenan Nemetz
15d ago
190 if err != nil {
191 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
192 return
193 }
edf9db6e
Keenan Nemetz
13d ago
194 w.noStore(rw)
395554f4
Keenan Nemetz
15d ago
195 w.render(rw, "admin_invite", w.adminInviteView(r, acct, inv), 0)
196 }
197
198 func (w *Web) RevokeAdminUserSessions(rw http.ResponseWriter, r *http.Request) {
199 acct, _, ok := w.adminUserPost(rw, r)
200 if !ok {
201 return
202 }
203 if w.Sessions == nil {
204 w.serverError(rw, r, "sessions are unavailable")
205 return
206 }
62576fa8
Keenan Nemetz
14d ago
207 if _, err := w.Sessions.RevokeAccount(acct.Name); err != nil {
395554f4
Keenan Nemetz
15d ago
208 w.serverError(rw, r, "cannot revoke sessions: "+err.Error())
209 return
210 }
ed03b3d1
Keenan Nemetz
6h ago
211 http.Redirect(rw, r, "/-/users/"+acct.Name, http.StatusSeeOther)
395554f4
Keenan Nemetz
15d ago
212 }
213
214 func (w *Web) RemoveAdminUserKey(rw http.ResponseWriter, r *http.Request) {
215 acct, pr, ok := w.adminUserPost(rw, r)
216 if !ok {
217 return
218 }
219 fingerprint := strings.TrimSpace(r.PostFormValue("fingerprint"))
220 if fingerprint == "" {
221 w.render(rw, "admin_user", w.adminUserView(r, acct, "Choose a key to remove."), http.StatusBadRequest)
222 return
223 }
62576fa8
Keenan Nemetz
14d ago
224 if err := w.Ident.RemoveKey(acct.Name, fingerprint, pr.account.Name); err != nil {
395554f4
Keenan Nemetz
15d ago
225 w.render(rw, "admin_user", w.adminUserView(r, acct, err.Error()), http.StatusBadRequest)
226 return
227 }
ed03b3d1
Keenan Nemetz
6h ago
228 http.Redirect(rw, r, "/-/users/"+acct.Name, http.StatusSeeOther)
395554f4
Keenan Nemetz
15d ago
229 }