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.go +20−22
| @@ -3,8 +3,8 @@ package web | |||
| 3 | 3 | // Browser authentication: passkey ceremonies, sessions and CSRF. | |
| 4 | 4 | // | |
| 5 | 5 | // The browser is a second credential for an account that already exists; it is | |
| 6 | // not a second account. A passkey assertion resolves to the same account id an | ||
| 7 | // SSH key does, and every write still goes through that id. | ||
| 6 | // not a second account. A passkey assertion resolves to the same account name an | ||
| 7 | // SSH key does, and every write still goes through that name. | ||
| 8 | 8 | ||
| 9 | 9 | import ( | |
| 10 | 10 | "context" | |
| @@ -132,7 +132,7 @@ func (w *Web) Middleware(next http.Handler) http.Handler { | |||
| 132 | 132 | if w.Sessions != nil { | |
| 133 | 133 | sess, cookieName := w.sessionFromCookie(r) | |
| 134 | 134 | if sess != nil { | |
| 135 | if acct, err := w.Ident.Find(sess.AccountID); err == nil && acct.Active() { | ||
| 135 | if acct, err := w.Ident.Find(sess.Name); err == nil && acct.Active() { | ||
| 136 | 136 | r = w.withPrincipal(r, acct, sess) | |
| 137 | 137 | } else { | |
| 138 | 138 | w.clearCookieFor(rw, cookieName, cookieName != devSessionCookieName) | |
| @@ -144,7 +144,7 @@ func (w *Web) Middleware(next http.Handler) http.Handler { | |||
| 144 | 144 | // only after an explicit logout has not opted out for this browser. | |
| 145 | 145 | if w.principal(r) == nil && w.devLoginRequest(r) { | |
| 146 | 146 | if sess := w.ensureDevSession(); sess != nil { | |
| 147 | if acct, err := w.Ident.Find(sess.AccountID); err == nil && acct.Active() { | ||
| 147 | if acct, err := w.Ident.Find(sess.Name); err == nil && acct.Active() { | ||
| 148 | 148 | w.setDevSessionCookie(rw, sess) | |
| 149 | 149 | r = w.withPrincipal(r, acct, sess) | |
| 150 | 150 | } | |
| @@ -237,7 +237,7 @@ func (w *Web) ensureDevSession() *session.Session { | |||
| 237 | 237 | return sess | |
| 238 | 238 | } | |
| 239 | 239 | } | |
| 240 | sess, err := w.Sessions.Create(w.devAccount.ID, nil, "dev-login") | ||
| 240 | sess, err := w.Sessions.Create(w.devAccount.Name, nil, "dev-login") | ||
| 241 | 241 | if err != nil { | |
| 242 | 242 | w.Logger("dev login: create session: %v", err) | |
| 243 | 243 | return nil | |
| @@ -296,41 +296,41 @@ func (w *Web) registrationEnabled() bool { | |||
| 296 | 296 | return w.enabled() && w.Cfg != nil && w.Cfg.OpenRegistration | |
| 297 | 297 | } | |
| 298 | 298 | ||
| 299 | func (w *Web) accountID(r *http.Request) string { | ||
| 299 | func (w *Web) accountName(r *http.Request) string { | ||
| 300 | 300 | if p := w.principal(r); p != nil { | |
| 301 | return p.account.ID | ||
| 301 | return p.account.Name | ||
| 302 | 302 | } | |
| 303 | 303 | return "" | |
| 304 | 304 | } | |
| 305 | 305 | ||
| 306 | 306 | func (w *Web) canDiscuss(r *http.Request, acl *access.ACL) bool { | |
| 307 | id := w.accountID(r) | ||
| 308 | return id != "" && acl.CanDiscuss(id) | ||
| 307 | name := w.accountName(r) | ||
| 308 | return name != "" && acl.CanDiscuss(name) | ||
| 309 | 309 | } | |
| 310 | 310 | ||
| 311 | func (w *Web) canApprove(r *http.Request, acl *access.ACL, authorID string) bool { | ||
| 312 | id := w.accountID(r) | ||
| 313 | return id != "" && id != authorID && acl.CanApprove(id) | ||
| 311 | func (w *Web) canApprove(r *http.Request, acl *access.ACL, author string) bool { | ||
| 312 | name := w.accountName(r) | ||
| 313 | return name != "" && name != author && acl.CanApprove(name) | ||
| 314 | 314 | } | |
| 315 | 315 | ||
| 316 | 316 | func (w *Web) page(r *http.Request, title string) Page { | |
| 317 | 317 | p := Page{Title: title} | |
| 318 | 318 | if pr := w.principal(r); pr != nil { | |
| 319 | 319 | p.LoggedIn = true | |
| 320 | p.User = pr.account.Label() | ||
| 321 | p.AccountID = pr.account.ID | ||
| 320 | p.User = pr.account.Name | ||
| 321 | p.AccountName = pr.account.Name | ||
| 322 | 322 | p.CSRF = pr.session.CSRF | |
| 323 | 323 | } | |
| 324 | 324 | if w.devAccount != nil { | |
| 325 | 325 | p.DevLogin = w.devAccount.Name | |
| 326 | 326 | } | |
| 327 | 327 | if p.LoggedIn && w.Ident != nil { | |
| 328 | p.InstanceAdmin = w.Ident.IsAdmin(p.AccountID) | ||
| 328 | p.InstanceAdmin = w.Ident.IsAdmin(p.AccountName) | ||
| 329 | 329 | } | |
| 330 | 330 | if p.LoggedIn && w.Repos != nil { | |
| 331 | 331 | if name := r.PathValue("name"); name != "" { | |
| 332 | 332 | if m, err := w.Repos.Open(name); err == nil { | |
| 333 | if acl, err := m.ACL(); err == nil && acl.Can(p.AccountID, access.Admin) { | ||
| 333 | if acl, err := m.ACL(); err == nil && acl.Can(p.AccountName, access.Admin) { | ||
| 334 | 334 | p.CanAdmin = true | |
| 335 | 335 | } | |
| 336 | 336 | } | |
| @@ -429,10 +429,8 @@ func writeJSONError(rw http.ResponseWriter, status int, msg string) { | |||
| 429 | 429 | type ceremony struct { | |
| 430 | 430 | Kind string // login | invite | passkey | register | |
| 431 | 431 | Data webauthn.SessionData | |
| 432 | AccountID string | ||
| 433 | InviteToken string | ||
| 434 | 432 | Name string | |
| 435 | Display string | ||
| 433 | InviteToken string | ||
| 436 | 434 | Label string | |
| 437 | 435 | Expires time.Time | |
| 438 | 436 | } | |
| @@ -519,13 +517,13 @@ type waUser struct { | |||
| 519 | 517 | creds []webauthn.Credential | |
| 520 | 518 | } | |
| 521 | 519 | ||
| 522 | func (u *waUser) WebAuthnID() []byte { return []byte(u.account.ID) } | ||
| 520 | func (u *waUser) WebAuthnID() []byte { return []byte(u.account.Name) } | ||
| 523 | 521 | func (u *waUser) WebAuthnName() string { return u.account.Name } | |
| 524 | 522 | func (u *waUser) WebAuthnDisplayName() string { return u.account.Name } | |
| 525 | 523 | func (u *waUser) WebAuthnCredentials() []webauthn.Credential { return u.creds } | |
| 526 | 524 | ||
| 527 | 525 | func (w *Web) waUser(a *identity.Account) (*waUser, error) { | |
| 528 | creds, err := w.Ident.Credentials(a.ID) | ||
| 526 | creds, err := w.Ident.Credentials(a.Name) | ||
| 529 | 527 | if err != nil { | |
| 530 | 528 | return nil, err | |
| 531 | 529 | } | |
| @@ -621,7 +619,7 @@ const authJS = `(function(){ | |||
| 621 | 619 | await post('/auth/invite/finish',resp(c)); | |
| 622 | 620 | location.href='/'; | |
| 623 | 621 | }else if(form.dataset.auth==='register'){ | |
| 624 | const b=await post('/auth/register/begin',{username:form.username.value.trim(),display:form.display?form.display.value.trim():''}); | ||
| 622 | const b=await post('/auth/register/begin',{username:form.username.value.trim()}); | ||
| 625 | 623 | const c=await navigator.credentials.create({publicKey:prepCreate(b)}); | |
| 626 | 624 | await post('/auth/register/finish',resp(c)); | |
| 627 | 625 | location.href='/'; | |