v2.31.0-dev8 ec8c5b9

auth_test.go RawHistoryFolder

1package domain
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/assert"
7	"github.com/stretchr/testify/require"
8)
9
10func TestParseScope(t *testing.T) {
11	tests := []struct {
12		name        string
13		input       string
14		wantScope   *Scope
15		wantErr     bool
16		errContains string
17	}{
18		// Valid cases
19		{
20			name:  "simple repository scope",
21			input: "repository:myrepo:pull",
22			wantScope: &Scope{
23				Type:    "repository",
24				Name:    "myrepo",
25				Actions: []string{"pull"},
26			},
27		},
28		{
29			name:  "multiple actions",
30			input: "repository:myrepo:push,pull",
31			wantScope: &Scope{
32				Type:    "repository",
33				Name:    "myrepo",
34				Actions: []string{"push", "pull"},
35			},
36		},
37		{
38			name:  "wildcard repository",
39			input: "repository:*:pull",
40			wantScope: &Scope{
41				Type:    "repository",
42				Name:    "*",
43				Actions: []string{"pull"},
44			},
45		},
46		{
47			name:  "org wildcard",
48			input: "repository:myorg/*:push,pull",
49			wantScope: &Scope{
50				Type:    "repository",
51				Name:    "myorg/*",
52				Actions: []string{"push", "pull"},
53			},
54		},
55		{
56			name:  "wildcard action",
57			input: "repository:myrepo:*",
58			wantScope: &Scope{
59				Type:    "repository",
60				Name:    "myrepo",
61				Actions: []string{"*"},
62			},
63		},
64		{
65			name:  "nested repository name",
66			input: "repository:myorg/team/app:pull",
67			wantScope: &Scope{
68				Type:    "repository",
69				Name:    "myorg/team/app",
70				Actions: []string{"pull"},
71			},
72		},
73		{
74			name:  "actions with whitespace trimmed",
75			input: "repository:myrepo:push, pull",
76			wantScope: &Scope{
77				Type:    "repository",
78				Name:    "myrepo",
79				Actions: []string{"push", "pull"},
80			},
81		},
82		{
83			name:  "registry scope type",
84			input: "registry:catalog:*",
85			wantScope: &Scope{
86				Type:    "registry",
87				Name:    "catalog",
88				Actions: []string{"*"},
89			},
90		},
91
92		// Invalid cases
93		{
94			name:        "empty string",
95			input:       "",
96			wantErr:     true,
97			errContains: "invalid scope format",
98		},
99		{
100			name:        "missing actions",
101			input:       "repository:myrepo",
102			wantErr:     true,
103			errContains: "invalid scope format",
104		},
105		{
106			name:        "missing name and actions",
107			input:       "repository",
108			wantErr:     true,
109			errContains: "invalid scope format",
110		},
111		{
112			name:  "only type and name",
113			input: "repository:myrepo:",
114			wantScope: &Scope{
115				Type:    "repository",
116				Name:    "myrepo",
117				Actions: []string{""},
118			},
119		},
120	}
121
122	for _, tt := range tests {
123		t.Run(tt.name, func(t *testing.T) {
124			scope, err := ParseScope(tt.input)
125
126			if tt.wantErr {
127				require.Error(t, err)
128				if tt.errContains != "" {
129					assert.Contains(t, err.Error(), tt.errContains)
130				}
131				return
132			}
133
134			require.NoError(t, err)
135			assert.Equal(t, tt.wantScope.Type, scope.Type)
136			assert.Equal(t, tt.wantScope.Name, scope.Name)
137			assert.Equal(t, tt.wantScope.Actions, scope.Actions)
138		})
139	}
140}
141
142func TestScope_CanAccess(t *testing.T) {
143	tests := []struct {
144		name     string
145		scope    *Scope
146		repoName string
147		action   string
148		want     bool
149	}{
150		// Exact match cases
151		{
152			name:     "exact repo match with pull",
153			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
154			repoName: "myrepo",
155			action:   "pull",
156			want:     true,
157		},
158		{
159			name:     "exact repo match with push",
160			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push"}},
161			repoName: "myrepo",
162			action:   "push",
163			want:     true,
164		},
165		{
166			name:     "exact repo match wrong action",
167			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
168			repoName: "myrepo",
169			action:   "push",
170			want:     false,
171		},
172		{
173			name:     "wrong repo name",
174			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
175			repoName: "otherrepo",
176			action:   "pull",
177			want:     false,
178		},
179
180		// Wildcard repository cases
181		{
182			name:     "wildcard repo matches any",
183			scope:    &Scope{Type: "repository", Name: "*", Actions: []string{"pull"}},
184			repoName: "anyrepo",
185			action:   "pull",
186			want:     true,
187		},
188		{
189			name:     "wildcard repo matches nested",
190			scope:    &Scope{Type: "repository", Name: "*", Actions: []string{"pull"}},
191			repoName: "myorg/myapp",
192			action:   "pull",
193			want:     true,
194		},
195
196		// Org-level wildcard cases
197		{
198			name:     "org wildcard matches repo in org",
199			scope:    &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
200			repoName: "myorg/myapp",
201			action:   "pull",
202			want:     true,
203		},
204		{
205			name:     "org wildcard matches nested repo in org",
206			scope:    &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
207			repoName: "myorg/team/myapp",
208			action:   "pull",
209			want:     true,
210		},
211		{
212			name:     "org wildcard does not match other org",
213			scope:    &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
214			repoName: "otherorg/myapp",
215			action:   "pull",
216			want:     false,
217		},
218		{
219			name:     "org wildcard does not match repo without slash",
220			scope:    &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
221			repoName: "myorg",
222			action:   "pull",
223			want:     false,
224		},
225
226		// Wildcard action cases
227		{
228			name:     "wildcard action grants pull",
229			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"*"}},
230			repoName: "myrepo",
231			action:   "pull",
232			want:     true,
233		},
234		{
235			name:     "wildcard action grants push",
236			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"*"}},
237			repoName: "myrepo",
238			action:   "push",
239			want:     true,
240		},
241
242		// Multiple actions
243		{
244			name:     "multiple actions includes requested",
245			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
246			repoName: "myrepo",
247			action:   "pull",
248			want:     true,
249		},
250		{
251			name:     "multiple actions includes push",
252			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
253			repoName: "myrepo",
254			action:   "push",
255			want:     true,
256		},
257
258		// Non-repository type
259		{
260			name:     "registry type does not grant repo access",
261			scope:    &Scope{Type: "registry", Name: "catalog", Actions: []string{"*"}},
262			repoName: "myrepo",
263			action:   "pull",
264			want:     false,
265		},
266
267		// Edge cases
268		{
269			name:     "empty actions",
270			scope:    &Scope{Type: "repository", Name: "myrepo", Actions: []string{}},
271			repoName: "myrepo",
272			action:   "pull",
273			want:     false,
274		},
275		{
276			name:     "empty repo name in scope",
277			scope:    &Scope{Type: "repository", Name: "", Actions: []string{"pull"}},
278			repoName: "myrepo",
279			action:   "pull",
280			want:     false,
281		},
282	}
283
284	for _, tt := range tests {
285		t.Run(tt.name, func(t *testing.T) {
286			got := tt.scope.CanAccess(tt.repoName, tt.action)
287			assert.Equal(t, tt.want, got)
288		})
289	}
290}
291
292func TestScope_String(t *testing.T) {
293	tests := []struct {
294		name  string
295		scope *Scope
296		want  string
297	}{
298		{
299			name:  "simple scope",
300			scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
301			want:  "repository:myrepo:pull",
302		},
303		{
304			name:  "multiple actions",
305			scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
306			want:  "repository:myrepo:push,pull",
307		},
308		{
309			name:  "wildcard",
310			scope: &Scope{Type: "repository", Name: "*", Actions: []string{"*"}},
311			want:  "repository:*:*",
312		},
313		{
314			name:  "org wildcard",
315			scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"push", "pull"}},
316			want:  "repository:myorg/*:push,pull",
317		},
318	}
319
320	for _, tt := range tests {
321		t.Run(tt.name, func(t *testing.T) {
322			got := tt.scope.String()
323			assert.Equal(t, tt.want, got)
324		})
325	}
326}
327
328func TestScopesGrantRegistryAccess(t *testing.T) {
329	tests := []struct {
330		name     string
331		granted  []string
332		repoName string
333		action   string
334		want     bool
335	}{
336		{
337			name:     "wildcard shorthand grants any action",
338			granted:  []string{"*"},
339			repoName: "myrepo",
340			action:   "push",
341			want:     true,
342		},
343		{
344			name:     "pull shorthand grants pull",
345			granted:  []string{"pull"},
346			repoName: "myrepo",
347			action:   "pull",
348			want:     true,
349		},
350		{
351			name:     "pull shorthand does not grant push",
352			granted:  []string{"pull"},
353			repoName: "myrepo",
354			action:   "push",
355			want:     false,
356		},
357		{
358			name:     "push shorthand grants push",
359			granted:  []string{"push"},
360			repoName: "myrepo",
361			action:   "push",
362			want:     true,
363		},
364		{
365			name:     "full scope grants matching repo and action",
366			granted:  []string{"repository:myrepo:push,pull"},
367			repoName: "myrepo",
368			action:   "pull",
369			want:     true,
370		},
371		{
372			name:     "full scope does not grant different repo",
373			granted:  []string{"repository:myrepo:push,pull"},
374			repoName: "other",
375			action:   "pull",
376			want:     false,
377		},
378		{
379			name:     "wildcard repo scope grants any repo",
380			granted:  []string{"repository:*:pull"},
381			repoName: "anything",
382			action:   "pull",
383			want:     true,
384		},
385		{
386			name:     "org wildcard scope grants repo in org",
387			granted:  []string{"repository:myorg/*:push"},
388			repoName: "myorg/app",
389			action:   "push",
390			want:     true,
391		},
392		{
393			name:     "org wildcard scope rejects repo outside org",
394			granted:  []string{"repository:myorg/*:push"},
395			repoName: "other/app",
396			action:   "push",
397			want:     false,
398		},
399		{
400			name:     "admin scope is ignored for registry access",
401			granted:  []string{"admin:routes:read"},
402			repoName: "myrepo",
403			action:   "pull",
404			want:     false,
405		},
406		{
407			name:     "no granted scopes",
408			granted:  []string{},
409			repoName: "myrepo",
410			action:   "pull",
411			want:     false,
412		},
413		{
414			name:     "whitespace around scope is trimmed",
415			granted:  []string{"  repository:myrepo:pull  "},
416			repoName: "myrepo",
417			action:   "pull",
418			want:     true,
419		},
420		{
421			name:     "invalid scope string is skipped",
422			granted:  []string{"garbage", "repository:myrepo:pull"},
423			repoName: "myrepo",
424			action:   "pull",
425			want:     true,
426		},
427	}
428
429	for _, tt := range tests {
430		t.Run(tt.name, func(t *testing.T) {
431			got := ScopesGrantRegistryAccess(tt.granted, tt.repoName, tt.action)
432			assert.Equal(t, tt.want, got)
433		})
434	}
435}
436
437func TestScopesGrantAdminAccess(t *testing.T) {
438	tests := []struct {
439		name     string
440		granted  []string
441		resource string
442		action   string
443		want     bool
444	}{
445		{
446			name:     "wildcard shorthand grants any admin access",
447			granted:  []string{"*"},
448			resource: "routes",
449			action:   "read",
450			want:     true,
451		},
452		{
453			name:     "exact admin scope grants matching resource and action",
454			granted:  []string{"admin:routes:read"},
455			resource: "routes",
456			action:   "read",
457			want:     true,
458		},
459		{
460			name:     "exact admin scope denies wrong action",
461			granted:  []string{"admin:routes:read"},
462			resource: "routes",
463			action:   "write",
464			want:     false,
465		},
466		{
467			name:     "exact admin scope denies wrong resource",
468			granted:  []string{"admin:routes:read"},
469			resource: "secrets",
470			action:   "read",
471			want:     false,
472		},
473		{
474			name:     "admin wildcard resource grants any resource",
475			granted:  []string{"admin:*:read"},
476			resource: "secrets",
477			action:   "read",
478			want:     true,
479		},
480		{
481			name:     "admin wildcard action grants any action",
482			granted:  []string{"admin:routes:*"},
483			resource: "routes",
484			action:   "write",
485			want:     true,
486		},
487		{
488			name:     "full admin wildcard grants everything",
489			granted:  []string{"admin:*:*"},
490			resource: "config",
491			action:   "write",
492			want:     true,
493		},
494		{
495			name:     "repository scope is ignored for admin access",
496			granted:  []string{"repository:myrepo:push"},
497			resource: "routes",
498			action:   "read",
499			want:     false,
500		},
501		{
502			name:     "no granted scopes",
503			granted:  []string{},
504			resource: "routes",
505			action:   "read",
506			want:     false,
507		},
508		{
509			name:     "whitespace around scope is trimmed",
510			granted:  []string{"  admin:routes:read  "},
511			resource: "routes",
512			action:   "read",
513			want:     true,
514		},
515	}
516
517	for _, tt := range tests {
518		t.Run(tt.name, func(t *testing.T) {
519			got := ScopesGrantAdminAccess(tt.granted, tt.resource, tt.action)
520			assert.Equal(t, tt.want, got)
521		})
522	}
523}
524
525func TestParseScope_RoundTrip(t *testing.T) {
526	// Test that parsing and stringifying produces the same result
527	inputs := []string{
528		"repository:myrepo:pull",
529		"repository:myrepo:push,pull",
530		"repository:*:*",
531		"repository:myorg/*:push,pull",
532		"repository:myorg/team/app:pull",
533	}
534
535	for _, input := range inputs {
536		t.Run(input, func(t *testing.T) {
537			scope, err := ParseScope(input)
538			require.NoError(t, err)
539
540			output := scope.String()
541			assert.Equal(t, input, output)
542		})
543	}
544}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close