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.

Blame · forge/access/acl_test.go

Line attribution on main · 376 line(s)

c5902451
Keenan Nemetz
16d ago
1 package access
653e0cfc
Keenan Nemetz
18d ago
2
93c7cae8
Keenan Nemetz
19h ago
3 import (
4 "strings"
5 "testing"
6 )
653e0cfc
Keenan Nemetz
18d ago
7
8 func TestRoleOrdering(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
9 t.Parallel()
5b814e99
Keenan Nemetz
14d ago
10 if !Admin.AtLeast(Maintain) || !Maintain.AtLeast(Contribute) || !Contribute.AtLeast(Read) ||
11 Read.AtLeast(Contribute) || Contribute.AtLeast(Maintain) || Maintain != Write {
653e0cfc
Keenan Nemetz
18d ago
12 t.Fatal("role ordering is wrong")
13 }
14 for _, c := range []struct {
15 in string
16 want Role
17 ok bool
18 }{
19 {"none", None, true},
20 {"", None, true},
21 {"read", Read, true},
5b814e99
Keenan Nemetz
14d ago
22 {"contributor", Contribute, true},
23 {"contribute", Contribute, true},
24 {"contrib", Contribute, true},
6c84d387
Keenan Nemetz
15d ago
25 {"maintain", Maintain, true},
26 {"maintainer", Maintain, true},
27 {"write", Maintain, true},
653e0cfc
Keenan Nemetz
18d ago
28 {"admin", Admin, true},
29 {"owner", Admin, true},
30 {"ADMIN", Admin, true},
31 {"root", None, false},
32 } {
33 got, err := ParseRole(c.in)
34 if c.ok != (err == nil) {
35 t.Fatalf("ParseRole(%q) err=%v want ok=%v", c.in, err, c.ok)
36 }
37 if c.ok && got != c.want {
38 t.Fatalf("ParseRole(%q)=%v want %v", c.in, got, c.want)
39 }
40 }
41 }
6c84d387
Keenan Nemetz
15d ago
42 func TestWriteIsTheHistoricalSpellingOfMaintain(t *testing.T) {
43 t.Parallel()
44 if got, err := ParseRole("write"); err != nil || got != Maintain || got.String() != "maintain" {
45 t.Fatalf("ParseRole(write) = %v, %v", got, err)
46 }
47 a, err := Parse([]byte(`{"version":1,"default":"write","entries":{"alice":"write"}}`))
48 if err != nil {
49 t.Fatal(err)
50 }
51 if a.Default != "maintain" || a.Entries["alice"] != "maintain" {
52 t.Fatalf("old spelling was not normalized: %+v", a)
53 }
54 }
55
5b814e99
Keenan Nemetz
14d ago
56 func TestContributorIsTheHistoricalSpellingOfContribute(t *testing.T) {
57 t.Parallel()
58 if Contributor != Contribute || Contribute.String() != "contribute" {
59 t.Fatalf("Contribute = %v, alias = %v", Contribute, Contributor)
60 }
61 a, err := Parse([]byte(`{"version":1,"default":"contributor","entries":{"alice":"contributor"}}`))
62 if err != nil {
63 t.Fatal(err)
64 }
65 if a.Default != "contribute" || a.Entries["alice"] != "contribute" {
66 t.Fatalf("old spelling was not normalized: %+v", a)
67 }
68 }
69
653e0cfc
Keenan Nemetz
18d ago
70 func TestRoleFor(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
71 t.Parallel()
653e0cfc
Keenan Nemetz
18d ago
72 a := &ACL{Default: "none", Entries: map[string]string{
5b814e99
Keenan Nemetz
14d ago
73 "owner": "admin", "dev": "write", "guest": "read", "contributor": "contribute",
653e0cfc
Keenan Nemetz
18d ago
74 }}
75 cases := []struct {
76 user string
77 want Role
78 }{
6c84d387
Keenan Nemetz
15d ago
79 {"owner", Admin}, {"dev", Maintain}, {"guest", Read},
5b814e99
Keenan Nemetz
14d ago
80 {"contributor", Contribute}, {"stranger", None}, {"", None},
653e0cfc
Keenan Nemetz
18d ago
81 }
82 for _, c := range cases {
83 if got := a.RoleFor(c.user); got != c.want {
84 t.Errorf("RoleFor(%q)=%v want %v", c.user, got, c.want)
85 }
86 }
87 }
381d6aac
Keenan Nemetz
14d ago
88
5b814e99
Keenan Nemetz
14d ago
89 func TestInstanceAdminsAreAdminEverywhere(t *testing.T) {
381d6aac
Keenan Nemetz
14d ago
90 t.Parallel()
91 a := &ACL{Default: "none", Entries: map[string]string{
92 "owner": "admin", "dev": "write", "guest": "read",
93 }}
94 for _, c := range []struct {
95 user string
96 instanceAdmin bool
97 want Role
98 }{
5b814e99
Keenan Nemetz
14d ago
99 {"stranger", true, Admin},
100 {"guest", true, Admin},
101 {"dev", true, Admin},
381d6aac
Keenan Nemetz
14d ago
102 {"owner", true, Admin},
103 {"stranger", false, None},
104 {"", true, None},
105 } {
106 if got := a.EffectiveRole(c.user, c.instanceAdmin); got != c.want {
107 t.Errorf("EffectiveRole(%q, %t)=%v want %v", c.user, c.instanceAdmin, got, c.want)
108 }
109 }
110 }
5b814e99
Keenan Nemetz
14d ago
111
112 func TestInstanceRolesApplyToPublicReposOnly(t *testing.T) {
113 t.Parallel()
114 roles := map[string]Role{
115 "contributor": Contribute,
116 "maintainer": Maintain,
117 "admin": Admin,
118 }
119 attach := func(a *ACL) *ACL {
120 a.instanceRole = func(name string) Role { return roles[name] }
121 return a
122 }
123
124 private := attach(&ACL{Default: "none", Entries: map[string]string{
125 "reader": "read",
126 }})
127 if got := private.RoleFor("contributor"); got != None {
128 t.Fatalf("instance contributor got %v on a private repo, want none", got)
129 }
130 if got := private.RoleFor("reader"); got != Read {
131 t.Fatalf("explicit repo read = %v, want read", got)
132 }
133 if got := private.RoleFor("admin"); got != Admin {
134 t.Fatalf("instance admin got %v on a private repo, want admin", got)
135 }
136
137 public := attach(&ACL{Default: "read"})
138 if got := public.RoleFor("contributor"); got != Contribute {
139 t.Fatalf("instance contributor on a public repo = %v", got)
140 }
141 if got := public.RoleFor("maintainer"); got != Maintain {
142 t.Fatalf("instance maintainer on a public repo = %v", got)
143 }
144 if got := public.RoleFor("stranger"); got != Read {
145 t.Fatalf("repository default on a public repo = %v", got)
146 }
147
148 raised := attach(&ACL{Default: "read", Entries: map[string]string{"maintainer": "admin"}})
149 if got := raised.RoleFor("maintainer"); got != Admin {
150 t.Fatalf("repo entry did not raise the instance role: %v", got)
151 }
152 if got := raised.RepositoryRoleFor("contributor"); got != Read {
153 t.Fatalf("repository-only role for a global contributor = %v, want the default read", got)
154 }
155 }
156
6c84d387
Keenan Nemetz
15d ago
157 func TestCapabilitiesFollowTheRole(t *testing.T) {
158 t.Parallel()
159 a := &ACL{Default: "read", Entries: map[string]string{
5b814e99
Keenan Nemetz
14d ago
160 "owner": "admin", "dev": "write", "guest": "read", "contributor": "contribute",
6c84d387
Keenan Nemetz
15d ago
161 }}
162 for _, c := range []struct {
163 user string
164 discuss, push, triage, approve bool
165 }{
166 {"owner", true, true, true, true},
167 {"dev", true, true, true, true},
168 {"contributor", true, true, false, false},
169 {"guest", false, false, false, false},
170 {"stranger", false, false, false, false},
171 } {
172 if got := a.CanDiscuss(c.user); got != c.discuss {
173 t.Errorf("CanDiscuss(%q)=%v want %v", c.user, got, c.discuss)
174 }
175 if got := a.CanOpenRequest(c.user); got != c.discuss {
176 t.Errorf("CanOpenRequest(%q)=%v want %v", c.user, got, c.discuss)
177 }
178 if got := a.CanPush(c.user); got != c.push {
179 t.Errorf("CanPush(%q)=%v want %v", c.user, got, c.push)
180 }
181 if got := a.CanTriage(c.user); got != c.triage {
182 t.Errorf("CanTriage(%q)=%v want %v", c.user, got, c.triage)
183 }
184 if got := a.CanApprove(c.user); got != c.approve {
185 t.Errorf("CanApprove(%q)=%v want %v", c.user, got, c.approve)
186 }
187 }
188 }
189
190 func TestContributorRefNamespace(t *testing.T) {
191 t.Parallel()
192 own := "refs/heads/contrib/alice/topic"
474a812d
Keenan Nemetz
15d ago
193 if name, ok := ContributorRefName(own); !ok || name != "alice" {
194 t.Fatalf("ContributorRefName(%q)=%q,%v", own, name, ok)
6c84d387
Keenan Nemetz
15d ago
195 }
474a812d
Keenan Nemetz
15d ago
196 if _, ok := ContributorRefName("refs/heads/contrib/alice"); ok {
6c84d387
Keenan Nemetz
15d ago
197 t.Fatal("an empty contributor namespace parsed as a branch")
198 }
474a812d
Keenan Nemetz
15d ago
199 if !IsContributorRef("alice", own) {
200 t.Fatalf("alice cannot push %s", own)
6c84d387
Keenan Nemetz
15d ago
201 }
202 for _, ref := range []string{
203 "refs/heads/main",
204 "refs/heads/contrib/bob/topic",
205 "refs/heads/contrib/alice",
206 "refs/tags/contrib/alice/v1",
207 "refs/heads/vole",
208 } {
474a812d
Keenan Nemetz
15d ago
209 if IsContributorRef("alice", ref) {
210 t.Errorf("alice was allowed to push %s", ref)
6c84d387
Keenan Nemetz
15d ago
211 }
212 }
474a812d
Keenan Nemetz
15d ago
213 if IsContributorRef("bob", own) {
6c84d387
Keenan Nemetz
15d ago
214 t.Fatal("a different account could push alice's namespace")
215 }
474a812d
Keenan Nemetz
15d ago
216 // A name is a whole path component: alice's namespace is not alice-b's.
217 if IsContributorRef("alice", "refs/heads/contrib/alice-b/topic") {
218 t.Fatal("the namespace prefix matched a longer account name")
219 }
220 if got := ContributorBranchPrefix("alice"); got != "refs/heads/contrib/alice/" {
221 t.Fatalf("ContributorBranchPrefix = %q", got)
222 }
6c84d387
Keenan Nemetz
15d ago
223 }
224
653e0cfc
Keenan Nemetz
18d ago
225 func TestDefaultAppliesToUnknownUsers(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
226 t.Parallel()
653e0cfc
Keenan Nemetz
18d ago
227 a := &ACL{Default: "read", Entries: map[string]string{"owner": "admin"}}
228 if !a.Can("anyone", Read) {
229 t.Fatal("default read should grant read to an unknown user")
230 }
6c84d387
Keenan Nemetz
15d ago
231 if a.Can("anyone", Maintain) {
653e0cfc
Keenan Nemetz
18d ago
232 t.Fatal("default read must not grant write")
233 }
6c84d387
Keenan Nemetz
15d ago
234 if !a.Can("owner", Maintain) {
653e0cfc
Keenan Nemetz
18d ago
235 t.Fatal("explicit entry should win")
236 }
237 }
238 func TestIsProtected(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
239 t.Parallel()
653e0cfc
Keenan Nemetz
18d ago
240 a := &ACL{Protected: []string{"refs/heads/main", "refs/heads/release/*"}}
241 for ref, want := range map[string]bool{
242 "refs/heads/main": true,
243 "refs/heads/mainline": false,
244 "refs/heads/release/1.0": true,
245 "refs/heads/release": false,
246 "refs/heads/feature/x": false,
247 } {
248 if got := a.IsProtected(ref); got != want {
249 t.Errorf("IsProtected(%q)=%v want %v", ref, got, want)
250 }
251 }
252 }
93c7cae8
Keenan Nemetz
19h ago
253
254 // A pattern IsProtected cannot match must be refused where it is written: today
255 // the repository reads as protected while the branch is open.
256 func TestValidateProtectedPattern(t *testing.T) {
257 t.Parallel()
258 valid := []string{
259 "refs/heads/main",
260 "refs/heads/release/*",
261 "refs/heads/release",
262 "refs/tags/v1.*",
263 "refs/*",
264 "refs/heads/*",
265 }
266 for _, pattern := range valid {
267 if err := ValidateProtectedPattern(pattern); err != nil {
268 t.Errorf("ValidateProtectedPattern(%q) = %v, want it accepted", pattern, err)
269 }
270 }
271 invalid := map[string]string{
272 "": "empty",
273 "refs/head/main": "refs/heads/main",
274 "refs/tag/v1.0": "refs/tags/v1.0",
275 "heads/main": "refs/heads/main",
276 "main": "refs/",
277 " refs/heads/main": "whitespace",
278 "refs/heads/ma in": "space",
279 "refs/heads/": "refs/heads/*",
280 "refs/heads": "refs/heads/*",
281 "refs": "refs/",
282 "refs/heads/ma*in": `"*"`,
283 "refs/heads/ma**": `"*"`,
284 "refs/heads/../main": `".."`,
285 "refs/heads/main.lock": ".lock",
286 "refs/heads/main.": "dot",
287 "refs/heads//main": "empty path component",
288 }
289 for pattern, want := range invalid {
290 err := ValidateProtectedPattern(pattern)
291 if err == nil {
292 t.Errorf("ValidateProtectedPattern(%q) was accepted", pattern)
293 continue
294 }
295 if !strings.Contains(err.Error(), want) {
296 t.Errorf("ValidateProtectedPattern(%q) = %q, want it to mention %q", pattern, err, want)
297 }
298 }
299 }
300
301 // Every accepted pattern must round-trip unchanged and still protect what the
302 // operator asked for, or validation has only moved the bug.
303 func TestAcceptedPatternsRoundTrip(t *testing.T) {
304 t.Parallel()
305 patterns := []string{"refs/heads/main", "refs/heads/release/*", "refs/tags/v1.*"}
306 if err := ValidateProtectedPatterns(patterns); err != nil {
307 t.Fatal(err)
308 }
309 a := &ACL{Protected: append([]string(nil), patterns...)}
310 if got := strings.Join(a.Protected, ","); got != strings.Join(patterns, ",") {
311 t.Fatalf("patterns = %q", got)
312 }
313 for _, ref := range []string{"refs/heads/main", "refs/heads/release/1.0", "refs/tags/v1.2"} {
314 if !a.IsProtected(ref) {
315 t.Errorf("%q is not protected", ref)
316 }
317 }
318 if a.IsProtected("refs/heads/mainline") {
319 t.Error("an accepted exact pattern protects a longer ref")
320 }
321 }
322
653e0cfc
Keenan Nemetz
18d ago
323 func TestJSONRoundTrip(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
324 t.Parallel()
653e0cfc
Keenan Nemetz
18d ago
325 a := Empty("keenan")
326 a.Protected = []string{"refs/heads/main"}
0789f22e
Keenan Nemetz
9d ago
327 a.RequireLinkedIssue = true
653e0cfc
Keenan Nemetz
18d ago
328 b, err := a.Bytes()
329 if err != nil {
330 t.Fatal(err)
331 }
332 got, err := Parse(b)
333 if err != nil {
334 t.Fatal(err)
335 }
336 if got.RoleFor("keenan") != Admin {
337 t.Fatalf("owner lost admin: %s", b)
338 }
339 if !got.IsProtected("refs/heads/main") {
340 t.Fatal("protection lost")
341 }
0789f22e
Keenan Nemetz
9d ago
342 if !got.RequiresLinkedIssue() {
343 t.Fatal("linked-issue rule lost")
344 }
653e0cfc
Keenan Nemetz
18d ago
345 }
346 func TestParseRejectsBadRoles(t *testing.T) {
44530e74
Keenan Nemetz
16d ago
347 t.Parallel()
653e0cfc
Keenan Nemetz
18d ago
348 if _, err := Parse([]byte(`{"version":1,"default":"root"}`)); err == nil {
349 t.Fatal("expected error for unknown default role")
350 }
351 if _, err := Parse([]byte(`{"version":1,"entries":{"x":"king"}}`)); err == nil {
352 t.Fatal("expected error for unknown entry role")
353 }
354 }
e5478085
Keenan Nemetz
11d ago
355
356 // A combined result derived from a contributor head is still untrusted code. The
357 // runner's cache namespace has to treat it as the contributor's, or that build could
358 // seed the cache a protected branch uses.
359 func TestTrustNamespaceFollowsAContributorCandidate(t *testing.T) {
360 t.Parallel()
361 cases := map[string]string{
362 "refs/heads/main": "direct",
363 "refs/heads/feature": "direct",
364 "refs/tags/v1.0.0": "direct",
365 "refs/heads/contrib/alice/fix": "contrib-alice",
366 "refs/vole/candidates/REQ/main-abc": "direct",
367 "refs/vole/candidates/contrib/alice/REQ/main-abc-def": "contrib-alice",
368 "refs/vole/candidates/contrib/alice": "direct",
369 "refs/vole/candidates/contrib/alice/": "contrib-alice",
370 }
371 for ref, want := range cases {
372 if got := TrustNamespace(ref); got != want {
373 t.Errorf("TrustNamespace(%q) = %q, want %q", ref, got, want)
374 }
375 }
376 }