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.

Allow opt-in open registration

f920a9f12c4ff12e4f47497a084f0201d8914181 · Keenan Nemetz · 16d ago · parent 6c84d387

Allow opt-in open registration

Add open_registration, off by default and effective only with an HTTPS public_url. /register creates an account and its first passkey without an administrator; the account row is written only after the WebAuthn ceremony succeeds, so an abandoned registration reserves no name.

New accounts start with no repository-specific ACL entries. Add self-service SSH key management on the Account page so a registered contributor can add the key needed to push to their contributor namespace.

Cover registration enablement, name validation, deferred account creation, and self-service key add/remove in tests, and document the policy.

server/web/account.go +71

@@ -0,0 +1,71 @@
1 package web
2
3 import (
4 "net/http"
5 "strings"
6 )
7
8 // AddAccountKey lets a signed-in person attach an SSH public key without an
9 // administrator. Open registration is only useful for Git contributions if the
10 // account can turn its browser session into a key that pushes.
11 func (w *Web) AddAccountKey(rw http.ResponseWriter, r *http.Request) {
12 pr, ok := w.requireHTMLUser(rw, r)
13 if !ok {
14 return
15 }
16 r.Body = http.MaxBytesReader(rw, r.Body, 64<<10)
17 if !w.checkCSRF(r) {
18 http.Error(rw, "bad request", http.StatusForbidden)
19 return
20 }
21 if err := r.ParseForm(); err != nil {
22 http.Error(rw, "cannot read the form", http.StatusBadRequest)
23 return
24 }
25 key := strings.TrimSpace(r.PostFormValue("key"))
26 if key == "" {
27 http.Error(rw, "paste an SSH public key", http.StatusBadRequest)
28 return
29 }
30 fp, err := w.Ident.AddKey(pr.account.ID, key, pr.account.Name)
31 if err != nil {
32 msg := err.Error()
33 if strings.Contains(msg, "already registered") {
34 // Do not turn a known public key into a lookup of somebody else's name.
35 msg = "that SSH key is already registered"
36 }
37 http.Error(rw, msg, http.StatusBadRequest)
38 return
39 }
40 w.audit("web.key.add", pr.account.ID, fp)
41 http.Redirect(rw, r, "/account", http.StatusSeeOther)
42 }
43
44 // RemoveAccountKey detaches one of the signed-in account's own keys. It is the
45 // same operation an administrator can perform, scoped by the session account.
46 func (w *Web) RemoveAccountKey(rw http.ResponseWriter, r *http.Request) {
47 pr, ok := w.requireHTMLUser(rw, r)
48 if !ok {
49 return
50 }
51 r.Body = http.MaxBytesReader(rw, r.Body, 16<<10)
52 if !w.checkCSRF(r) {
53 http.Error(rw, "bad request", http.StatusForbidden)
54 return
55 }
56 if err := r.ParseForm(); err != nil {
57 http.Error(rw, "cannot read the form", http.StatusBadRequest)
58 return
59 }
60 fp := strings.TrimSpace(r.PostFormValue("fingerprint"))
61 if fp == "" {
62 http.Error(rw, "choose a key", http.StatusBadRequest)
63 return
64 }
65 if err := w.Ident.RemoveKey(pr.account.ID, fp, pr.account.Name); err != nil {
66 http.Error(rw, err.Error(), http.StatusBadRequest)
67 return
68 }
69 w.audit("web.key.remove", pr.account.ID, fp)
70 http.Redirect(rw, r, "/account", http.StatusSeeOther)
71 }