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