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/access/acl_test.go +72−12
| @@ -4,8 +4,8 @@ import "testing" | |||
| 4 | 4 | ||
| 5 | 5 | func TestRoleOrdering(t *testing.T) { | |
| 6 | 6 | t.Parallel() | |
| 7 | if !Admin.AtLeast(Maintain) || !Maintain.AtLeast(Contributor) || !Contributor.AtLeast(Read) || | ||
| 8 | Read.AtLeast(Contributor) || Contributor.AtLeast(Maintain) || Maintain != Write { | ||
| 7 | if !Admin.AtLeast(Maintain) || !Maintain.AtLeast(Contribute) || !Contribute.AtLeast(Read) || | ||
| 8 | Read.AtLeast(Contribute) || Contribute.AtLeast(Maintain) || Maintain != Write { | ||
| 9 | 9 | t.Fatal("role ordering is wrong") | |
| 10 | 10 | } | |
| 11 | 11 | for _, c := range []struct { | |
| @@ -16,9 +16,9 @@ func TestRoleOrdering(t *testing.T) { | |||
| 16 | 16 | {"none", None, true}, | |
| 17 | 17 | {"", None, true}, | |
| 18 | 18 | {"read", Read, true}, | |
| 19 | {"contributor", Contributor, true}, | ||
| 20 | {"contribute", Contributor, true}, | ||
| 21 | {"contrib", Contributor, true}, | ||
| 19 | {"contributor", Contribute, true}, | ||
| 20 | {"contribute", Contribute, true}, | ||
| 21 | {"contrib", Contribute, true}, | ||
| 22 | 22 | {"maintain", Maintain, true}, | |
| 23 | 23 | {"maintainer", Maintain, true}, | |
| 24 | 24 | {"write", Maintain, true}, | |
| @@ -50,17 +50,31 @@ func TestWriteIsTheHistoricalSpellingOfMaintain(t *testing.T) { | |||
| 50 | 50 | } | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | func TestContributorIsTheHistoricalSpellingOfContribute(t *testing.T) { | ||
| 54 | t.Parallel() | ||
| 55 | if Contributor != Contribute || Contribute.String() != "contribute" { | ||
| 56 | t.Fatalf("Contribute = %v, alias = %v", Contribute, Contributor) | ||
| 57 | } | ||
| 58 | a, err := Parse([]byte(`{"version":1,"default":"contributor","entries":{"alice":"contributor"}}`)) | ||
| 59 | if err != nil { | ||
| 60 | t.Fatal(err) | ||
| 61 | } | ||
| 62 | if a.Default != "contribute" || a.Entries["alice"] != "contribute" { | ||
| 63 | t.Fatalf("old spelling was not normalized: %+v", a) | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 53 | 67 | func TestRoleFor(t *testing.T) { | |
| 54 | 68 | t.Parallel() | |
| 55 | 69 | a := &ACL{Default: "none", Entries: map[string]string{ | |
| 56 | "owner": "admin", "dev": "write", "guest": "read", "contributor": "contributor", | ||
| 70 | "owner": "admin", "dev": "write", "guest": "read", "contributor": "contribute", | ||
| 57 | 71 | }} | |
| 58 | 72 | cases := []struct { | |
| 59 | 73 | user string | |
| 60 | 74 | want Role | |
| 61 | 75 | }{ | |
| 62 | 76 | {"owner", Admin}, {"dev", Maintain}, {"guest", Read}, | |
| 63 | {"contributor", Contributor}, {"stranger", None}, {"", None}, | ||
| 77 | {"contributor", Contribute}, {"stranger", None}, {"", None}, | ||
| 64 | 78 | } | |
| 65 | 79 | for _, c := range cases { | |
| 66 | 80 | if got := a.RoleFor(c.user); got != c.want { | |
| @@ -69,7 +83,7 @@ func TestRoleFor(t *testing.T) { | |||
| 69 | 83 | } | |
| 70 | 84 | } | |
| 71 | 85 | ||
| 72 | func TestEffectiveRoleRaisesInstanceAdminsToRead(t *testing.T) { | ||
| 86 | func TestInstanceAdminsAreAdminEverywhere(t *testing.T) { | ||
| 73 | 87 | t.Parallel() | |
| 74 | 88 | a := &ACL{Default: "none", Entries: map[string]string{ | |
| 75 | 89 | "owner": "admin", "dev": "write", "guest": "read", | |
| @@ -79,9 +93,9 @@ func TestEffectiveRoleRaisesInstanceAdminsToRead(t *testing.T) { | |||
| 79 | 93 | instanceAdmin bool | |
| 80 | 94 | want Role | |
| 81 | 95 | }{ | |
| 82 | {"stranger", true, Read}, | ||
| 83 | {"guest", true, Read}, | ||
| 84 | {"dev", true, Maintain}, | ||
| 96 | {"stranger", true, Admin}, | ||
| 97 | {"guest", true, Admin}, | ||
| 98 | {"dev", true, Admin}, | ||
| 85 | 99 | {"owner", true, Admin}, | |
| 86 | 100 | {"stranger", false, None}, | |
| 87 | 101 | {"", true, None}, | |
| @@ -91,10 +105,56 @@ func TestEffectiveRoleRaisesInstanceAdminsToRead(t *testing.T) { | |||
| 91 | 105 | } | |
| 92 | 106 | } | |
| 93 | 107 | } | |
| 108 | |||
| 109 | func TestInstanceRolesApplyToPublicReposOnly(t *testing.T) { | ||
| 110 | t.Parallel() | ||
| 111 | roles := map[string]Role{ | ||
| 112 | "contributor": Contribute, | ||
| 113 | "maintainer": Maintain, | ||
| 114 | "admin": Admin, | ||
| 115 | } | ||
| 116 | attach := func(a *ACL) *ACL { | ||
| 117 | a.instanceRole = func(name string) Role { return roles[name] } | ||
| 118 | return a | ||
| 119 | } | ||
| 120 | |||
| 121 | private := attach(&ACL{Default: "none", Entries: map[string]string{ | ||
| 122 | "reader": "read", | ||
| 123 | }}) | ||
| 124 | if got := private.RoleFor("contributor"); got != None { | ||
| 125 | t.Fatalf("instance contributor got %v on a private repo, want none", got) | ||
| 126 | } | ||
| 127 | if got := private.RoleFor("reader"); got != Read { | ||
| 128 | t.Fatalf("explicit repo read = %v, want read", got) | ||
| 129 | } | ||
| 130 | if got := private.RoleFor("admin"); got != Admin { | ||
| 131 | t.Fatalf("instance admin got %v on a private repo, want admin", got) | ||
| 132 | } | ||
| 133 | |||
| 134 | public := attach(&ACL{Default: "read"}) | ||
| 135 | if got := public.RoleFor("contributor"); got != Contribute { | ||
| 136 | t.Fatalf("instance contributor on a public repo = %v", got) | ||
| 137 | } | ||
| 138 | if got := public.RoleFor("maintainer"); got != Maintain { | ||
| 139 | t.Fatalf("instance maintainer on a public repo = %v", got) | ||
| 140 | } | ||
| 141 | if got := public.RoleFor("stranger"); got != Read { | ||
| 142 | t.Fatalf("repository default on a public repo = %v", got) | ||
| 143 | } | ||
| 144 | |||
| 145 | raised := attach(&ACL{Default: "read", Entries: map[string]string{"maintainer": "admin"}}) | ||
| 146 | if got := raised.RoleFor("maintainer"); got != Admin { | ||
| 147 | t.Fatalf("repo entry did not raise the instance role: %v", got) | ||
| 148 | } | ||
| 149 | if got := raised.RepositoryRoleFor("contributor"); got != Read { | ||
| 150 | t.Fatalf("repository-only role for a global contributor = %v, want the default read", got) | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 94 | 154 | func TestCapabilitiesFollowTheRole(t *testing.T) { | |
| 95 | 155 | t.Parallel() | |
| 96 | 156 | a := &ACL{Default: "read", Entries: map[string]string{ | |
| 97 | "owner": "admin", "dev": "write", "guest": "read", "contributor": "contributor", | ||
| 157 | "owner": "admin", "dev": "write", "guest": "read", "contributor": "contribute", | ||
| 98 | 158 | }} | |
| 99 | 159 | for _, c := range []struct { | |
| 100 | 160 | user string | |