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.
Add instance-wide roles and refine access UI
5b814e9958090cfd1fa826efb2d375bf613547c1 · Keenan Nemetz · 14d ago · parent 9579c1f9
Add instance-wide roles and refine access UI Instance roles now live on accounts, apply to public repositories, and the Admin role is the unified instance-admin permission with access to every repository. Repository ACLs can raise an instance role but not lower it. Adds role assignment to the users UI and CLI, open-registration role config, a reusable role picker, and separates adding a repository account from saving access rules.
forge/identity/identity_test.go +56
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "sync" | |
| 10 | 10 | "testing" | |
| 11 | 11 | ||
| 12 | "github.com/rodent-software/vole/forge/access" | ||
| 12 | 13 | "golang.org/x/crypto/ssh" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| @@ -616,6 +617,61 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 616 | 617 | } | |
| 617 | 618 | } | |
| 618 | 619 | ||
| 620 | func TestInstanceRolesAreAccountStanding(t *testing.T) { | ||
| 621 | t.Parallel() | ||
| 622 | s := newStore(t) | ||
| 623 | line, _ := pubLine(t) | ||
| 624 | alice, err := s.Create("alice", line, "operator") | ||
| 625 | if err != nil { | ||
| 626 | t.Fatal(err) | ||
| 627 | } | ||
| 628 | if alice.Role != access.None || s.InstanceRole(alice.Name) != access.None { | ||
| 629 | t.Fatalf("new account role = %v", alice.Role) | ||
| 630 | } | ||
| 631 | if _, err := s.SetInstanceRole(alice.Name, access.Contribute, "operator"); err != nil { | ||
| 632 | t.Fatal(err) | ||
| 633 | } | ||
| 634 | a, err := s.Find(alice.Name) | ||
| 635 | if err != nil { | ||
| 636 | t.Fatal(err) | ||
| 637 | } | ||
| 638 | if a.Role != access.Contribute || a.Admin || s.IsAdmin(alice.Name) { | ||
| 639 | t.Fatalf("instance role = %v admin=%t", a.Role, a.Admin) | ||
| 640 | } | ||
| 641 | if s.InstanceRole(alice.Name) != access.Contribute { | ||
| 642 | t.Fatalf("InstanceRole = %v", s.InstanceRole(alice.Name)) | ||
| 643 | } | ||
| 644 | bob, inv, err := s.CreateWithInviteRole("bob", access.Maintain, "operator", 0) | ||
| 645 | if err != nil || inv == nil { | ||
| 646 | t.Fatal(err) | ||
| 647 | } | ||
| 648 | if bob.Role != access.Maintain { | ||
| 649 | t.Fatalf("invited role = %v", bob.Role) | ||
| 650 | } | ||
| 651 | if _, err := s.SetInstanceRole(alice.Name, access.Admin, "operator"); err != nil { | ||
| 652 | t.Fatal(err) | ||
| 653 | } | ||
| 654 | if !s.IsAdmin(alice.Name) { | ||
| 655 | t.Fatal("Admin role did not grant instance admin") | ||
| 656 | } | ||
| 657 | if _, err := s.SetInstanceRole(alice.Name, access.None, "operator"); err == nil { | ||
| 658 | t.Fatal("removed the last instance admin") | ||
| 659 | } | ||
| 660 | if _, err := s.Retire(bob.Name, "operator"); err != nil { | ||
| 661 | t.Fatal(err) | ||
| 662 | } | ||
| 663 | if got := s.InstanceRole(bob.Name); got != access.None { | ||
| 664 | t.Fatalf("retired account role = %v, want none", got) | ||
| 665 | } | ||
| 666 | log, err := os.ReadFile(s.AuditPath) | ||
| 667 | if err != nil { | ||
| 668 | t.Fatal(err) | ||
| 669 | } | ||
| 670 | if !strings.Contains(string(log), "identity.set-role\toperator\talice\trole=contribute") { | ||
| 671 | t.Fatalf("role change was not audited:\n%s", log) | ||
| 672 | } | ||
| 673 | } | ||
| 674 | |||
| 619 | 675 | func TestLastInstanceAdminCannotBeRemoved(t *testing.T) { | |
| 620 | 676 | t.Parallel() | |
| 621 | 677 | s := newStore(t) | |