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.
Allow opt-in open registration
f920a9f12c4ff12e4f47497a084f0201d8914181 · Keenan Nemetz · 15d 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/auth.go +13−1
| @@ -125,6 +125,12 @@ func (w *Web) securityHeaders(rw http.ResponseWriter) { | |||
| 125 | 125 | ||
| 126 | 126 | func (w *Web) enabled() bool { return w.Auth != nil && w.Auth.Enabled() } | |
| 127 | 127 | ||
| 128 | // registrationEnabled is a policy switch on top of passkey availability: an | ||
| 129 | // instance can have browser login without accepting new accounts. | ||
| 130 | func (w *Web) registrationEnabled() bool { | ||
| 131 | return w.enabled() && w.Cfg != nil && w.Cfg.OpenRegistration | ||
| 132 | } | ||
| 133 | |||
| 128 | 134 | func (w *Web) accountID(r *http.Request) string { | |
| 129 | 135 | if p := w.principal(r); p != nil { | |
| 130 | 136 | return p.account.ID | |
| @@ -223,10 +229,11 @@ func writeJSONError(rw http.ResponseWriter, status int, msg string) { | |||
| 223 | 229 | // ---------- ceremony state ---------- | |
| 224 | 230 | ||
| 225 | 231 | type ceremony struct { | |
| 226 | Kind string // login | invite | passkey | ||
| 232 | Kind string // login | invite | passkey | register | ||
| 227 | 233 | Data webauthn.SessionData | |
| 228 | 234 | AccountID string | |
| 229 | 235 | InviteToken string | |
| 236 | Name string | ||
| 230 | 237 | Label string | |
| 231 | 238 | Expires time.Time | |
| 232 | 239 | } | |
| @@ -414,6 +421,11 @@ const authJS = `(function(){ | |||
| 414 | 421 | const c=await navigator.credentials.create({publicKey:prepCreate(b)}); | |
| 415 | 422 | await post('/auth/invite/finish',resp(c)); | |
| 416 | 423 | location.href='/'; | |
| 424 | }else if(form.dataset.auth==='register'){ | ||
| 425 | const b=await post('/auth/register/begin',{username:form.username.value.trim()}); | ||
| 426 | const c=await navigator.credentials.create({publicKey:prepCreate(b)}); | ||
| 427 | await post('/auth/register/finish',resp(c)); | ||
| 428 | location.href='/'; | ||
| 417 | 429 | }else if(form.dataset.auth==='passkey'){ | |
| 418 | 430 | const b=await post('/account/passkeys/begin',{label:form.label.value.trim()},true); | |
| 419 | 431 | const c=await navigator.credentials.create({publicKey:prepCreate(b)}); | |