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 | } | |