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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 59m ago 4b8c9e1d
history blame raw 4.4 KiB
1package web
2
3import (
4 "errors"
5 "net/http"
6 "strings"
7
8 "rodent.software/vole/forge/identity"
9)
10
11// AddAccountKey lets a signed-in person attach an SSH public key without an
12// instance admin. Open registration is only useful for Git contributions if the
13// account can turn its browser session into a key that pushes.
14func (w *Web) AddAccountKey(rw http.ResponseWriter, r *http.Request) {
15 pr, ok := w.beginPost(rw, r, maxTinyForm)
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 }
24 fp, err := w.Ident.AddKey(pr.account.Name, key, pr.account.Name)
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 }
34 w.audit("web.key.add", pr.account.Name, "", fp)
35 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
36}
37
38// RemoveAccountKey detaches one of the signed-in account's own keys. It is the
39// same operation an instance admin can perform, scoped by the session account.
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.
43func (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.
74func (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
108func (w *Web) RemoveAccountKey(rw http.ResponseWriter, r *http.Request) {
109 pr, ok := w.beginPost(rw, r, maxTinyForm)
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 }
118 if err := w.Ident.RemoveKey(pr.account.Name, fp, pr.account.Name); err != nil {
119 http.Error(rw, err.Error(), http.StatusBadRequest)
120 return
121 }
122 w.audit("web.key.remove", pr.account.Name, "", fp)
123 http.Redirect(rw, r, "/-/account", http.StatusSeeOther)
124}
125