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.

Name branches after the account, and labels after the display

474a812d199a109ba894124cd6f26be12c73285d · Keenan Nemetz · 16d ago · parent ade6107e

Name branches after the account, and labels after the display

A contributor's folder was `refs/heads/contrib/<account-id>/`, which told a
reader nothing without decoding a ULID. It is now the account name,

    refs/heads/contrib/<name>/<branch>

and the name is therefore fixed at creation: renaming an account would strand
every ref under its old folder, so `vole admin user rename` is gone and the
users row itself — kept after retirement — is what stops a name being reissued.

What people see is now a separate, mutable display name, validated by the same
rules wherever it is set (CLI, remote admin, registration, `/account`) and shown
on pages, approvals, listings and refusals. Nothing is authorized by it and
nothing on disk is keyed by it: metadata authors stay account names because the
push boundary resolves them for ownership, and approvals stay account ids.

`user_names`, `Snapshot.Reserved` and `Account.Former` are dropped: with a fixed
name there are no former names to remember, and the UNIQUE column already does
the reserving. Opening an older roster adds the `display` column and drops the
alias table in place; contributor refs written under the old id namespace are
left where they are, readable, and landable by a maintainer.

server/web/auth_handlers.go +12−5

@@ -262,6 +262,7 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
262 262 }
263 263 var body struct {
264 264 Username string `json:"username"`
265 Display string `json:"display"`
265 266 }
266 267 if err := decodeJSONBody(r, &body); err != nil {
267 268 writeJSONError(rw, http.StatusBadRequest, err.Error())
@@ -272,6 +273,11 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
272 273 writeJSONError(rw, http.StatusBadRequest, err.Error())
273 274 return
274 275 }
276 display := strings.TrimSpace(body.Display)
277 if err := identity.ValidateDisplay(display); err != nil {
278 writeJSONError(rw, http.StatusBadRequest, err.Error())
279 return
280 }
275 281 if _, err := w.Ident.Find(name); err == nil {
276 282 writeJSONError(rw, http.StatusConflict, "that account name is already taken")
277 283 return
@@ -281,9 +287,10 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
281 287 writeJSONError(rw, http.StatusInternalServerError, "cannot create an account")
282 288 return
283 289 }
284 // The user handle is fixed now and written only when the ceremony succeeds.
285 // An abandoned registration therefore reserves neither a name nor an account.
286 user := &waUser{account: &identity.Account{ID: accountID, Name: name, Status: "active"}}
290 // The user handle and display name are fixed now and written only when the
291 // ceremony succeeds. An abandoned registration therefore reserves neither a
292 // name nor an account.
293 user := &waUser{account: &identity.Account{ID: accountID, Name: name, Display: display, Status: "active"}}
287 294 creation, data, err := w.Auth.WA.BeginRegistration(user,
288 295 webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred))
289 296 if err != nil {
@@ -291,7 +298,7 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
291 298 return
292 299 }
293 300 if err := w.beginCeremony(rw, ceremony{
294 Kind: "register", Data: *data, AccountID: accountID, Name: name,
301 Kind: "register", Data: *data, AccountID: accountID, Name: name, Display: display,
295 302 }, creation); err != nil {
296 303 writeJSONError(rw, http.StatusInternalServerError, "cannot store the ceremony")
297 304 }
@@ -316,7 +323,7 @@ func (w *Web) RegisterFinish(rw http.ResponseWriter, r *http.Request) {
316 323 writeJSONError(rw, http.StatusBadRequest, "registration failed: "+err.Error())
317 324 return
318 325 }
319 acct, err := w.Ident.CreatePasskey(c.Name, c.AccountID, fromWebAuthn(cred, "first passkey"), "self-registration")
326 acct, err := w.Ident.CreatePasskey(c.Name, c.Display, c.AccountID, fromWebAuthn(cred, "first passkey"), "self-registration")
320 327 if err != nil {
321 328 writeJSONError(rw, http.StatusConflict, "cannot create the account: "+err.Error())
322 329 return