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.

Identify accounts by their registered name

62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 14d ago · parent 642e5994

Identify accounts by their registered name

Drop the internal account id and display name. Users, keys, passkeys,
invites, sessions, ACL entries, approvals, locks and CI records now all
use the registered name as the single identity.

The roster migrates in place: old id-keyed tables are rebuilt around the
name, and access-rule/lock references are rewritten through the same
mapping. Display-name editing and whoami --id are removed.

server/web/auth_handlers.go +19−32

@@ -9,7 +9,6 @@ import (
9 9 "github.com/go-webauthn/webauthn/protocol"
10 10 "github.com/go-webauthn/webauthn/webauthn"
11 11
12 "github.com/rodent-software/vole/core/id"
13 12 "github.com/rodent-software/vole/forge/identity"
14 13 "github.com/rodent-software/vole/forge/session"
15 14 )
@@ -113,7 +112,7 @@ func (w *Web) LoginBegin(rw http.ResponseWriter, r *http.Request) {
113 112 writeJSONError(rw, http.StatusBadRequest, "cannot begin login: "+err.Error())
114 113 return
115 114 }
116 if err := w.beginCeremony(rw, ceremony{Kind: "login", Data: *data, AccountID: acct.ID}, assertion); err != nil {
115 if err := w.beginCeremony(rw, ceremony{Kind: "login", Data: *data, Name: acct.Name}, assertion); err != nil {
117 116 writeJSONError(rw, http.StatusInternalServerError, "cannot store the ceremony")
118 117 }
119 118 }
@@ -131,7 +130,7 @@ func (w *Web) LoginFinish(rw http.ResponseWriter, r *http.Request) {
131 130 writeJSONError(rw, http.StatusBadRequest, "wrong ceremony")
132 131 return
133 132 }
134 acct, err := w.Ident.Find(c.AccountID)
133 acct, err := w.Ident.Find(c.Name)
135 134 if err != nil || !acct.Active() {
136 135 writeJSONError(rw, http.StatusUnauthorized, "login failed")
137 136 return
@@ -150,7 +149,7 @@ func (w *Web) LoginFinish(rw http.ResponseWriter, r *http.Request) {
150 149 writeJSONError(rw, http.StatusInternalServerError, "cannot update the passkey")
151 150 return
152 151 }
153 sess, err := w.Sessions.Create(acct.ID, cred.ID, r.UserAgent())
152 sess, err := w.Sessions.Create(acct.Name, cred.ID, r.UserAgent())
154 153 if err != nil {
155 154 writeJSONError(rw, http.StatusInternalServerError, "cannot create a session")
156 155 return
@@ -189,7 +188,7 @@ func (w *Web) InviteBegin(rw http.ResponseWriter, r *http.Request) {
189 188 return
190 189 }
191 190 if err := w.beginCeremony(rw, ceremony{
192 Kind: "invite", Data: *data, AccountID: acct.ID,
191 Kind: "invite", Data: *data, Name: acct.Name,
193 192 InviteToken: strings.TrimSpace(body.Token),
194 193 }, creation); err != nil {
195 194 writeJSONError(rw, http.StatusInternalServerError, "cannot store the ceremony")
@@ -210,7 +209,7 @@ func (w *Web) InviteFinish(rw http.ResponseWriter, r *http.Request) {
210 209 return
211 210 }
212 211 acct, err := w.Ident.InviteAccount(c.InviteToken)
213 if err != nil || acct.ID != c.AccountID {
212 if err != nil || acct.Name != c.Name {
214 213 writeJSONError(rw, http.StatusBadRequest, "this enrollment link is no longer valid")
215 214 return
216 215 }
@@ -229,7 +228,7 @@ func (w *Web) InviteFinish(rw http.ResponseWriter, r *http.Request) {
229 228 writeJSONError(rw, http.StatusBadRequest, "this enrollment link is no longer valid")
230 229 return
231 230 }
232 sess, err := w.Sessions.Create(acct.ID, cred.ID, r.UserAgent())
231 sess, err := w.Sessions.Create(acct.Name, cred.ID, r.UserAgent())
233 232 if err != nil {
234 233 writeJSONError(rw, http.StatusInternalServerError, "cannot create a session")
235 234 return
@@ -262,7 +261,6 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
262 261 }
263 262 var body struct {
264 263 Username string `json:"username"`
265 Display string `json:"display"`
266 264 }
267 265 if err := decodeJSONBody(r, &body); err != nil {
268 266 writeJSONError(rw, http.StatusBadRequest, err.Error())
@@ -273,24 +271,13 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
273 271 writeJSONError(rw, http.StatusBadRequest, err.Error())
274 272 return
275 273 }
276 display := strings.TrimSpace(body.Display)
277 if err := identity.ValidateDisplay(display); err != nil {
278 writeJSONError(rw, http.StatusBadRequest, err.Error())
279 return
280 }
281 274 if _, err := w.Ident.Find(name); err == nil {
282 275 writeJSONError(rw, http.StatusConflict, "that account name is already taken")
283 276 return
284 277 }
285 accountID, err := id.New(time.Now().UTC())
286 if err != nil {
287 writeJSONError(rw, http.StatusInternalServerError, "cannot create an account")
288 return
289 }
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"}}
278 // The name is the user handle, fixed now and written only when the ceremony
279 // succeeds. An abandoned registration therefore reserves no account.
280 user := &waUser{account: &identity.Account{Name: name, Status: "active"}}
294 281 creation, data, err := w.Auth.WA.BeginRegistration(user,
295 282 webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred))
296 283 if err != nil {
@@ -298,7 +285,7 @@ func (w *Web) RegisterBegin(rw http.ResponseWriter, r *http.Request) {
298 285 return
299 286 }
300 287 if err := w.beginCeremony(rw, ceremony{
301 Kind: "register", Data: *data, AccountID: accountID, Name: name, Display: display,
288 Kind: "register", Data: *data, Name: name,
302 289 }, creation); err != nil {
303 290 writeJSONError(rw, http.StatusInternalServerError, "cannot store the ceremony")
304 291 }
@@ -317,23 +304,23 @@ func (w *Web) RegisterFinish(rw http.ResponseWriter, r *http.Request) {
317 304 writeJSONError(rw, http.StatusBadRequest, "wrong ceremony")
318 305 return
319 306 }
320 user := &waUser{account: &identity.Account{ID: c.AccountID, Name: c.Name, Status: "active"}}
307 user := &waUser{account: &identity.Account{Name: c.Name, Status: "active"}}
321 308 cred, err := w.Auth.WA.FinishRegistration(user, c.Data, r)
322 309 if err != nil {
323 310 writeJSONError(rw, http.StatusBadRequest, "registration failed: "+err.Error())
324 311 return
325 312 }
326 acct, err := w.Ident.CreatePasskey(c.Name, c.Display, c.AccountID, fromWebAuthn(cred, "first passkey"), "self-registration")
313 acct, err := w.Ident.CreatePasskey(c.Name, fromWebAuthn(cred, "first passkey"), "self-registration")
327 314 if err != nil {
328 315 writeJSONError(rw, http.StatusConflict, "cannot create the account: "+err.Error())
329 316 return
330 317 }
331 sess, err := w.Sessions.Create(acct.ID, cred.ID, r.UserAgent())
318 sess, err := w.Sessions.Create(acct.Name, cred.ID, r.UserAgent())
332 319 if err != nil {
333 320 writeJSONError(rw, http.StatusInternalServerError, "cannot create a session")
334 321 return
335 322 }
336 w.audit("web.register", acct.ID, acct.Name)
323 w.audit("web.register", acct.Name, acct.Name)
337 324 w.setSessionCookie(rw, sess)
338 325 writeJSON(rw, http.StatusOK, map[string]any{"ok": true, "redirect": "/"})
339 326 }
@@ -367,14 +354,14 @@ func (w *Web) AccountPage(rw http.ResponseWriter, r *http.Request) {
367 354 if !ok {
368 355 return
369 356 }
370 creds, err := w.Ident.Credentials(pr.account.ID)
357 creds, err := w.Ident.Credentials(pr.account.Name)
371 358 if err != nil {
372 359 w.serverError(rw, r, "cannot read passkeys: "+err.Error())
373 360 return
374 361 }
375 362 var sessions []session.Session
376 363 if w.Sessions != nil {
377 sessions, _ = w.Sessions.List(pr.account.ID)
364 sessions, _ = w.Sessions.List(pr.account.Name)
378 365 }
379 366 w.render(rw, "account", struct {
380 367 Page
@@ -426,7 +413,7 @@ func (w *Web) AddPasskeyBegin(rw http.ResponseWriter, r *http.Request) {
426 413 return
427 414 }
428 415 if err := w.beginCeremony(rw, ceremony{
429 Kind: "passkey", Data: *data, AccountID: pr.account.ID,
416 Kind: "passkey", Data: *data, Name: pr.account.Name,
430 417 Label: strings.TrimSpace(body.Label),
431 418 }, creation); err != nil {
432 419 writeJSONError(rw, http.StatusInternalServerError, "cannot store the ceremony")
@@ -450,7 +437,7 @@ func (w *Web) AddPasskeyFinish(rw http.ResponseWriter, r *http.Request) {
450 437 if !ok {
451 438 return
452 439 }
453 if c.Kind != "passkey" || c.AccountID != pr.account.ID {
440 if c.Kind != "passkey" || c.Name != pr.account.Name {
454 441 writeJSONError(rw, http.StatusBadRequest, "wrong ceremony")
455 442 return
456 443 }
@@ -468,7 +455,7 @@ func (w *Web) AddPasskeyFinish(rw http.ResponseWriter, r *http.Request) {
468 455 if label == "" {
469 456 label = "passkey"
470 457 }
471 if err := w.Ident.AddCredential(pr.account.ID, fromWebAuthn(cred, label), pr.account.Name); err != nil {
458 if err := w.Ident.AddCredential(pr.account.Name, fromWebAuthn(cred, label), pr.account.Name); err != nil {
472 459 writeJSONError(rw, http.StatusBadRequest, err.Error())
473 460 return
474 461 }