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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/account.go

Line attribution on main · 124 line(s)

f920a9f1
Keenan Nemetz
15d ago
1 package web
2
3 import (
7d7718fa
Keenan Nemetz
1h ago
4 "errors"
f920a9f1
Keenan Nemetz
15d ago
5 "net/http"
6 "strings"
7d7718fa
Keenan Nemetz
1h ago
7
8 "rodent.software/vole/forge/identity"
f920a9f1
Keenan Nemetz
15d ago
9 )
10
11 // AddAccountKey lets a signed-in person attach an SSH public key without an
ab326d3a
Keenan Nemetz
14d ago
12 // instance admin. Open registration is only useful for Git contributions if the
f920a9f1
Keenan Nemetz
15d ago
13 // account can turn its browser session into a key that pushes.
14 func (w *Web) AddAccountKey(rw http.ResponseWriter, r *http.Request) {
c717d4eb
Keenan Nemetz
13d ago
15 pr, ok := w.beginPost(rw, r, maxTinyForm)
f920a9f1
Keenan Nemetz
15d ago
16 if !ok {
17 return
18 }
19 key := strings.TrimSpace(r.PostFormValue("key"))
20 if key == "" {
21 http.Error(rw, "paste an SSH public key", http.StatusBadRequest)
22 return
23 }
62576fa8
Keenan Nemetz
14d ago
24 fp, err := w.Ident.AddKey(pr.account.Name, key, pr.account.Name)
f920a9f1
Keenan Nemetz
15d ago
25 if err != nil {
26 msg := err.Error()
27 if strings.Contains(msg, "already registered") {
28 // Do not turn a known public key into a lookup of somebody else's name.
29 msg = "that SSH key is already registered"
30 }
31 http.Error(rw, msg, http.StatusBadRequest)
32 return
33 }
b1499c72
Keenan Nemetz
13d ago
34 w.audit("web.key.add", pr.account.Name, "", fp)
ed03b3d1
Keenan Nemetz
3h ago
35 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
f920a9f1
Keenan Nemetz
15d ago
36 }
37
38 // RemoveAccountKey detaches one of the signed-in account's own keys. It is the
ab326d3a
Keenan Nemetz
14d ago
39 // same operation an instance admin can perform, scoped by the session account.
7d7718fa
Keenan Nemetz
1h ago
40 // RenameAccountPasskey relabels one of the signed-in account's own passkeys. A label
41 // is display text: the credential id is what signs in, so this is a small update and
42 // not a re-registration.
43 func (w *Web) RenameAccountPasskey(rw http.ResponseWriter, r *http.Request) {
44 pr, ok := w.beginPost(rw, r, maxTinyForm)
45 if !ok {
46 return
47 }
48 id, err := identity.ParseCredentialID(strings.TrimSpace(r.PostFormValue("credential")))
49 if err != nil {
50 http.Error(rw, "choose a passkey", http.StatusBadRequest)
51 return
52 }
53 label := strings.TrimSpace(r.PostFormValue("label"))
54 if err := w.Ident.RenameCredential(pr.account.Name, id, label, pr.account.Name); err != nil {
55 if errors.Is(err, identity.ErrCredentialNotFound) {
56 http.Error(rw, err.Error(), http.StatusNotFound)
57 return
58 }
59 http.Error(rw, err.Error(), http.StatusBadRequest)
60 return
61 }
62 w.audit("web.passkey.rename", pr.account.Name, "", identity.CredentialIDString(id)+" "+label)
63 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
64 }
65
66 // RemoveAccountPasskey revokes one of the signed-in account's own passkeys, and with
67 // it the sessions that signed in with it.
68 //
69 // The last passkey is the last way into the browser, so it is refused when the account
70 // has no SSH key either: an account with neither can only be brought back by an
71 // instance admin, and losing the only way in should never be a click that succeeded.
72 // An SSH key is a way in — it pushes and clones — so an account that has one loses only
73 // its browser sign-in.
74 func (w *Web) RemoveAccountPasskey(rw http.ResponseWriter, r *http.Request) {
75 pr, ok := w.beginPost(rw, r, maxTinyForm)
76 if !ok {
77 return
78 }
79 id, err := identity.ParseCredentialID(strings.TrimSpace(r.PostFormValue("credential")))
80 if err != nil {
81 http.Error(rw, "choose a passkey", http.StatusBadRequest)
82 return
83 }
84 creds, err := w.Ident.Credentials(pr.account.Name)
85 if err != nil {
86 w.serverError(rw, r, "cannot read passkeys: "+err.Error())
87 return
88 }
89 last := len(creds) == 1 && identity.CredentialIDString(creds[0].ID) == identity.CredentialIDString(id)
90 if last && len(pr.account.Keys) == 0 {
91 http.Error(rw,
92 "this is the only way into your account: register another passkey or add an SSH key "+
93 "first, or ask an instance admin for an invite", http.StatusConflict)
94 return
95 }
96 if err := w.Ident.RemoveCredential(pr.account.Name, id, pr.account.Name); err != nil {
97 if errors.Is(err, identity.ErrCredentialNotFound) {
98 http.Error(rw, err.Error(), http.StatusNotFound)
99 return
100 }
101 http.Error(rw, err.Error(), http.StatusBadRequest)
102 return
103 }
104 w.audit("web.passkey.remove", pr.account.Name, "", identity.CredentialIDString(id))
105 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
106 }
107
f920a9f1
Keenan Nemetz
15d ago
108 func (w *Web) RemoveAccountKey(rw http.ResponseWriter, r *http.Request) {
c717d4eb
Keenan Nemetz
13d ago
109 pr, ok := w.beginPost(rw, r, maxTinyForm)
f920a9f1
Keenan Nemetz
15d ago
110 if !ok {
111 return
112 }
113 fp := strings.TrimSpace(r.PostFormValue("fingerprint"))
114 if fp == "" {
115 http.Error(rw, "choose a key", http.StatusBadRequest)
116 return
117 }
62576fa8
Keenan Nemetz
14d ago
118 if err := w.Ident.RemoveKey(pr.account.Name, fp, pr.account.Name); err != nil {
f920a9f1
Keenan Nemetz
15d ago
119 http.Error(rw, err.Error(), http.StatusBadRequest)
120 return
121 }
b1499c72
Keenan Nemetz
13d ago
122 w.audit("web.key.remove", pr.account.Name, "", fp)
ed03b3d1
Keenan Nemetz
3h ago
123 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
f920a9f1
Keenan Nemetz
15d ago
124 }