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/auth_test.go +17−101

@@ -116,7 +116,7 @@ func TestBrowserLoginAndInvitePages(t *testing.T) {
116 116 t.Fatalf("login page lost the next path: %s", body)
117 117 }
118 118
119 acct, inv, err := f.srv.Identity.CreateWithInvite("newbie", "", "test", time.Hour)
119 acct, inv, err := f.srv.Identity.CreateWithInvite("newbie", "test", time.Hour)
120 120 if err != nil {
121 121 t.Fatal(err)
122 122 }
@@ -228,7 +228,7 @@ func TestWebApprovalNamesTheSessionAccount(t *testing.T) {
228 228 f.addWriter(t)
229 229 m := mustRepo(t, f)
230 230 head := strings.Repeat("a", 40)
231 req, err := metadata.NewRequest("A change", "please review", "wilbur", f.writerID, "main", head, time.Now().UTC())
231 req, err := metadata.NewRequest("A change", "please review", "wilbur", "main", head, time.Now().UTC())
232 232 if err != nil {
233 233 t.Fatal(err)
234 234 }
@@ -271,7 +271,7 @@ func TestWebAuthnBeginEndpoints(t *testing.T) {
271 271 t.Fatalf("login begin: status=%d body=%v", resp.StatusCode, out)
272 272 }
273 273
274 _, inv, err := f.srv.Identity.CreateWithInvite("newbie", "", "test", time.Hour)
274 _, inv, err := f.srv.Identity.CreateWithInvite("newbie", "test", time.Hour)
275 275 if err != nil {
276 276 t.Fatal(err)
277 277 }
@@ -297,7 +297,7 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
297 297 f := browserFixture(t)
298 298
299 299 pub, priv := keypair(t, filepath.Join(f.cfg.DataDir, "id_contrib"))
300 contrib, err := f.srv.Identity.Create("contrib", "", pub, "test")
300 contrib, err := f.srv.Identity.Create("contrib", pub, "test")
301 301 if err != nil {
302 302 t.Fatal(err)
303 303 }
@@ -306,7 +306,7 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
306 306 if err != nil {
307 307 t.Fatal(err)
308 308 }
309 if err := a.SetEntry(contrib.ID, "contributor"); err != nil {
309 if err := a.SetEntry(contrib.Name, "contributor"); err != nil {
310 310 t.Fatal(err)
311 311 }
312 312 if err := m.SetACL(a, "test"); err != nil {
@@ -330,13 +330,7 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
330 330 t.Fatal("contributor pushed another account's namespace")
331 331 }
332 332
333 sess, cookie := f.browserSession(t, contrib.ID)
334 // The display name is a label: it may change, and neither the branch
335 // namespace, the session, nor an existing request depends on it.
336 if _, err := f.srv.Identity.SetDisplay(contrib.ID, "Contributor Pat", "test"); err != nil {
337 t.Fatal(err)
338 }
339
333 sess, cookie := f.browserSession(t, contrib.Name)
340 334 // Contributors can discuss without access to shared branches.
341 335 resp, body := f.browserPost(t, "/r/"+f.repo+"/issues", cookie, url.Values{
342 336 "csrf": {sess.CSRF}, "title": {"Contributor issue"}, "body": {"from outside"},
@@ -361,7 +355,7 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
361 355 if err != nil {
362 356 t.Fatal(err)
363 357 }
364 if len(list) != 1 || list[0].AuthorID != contrib.ID || list[0].Author != "contrib" || list[0].Base != "main" {
358 if len(list) != 1 || list[0].Author != "contrib" || list[0].Base != "main" {
365 359 t.Fatalf("request = %+v", list)
366 360 }
367 361 want, _ := m.Git().RevParse("refs/heads/" + headRef)
@@ -384,7 +378,7 @@ func TestContributorCIIsHeldUntilWriterAllows(t *testing.T) {
384 378 f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 })
385 379
386 380 pub, priv := keypair(t, filepath.Join(f.cfg.DataDir, "id_ci_contrib"))
387 contrib, err := f.srv.Identity.Create("ci-contrib", "", pub, "test")
381 contrib, err := f.srv.Identity.Create("ci-contrib", pub, "test")
388 382 if err != nil {
389 383 t.Fatal(err)
390 384 }
@@ -393,7 +387,7 @@ func TestContributorCIIsHeldUntilWriterAllows(t *testing.T) {
393 387 if err != nil {
394 388 t.Fatal(err)
395 389 }
396 if err := a.SetEntry(contrib.ID, "contributor"); err != nil {
390 if err := a.SetEntry(contrib.Name, "contributor"); err != nil {
397 391 t.Fatal(err)
398 392 }
399 393 if err := m.SetACL(a, "test"); err != nil {
@@ -436,7 +430,7 @@ jobs:
436 430 time.Sleep(100 * time.Millisecond)
437 431 }
438 432
439 contribSess, contribCookie := f.browserSession(t, contrib.ID)
433 contribSess, contribCookie := f.browserSession(t, contrib.Name)
440 434 resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", contribCookie, url.Values{
441 435 "csrf": {contribSess.CSRF}, "title": {"CI gate"}, "body": {"please review"},
442 436 "base": {"main"}, "head": {ref},
@@ -534,16 +528,6 @@ func TestOpenRegistrationPagesAndBegin(t *testing.T) {
534 528 if resp.StatusCode != http.StatusBadRequest {
535 529 t.Fatalf("invalid name status = %d, want 400", resp.StatusCode)
536 530 }
537 // The display name is optional and validated with the same rules the roster
538 // applies, so a form cannot hand the store something it would reject.
539 resp, _ = f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "newbie", "display": strings.Repeat("x", 100)})
540 if resp.StatusCode != http.StatusBadRequest {
541 t.Fatalf("overlong display status = %d, want 400", resp.StatusCode)
542 }
543 resp, out = f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "newbie", "display": "New Bie"})
544 if resp.StatusCode != http.StatusOK || out["publicKey"] == nil {
545 t.Fatalf("register begin with a display name: status=%d body=%v", resp.StatusCode, out)
546 }
547 531
548 532 // An account is not created until the authenticator completes the ceremony.
549 533 if _, err := f.srv.Identity.Find("newbie"); err == nil {
@@ -613,82 +597,14 @@ func TestAccountAddsAndRemovesItsOwnSSHKey(t *testing.T) {
613 597 }
614 598 }
615 599
616 // TestAccountDisplayNameIsALabelNotAnIdentity: the display name is what pages
617 // show, and it may change; the account name is what refs and metadata files were
618 // written with, and it may not.
619 func TestAccountDisplayNameIsALabelNotAnIdentity(t *testing.T) {
600 // TestDisplayNameRouteIsGone: an account is only its registered name, so there
601 // is no second label to change.
602 func TestDisplayNameRouteIsGone(t *testing.T) {
620 603 t.Parallel()
621 604 f := browserFixture(t)
622 sess, cookie := f.browserSession(t, f.ownerID)
623
624 // The signed-in person sets their own label; no administrator involved.
625 resp, body := f.browserPost(t, "/account/display", cookie, url.Values{
626 "csrf": {sess.CSRF}, "display": {"Keenan O'Brien"},
627 })
628 if resp.StatusCode != http.StatusSeeOther {
629 t.Fatalf("set display: status=%d body=%s", resp.StatusCode, body)
630 }
631 a, err := f.srv.Identity.Find(f.ownerID)
632 if err != nil {
633 t.Fatal(err)
634 }
635 if a.Name != "keenan" || a.Display != "Keenan O'Brien" {
636 t.Fatalf("account = %+v, want name keenan and the display label", a)
637 }
638
639 // Pages show the label; metadata still records the name.
640 if _, body := f.browserGet(t, "/account", cookie); !strings.Contains(body, "Keenan O'Brien") {
641 t.Fatalf("account page does not show the display name: %s", body)
642 }
643 resp, body = f.browserPost(t, "/r/"+f.repo+"/issues", cookie, url.Values{
644 "csrf": {sess.CSRF}, "title": {"Label check"}, "body": {"who wrote this?"},
645 })
646 if resp.StatusCode != http.StatusSeeOther {
647 t.Fatalf("create issue: status=%d body=%s", resp.StatusCode, body)
648 }
649 loc := resp.Header.Get("Location")
650 if _, body := f.browserGet(t, loc, cookie); !strings.Contains(body, "Keenan O'Brien") {
651 t.Fatalf("issue page does not show the display name: %s", body)
652 }
653 iss, err := index.LoadIssue(mustRepo(t, f), strings.TrimPrefix(loc, "/r/"+f.repo+"/issues/"))
654 if err != nil {
655 t.Fatal(err)
656 }
657 if iss.Author != "keenan" {
658 t.Fatalf("issue author = %q; metadata must keep the account name", iss.Author)
659 }
660
661 // A display name is not an identity: it cannot be used to resolve an account.
662 if _, err := f.srv.Identity.Find("Keenan O'Brien"); err == nil {
663 t.Fatal("a display name resolved as an account")
664 }
665 if _, ok := f.srv.Identity.ResolveID("Keenan O'Brien"); ok {
666 t.Fatal("a display name resolved to an account id")
667 }
668
669 // Invalid labels are refused before anything is written, and a write without
670 // the token never reaches the roster.
671 resp, _ = f.browserPost(t, "/account/display", cookie, url.Values{
672 "csrf": {sess.CSRF}, "display": {strings.Repeat("x", 100)},
673 })
674 if resp.StatusCode != http.StatusBadRequest {
675 t.Fatalf("overlong display status = %d, want 400", resp.StatusCode)
676 }
677 resp, _ = f.browserPost(t, "/account/display", cookie, url.Values{
678 "display": {"forged"},
679 })
680 if resp.StatusCode != http.StatusForbidden {
681 t.Fatalf("display without CSRF status = %d, want 403", resp.StatusCode)
682 }
683
684 // Clearing the label falls back to the name.
685 resp, _ = f.browserPost(t, "/account/display", cookie, url.Values{
686 "csrf": {sess.CSRF}, "display": {""},
687 })
688 if resp.StatusCode != http.StatusSeeOther {
689 t.Fatalf("clear display: status=%d", resp.StatusCode)
690 }
691 if a, err := f.srv.Identity.Find(f.ownerID); err != nil || a.Display != "" || a.Label() != "keenan" {
692 t.Fatalf("cleared display = %+v, %v", a, err)
605 _, cookie := f.browserSession(t, f.ownerID)
606 resp, _ := f.browserPost(t, "/account/display", cookie, url.Values{"display": {"Keenan O'Brien"}})
607 if resp.StatusCode != http.StatusNotFound {
608 t.Fatalf("display route status = %d, want 404", resp.StatusCode)
693 609 }
694 610 }