diff --git a/auth/authz/authz.go b/auth/authz/authz.go index f67f2ee..0902064 100644 --- a/auth/authz/authz.go +++ b/auth/authz/authz.go @@ -3,6 +3,7 @@ package authz import ( "context" "fmt" + "slices" "strings" "github.com/portward/registry-auth/auth" @@ -88,5 +89,5 @@ func (a DefaultRepositoryAuthorizer) Authorize(_ context.Context, name string, s return []string{}, nil } - return requestedActions, nil + return slices.Clone(requestedActions), nil } diff --git a/auth/scope.go b/auth/scope.go index 3df3e71..fad94b8 100644 --- a/auth/scope.go +++ b/auth/scope.go @@ -1,19 +1,38 @@ package auth import ( + "cmp" "fmt" "regexp" + "slices" "strings" - "github.com/portward/registry-auth/pkg/slices" + slicesx "github.com/portward/registry-auth/pkg/slices" ) // Scopes is a list of [Scope] instances. type Scopes []Scope +// Compare compares this with another instance of Scopes. +// It compares the values of each Scope +// and returns a value following the mechanics of [cmp.Compare]. +// +// Note that the values of Scope.Actions are always cloned and sorted before comparison, +// so this is not a cheap operation. +func (s Scopes) Compare(other Scopes) int { + return slices.CompareFunc(s, other, func(x Scope, y Scope) int { + return x.Compare(y) + }) +} + +// Equals returns true if the other instance equals to this one, otherwise it returns false. +func (s Scopes) Equals(other Scopes) bool { + return s.Compare(other) == 0 +} + func (s Scopes) String() string { // TODO: create a slices.MapToString?? - return strings.Join(slices.Map(s, func(s Scope) string { return s.String() }), " ") + return strings.Join(slicesx.Map(s, func(s Scope) string { return s.String() }), " ") } // Scope describes an access request to a specific resource. @@ -22,6 +41,35 @@ type Scope struct { Actions []string `json:"actions"` } +// Compare compares this with another instance of Scope. +// It compares the values of Resource and Actions (in this order) +// and returns a value following the mechanics of [cmp.Compare]. +// +// Note that the values of Actions are always cloned and sorted before comparison, +// so this is not a cheap operation. +func (s Scope) Compare(other Scope) int { + if result := s.Resource.Compare(other.Resource); result != 0 { + return result + } + + thisActions := slices.Clone(s.Actions) + slices.Sort(thisActions) + + otherActions := slices.Clone(other.Actions) + slices.Sort(otherActions) + + if result := slices.Compare(thisActions, otherActions); result != 0 { + return result + } + + return 0 +} + +// Equals returns true if the other instance equals to this one, otherwise it returns false. +func (s Scope) Equals(other Scope) bool { + return s.Compare(other) == 0 +} + func (s Scope) String() string { return fmt.Sprintf("%s:%s", s.Resource.String(), strings.Join(s.Actions, ",")) } @@ -32,6 +80,26 @@ type Resource struct { Name string `json:"name"` } +// Compare compares this with another instance of Resource. +// It compares the values of Type and Name (in this order) +// and returns a value following the mechanics of [cmp.Compare]. +func (r Resource) Compare(other Resource) int { + if result := cmp.Compare(r.Type, other.Type); result != 0 { + return result + } + + if result := cmp.Compare(r.Name, other.Name); result != 0 { + return result + } + + return 0 +} + +// Equals returns true if the other instance equals to this one, otherwise it returns false. +func (r Resource) Equals(other Resource) bool { + return r.Compare(other) == 0 +} + func (r Resource) String() string { return fmt.Sprintf("%s:%s", r.Type, r.Name) } @@ -39,7 +107,7 @@ func (r Resource) String() string { // ParseScopes calls ParseScope for each scope in the list. // If any of the scopes is invalid, ParseScopes returns an empty slice and an error. func ParseScopes(scopes []string) ([]Scope, error) { - return slices.TryMap(scopes, ParseScope) + return slicesx.TryMap(scopes, ParseScope) } // ParseScope parses a scope string into a formal structure according to the [Token Scope documentation]. @@ -72,7 +140,7 @@ func ParseScope(scope string) (Scope, error) { Type: resourceType, Name: resourceName, }, - Actions: slices.Map(strings.Split(actions, ","), strings.TrimSpace), + Actions: slicesx.Map(strings.Split(actions, ","), strings.TrimSpace), }, nil } diff --git a/auth/scope_test.go b/auth/scope_test.go index a183488..f445abd 100644 --- a/auth/scope_test.go +++ b/auth/scope_test.go @@ -95,6 +95,188 @@ func TestParseScope(t *testing.T) { }) } +func TestScope_CompareAndEquals(t *testing.T) { + testCases := []struct { + x auth.Scope + y auth.Scope + expected int + }{ + { + auth.Scope{}, + auth.Scope{}, + 0, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + }, + }, + 0, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + }, + 0, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull", "push"}, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull", "push"}, + }, + 0, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"push", "pull"}, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull", "push"}, + }, + 0, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "a", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "b", + }, + }, + -1, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "b", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "a", + }, + }, + 1, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "a", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "b", + }, + }, + -1, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "b", + }, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "a", + }, + }, + 1, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull"}, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"push"}, + }, + -1, + }, + { + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"push"}, + }, + auth.Scope{ + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull"}, + }, + 1, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run("", func(t *testing.T) { + actual := testCase.x.Compare(testCase.y) + + assert.Equal(t, testCase.expected, actual) + + if testCase.expected == 0 { + assert.True(t, testCase.x.Equals(testCase.y)) + } else { + assert.False(t, testCase.x.Equals(testCase.y)) + } + }) + } +} + func TestScope_String(t *testing.T) { testCases := []struct { scope auth.Scope @@ -147,3 +329,119 @@ func TestParseScopes(t *testing.T) { } }) } + +func TestScopes_CompareAndEquals(t *testing.T) { + testCases := []struct { + x auth.Scopes + y auth.Scopes + expected int + }{ + { + auth.Scopes{}, + auth.Scopes{}, + 0, + }, + { + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"pull", "push"}, + }, + }, + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo", + }, + Actions: []string{"push", "pull"}, + }, + }, + 0, + }, + { + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-a", + }, + Actions: []string{"pull", "push"}, + }, + }, + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-b", + }, + Actions: []string{"pull", "push"}, + }, + }, + -1, + }, + { + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-b", + }, + Actions: []string{"pull", "push"}, + }, + }, + auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-a", + }, + Actions: []string{"pull", "push"}, + }, + }, + 1, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run("", func(t *testing.T) { + actual := testCase.x.Compare(testCase.y) + + assert.Equal(t, testCase.expected, actual) + + if testCase.expected == 0 { + assert.True(t, testCase.x.Equals(testCase.y)) + } else { + assert.False(t, testCase.x.Equals(testCase.y)) + } + }) + } +} + +func TestScopes_String(t *testing.T) { + scopes := auth.Scopes{ + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-a", + }, + Actions: []string{"pull", "push"}, + }, + { + Resource: auth.Resource{ + Type: "repository", + Name: "path/to/repo-b", + }, + Actions: []string{"pull", "push"}, + }, + } + + const expected = "repository:path/to/repo-a:pull,push repository:path/to/repo-b:pull,push" + + assert.Equal(t, expected, scopes.String()) +} diff --git a/auth/server_test.go b/auth/server_test.go index 85397df..3063d0a 100644 --- a/auth/server_test.go +++ b/auth/server_test.go @@ -189,7 +189,7 @@ func TestAuthorizationServer(t *testing.T) { requestModifier: func(request *http.Request) { query := request.URL.Query() - query.Add("scope", "repository:name:push,pull") + query.Add("scope", "repository:name:pull,push") request.URL.RawQuery = query.Encode() }, @@ -204,12 +204,28 @@ func TestAuthorizationServer(t *testing.T) { requestModifier: func(request *http.Request) { query := request.URL.Query() - query.Add("scope", "repository:user/name:push,pull repository:user/name2:push,pull") + query.Add("scope", "repository:user/name:pull,push") + query.Add("scope", "repository:user/name2:pull,push") request.URL.RawQuery = query.Encode() }, expected: auth.TokenResponse{ - Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1c2giLCJwdWxsIHJlcG9zaXRvcnk6dXNlci9uYW1lMjpwdXNoIiwicHVsbCJdfV19.TG8kV9z-jZxPA2T15uu2Kk2AJW2t0i3J6CP7Dd_RE3i0kV9Q-5h988wXUkiKadXW_Q2bCHJ6PLNG2VIDiVjZHy9wWJi_lhf1248M55V_DqRcu2gjwW3HdG42GkrFRZR1kInUQVSy3D_A3ACnIbF_W0X11IWdntHmMEw6lYRzO-_EHNaK2W8qEhzjePlUOswts8g1FnKDPQpeclxBstcUayTZ-srZBN12cGhqfrEbtW5pFcd3pJLejUsdyo7a8wZEXkzGM_qRZumK0zmZK3V8fF2ThCD9jmAINsKBWHIP64twhqgpoUWMDbXCcCmhrDnwWNC42WP28iMaps7aagAvd1FWC-eY5Ffdtiz--90iI0dTfMRsOb3a7rl44aX553-_Wla4JeGPnezZoiLnVPeHr5yM9QyBqq0Hz3jmoDA5QCRO11aN5a5GuyHOotTPZZF-fsWy13HMMcALlPi9xQKJJMNlRiks9zjOS77du9LiPd3FsVY2faQJ6aStRZUR9Gj8Fw3b63kL0iNmwhvyEkSqyyLQg1vMOo7KY8bDsAJWIEyML_6BujKt7SOrijV2t8VLZiQwuWHbmPQiC7WcAK8HfTZMaaeUr6Mzn1r0jGp6REpofZ3eZhoeo8fZzKBt09jE_sDWAiW5RzIrv1s88NemsC1j1Dexo8gDAxODbkf6b-4", + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.o01yTLqJ-C5Ql-m3TwybCm9_DE2dVOCuFhLKuP5dbA1sN1ZauIofzRR4lfNGcBNbqE83kscRbOgbKx03tLEYo-DfRyF29B63SLzWOF6FnYX1ByOshfO5T_fRjH39QCYF_hQwEVH2aFYf7m1KNS-lX8KvW-r4oV-_mg0LmvjY_kE3ja8qJvHrt3Fl82IsfoKjJksjpQQC6ZMM0zzc4L_-D5cELfNeqg_NLzzzJB8PqC5sUpnKlO3dL1wnv08OvB2GO7rWNOjKV-4AY6ZJBvt15QXCuAEBRBP0ymulMijNBqwthI8nmhtW1miD_orCPyQfhnE7F4b1R5W6Ch4OlIAP30QAa2u5omgXfrnbD_en7KG5TNBxjq0KVm5r4UaATN2w7renvLC3QFyWcod15G0_YKiX30D25kRVosbgZqVWYUhsS-WEaBS8FDdONVrhDjwRgNVA_bYHAMcR0Enu1smcVWeuCl-pPv6aP3yDU2UcKoTZLrBmWySKexBD8cRB7Lxwdi_r2NaXbnOFyC5Rye6yizL_18ScCihFijntKE_x2qcGLlGLEDR_xCaPNV3_7S_yzN7v8jTE0geYrMH7NqJoLOtIq04yENzdOx5PIQw2DnTCY1IvG2fFzba-pyHXcnbV9ALA4FNloRtbAaMs2HkJ6NaBXSfzZ7SlNLcj7DBKf1o", + RefreshToken: "", + ExpiresIn: 900, + }, + }, + { // Note: this test relies on the internal behavior of DefaultRepositoryAuthorizer that returns requested scopes as passed to it + name: "scope actions are sorted", + requestModifier: func(request *http.Request) { + query := request.URL.Query() + + query.Add("scope", "repository:user/name:push,pull") + + request.URL.RawQuery = query.Encode() + }, + expected: auth.TokenResponse{ + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19XX0.gKkhAPCXGr3UGwW1AhxJvlHc719DVV0DhF7wVikW3nPt61QJLlglgzJTogmZC65ruW4HS1CCccGXwAGdbRw-MDCBXY-bLHpdPCCP9I0az5xDojqcJ7KelZnP_e7qnZOXyU2gBqj9LhQse20lJ7trHMnp8rIGXC9z5tfRInNBEUQnC2Bw3u6QO2aZkFnDRWKjHAd_Koj8OkMsR85v5SbFDbln5bea91LXZbEVoP1GfezR8KiL2JhVS6XCbgvOxvbMBfbeigeZtQAOZyGF-JKNEA4NjknVs2CAw8h_vEInpc6KWuTRVyLtSn_tkgvNFb-4Ikwd-Sjc_zWQiZCo3ktYnp1KpanG6AkcxFrGY_0i_2Fzs0HV5nQ4q0ydo8VHIlokd0kcpWBp9Z-yFg2tM-iOvGtUTlIzfo5P3Vdgu5_9JbK0HvwFswOIwCiuxnXnX3-6Qz_RCTgdqf_wSI6KZE2TfPJzvb5LatwHM7DBnFpavMM_pxqR4mGFGxRsWOOYJTNI2Ot0LRkE38aDXBFrXClcSKCJMrJ_sthKcH1UE2ehmdY3hkVrltiP-nZ-C-oTebr5HynHWPIom9Y7WXo2VxHlOn3u4BV5a1msw-gcaEMieWUM9rQPTg4qx8VExVdsK-tRqMtVVRt4Ysz4RSkdmHNgkh_eHTnHLTe31IHDc9PtmtU", RefreshToken: "", ExpiresIn: 900, }, @@ -219,12 +235,14 @@ func TestAuthorizationServer(t *testing.T) { requestModifier: func(request *http.Request) { query := request.URL.Query() - query.Add("scope", "repository:name:push,pull repository:user/name:push,pull repository:user/name2:push,pull") + query.Add("scope", "repository:name:pull,push") + query.Add("scope", "repository:user/name:pull,push") + query.Add("scope", "repository:user/name2:pull,push") request.URL.RawQuery = query.Encode() }, expected: auth.TokenResponse{ - Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbXX0.iOWJdvCnT5r7XyA4nftdMqdJ9GVRA7-R81HCJpG0DZK94ktt5158DXm5nGhtIsmjfu398HyP3JBOjNcJ0kgIssGILXDxBXTa3J_k85IJYS9hCuRAlwhFTTjwO-3HRmVyKKIgkesrMZL4jCYzai1dtOUbHj6WQM8EC82UeoGxZP3YOQep8z2mveS7_N2xob2fQnXBdSCW8vGO6hvHlS_LPWd0K_slzAtKV_3wKGexOGMyKyB4SVi_7mTAfhyOKtof-rnHVBtFJmpLpghFY8jOTV-tpt3kLtdw_d7RfJSv79uqzuD_VqYxG1jQB65KNqSmXl32xt1-Mv3saxjDTnewn0HnJU00kdk-iTjErgqtC2PhgGG5lBHn0b2fbXZ9-NWQ8qQVOtM1jZMBknWKi0NpEtRon7lXKUpdBxvJW_8LKlFyvcmQzkg7D8VwYQ2mg1VNOj1jpP2a3bHcCgSvobTxcaxG8i4YLl8rwN-LrV5JVWIcAjcwUPlL6Sdfk8vCpRWAD3XY2CmccMRIlvqU0VnVkr2iE9o6Y5Cpbm-acivHMO8dj1rv7pkZmTuRGktgq2kTUyZ6f-pg_0S9eLMpzGX9Ek_iCssZaS8z179aw5r-_r1v8TWImjfNEitC9hOaJcE3XifEuzIKLnlJwSzmi3gy1M5lfQB5_SeJimycbGfMy5A", + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.o01yTLqJ-C5Ql-m3TwybCm9_DE2dVOCuFhLKuP5dbA1sN1ZauIofzRR4lfNGcBNbqE83kscRbOgbKx03tLEYo-DfRyF29B63SLzWOF6FnYX1ByOshfO5T_fRjH39QCYF_hQwEVH2aFYf7m1KNS-lX8KvW-r4oV-_mg0LmvjY_kE3ja8qJvHrt3Fl82IsfoKjJksjpQQC6ZMM0zzc4L_-D5cELfNeqg_NLzzzJB8PqC5sUpnKlO3dL1wnv08OvB2GO7rWNOjKV-4AY6ZJBvt15QXCuAEBRBP0ymulMijNBqwthI8nmhtW1miD_orCPyQfhnE7F4b1R5W6Ch4OlIAP30QAa2u5omgXfrnbD_en7KG5TNBxjq0KVm5r4UaATN2w7renvLC3QFyWcod15G0_YKiX30D25kRVosbgZqVWYUhsS-WEaBS8FDdONVrhDjwRgNVA_bYHAMcR0Enu1smcVWeuCl-pPv6aP3yDU2UcKoTZLrBmWySKexBD8cRB7Lxwdi_r2NaXbnOFyC5Rye6yizL_18ScCihFijntKE_x2qcGLlGLEDR_xCaPNV3_7S_yzN7v8jTE0geYrMH7NqJoLOtIq04yENzdOx5PIQw2DnTCY1IvG2fFzba-pyHXcnbV9ALA4FNloRtbAaMs2HkJ6NaBXSfzZ7SlNLcj7DBKf1o", RefreshToken: "", ExpiresIn: 900, }, @@ -235,6 +253,8 @@ func TestAuthorizationServer(t *testing.T) { testCase := testCase t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + request := request.Clone(context.Background()) if testCase.requestModifier != nil { @@ -356,7 +376,7 @@ func TestAuthorizationServer(t *testing.T) { { name: "non-empty scope with no access", formModifier: func(form url.Values) { - form.Add("scope", "repository:name:push,pull") + form.Add("scope", "repository:name:pull,push") }, expected: auth.OAuth2Response{ Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbXX0.iOWJdvCnT5r7XyA4nftdMqdJ9GVRA7-R81HCJpG0DZK94ktt5158DXm5nGhtIsmjfu398HyP3JBOjNcJ0kgIssGILXDxBXTa3J_k85IJYS9hCuRAlwhFTTjwO-3HRmVyKKIgkesrMZL4jCYzai1dtOUbHj6WQM8EC82UeoGxZP3YOQep8z2mveS7_N2xob2fQnXBdSCW8vGO6hvHlS_LPWd0K_slzAtKV_3wKGexOGMyKyB4SVi_7mTAfhyOKtof-rnHVBtFJmpLpghFY8jOTV-tpt3kLtdw_d7RfJSv79uqzuD_VqYxG1jQB65KNqSmXl32xt1-Mv3saxjDTnewn0HnJU00kdk-iTjErgqtC2PhgGG5lBHn0b2fbXZ9-NWQ8qQVOtM1jZMBknWKi0NpEtRon7lXKUpdBxvJW_8LKlFyvcmQzkg7D8VwYQ2mg1VNOj1jpP2a3bHcCgSvobTxcaxG8i4YLl8rwN-LrV5JVWIcAjcwUPlL6Sdfk8vCpRWAD3XY2CmccMRIlvqU0VnVkr2iE9o6Y5Cpbm-acivHMO8dj1rv7pkZmTuRGktgq2kTUyZ6f-pg_0S9eLMpzGX9Ek_iCssZaS8z179aw5r-_r1v8TWImjfNEitC9hOaJcE3XifEuzIKLnlJwSzmi3gy1M5lfQB5_SeJimycbGfMy5A", @@ -368,13 +388,27 @@ func TestAuthorizationServer(t *testing.T) { { name: "non-empty scope with access", formModifier: func(form url.Values) { - form.Add("scope", "repository:user/name:push,pull") + form.Add("scope", "repository:user/name:pull,push") + form.Add("scope", "repository:user/name2:pull,push") + }, + expected: auth.OAuth2Response{ + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.o01yTLqJ-C5Ql-m3TwybCm9_DE2dVOCuFhLKuP5dbA1sN1ZauIofzRR4lfNGcBNbqE83kscRbOgbKx03tLEYo-DfRyF29B63SLzWOF6FnYX1ByOshfO5T_fRjH39QCYF_hQwEVH2aFYf7m1KNS-lX8KvW-r4oV-_mg0LmvjY_kE3ja8qJvHrt3Fl82IsfoKjJksjpQQC6ZMM0zzc4L_-D5cELfNeqg_NLzzzJB8PqC5sUpnKlO3dL1wnv08OvB2GO7rWNOjKV-4AY6ZJBvt15QXCuAEBRBP0ymulMijNBqwthI8nmhtW1miD_orCPyQfhnE7F4b1R5W6Ch4OlIAP30QAa2u5omgXfrnbD_en7KG5TNBxjq0KVm5r4UaATN2w7renvLC3QFyWcod15G0_YKiX30D25kRVosbgZqVWYUhsS-WEaBS8FDdONVrhDjwRgNVA_bYHAMcR0Enu1smcVWeuCl-pPv6aP3yDU2UcKoTZLrBmWySKexBD8cRB7Lxwdi_r2NaXbnOFyC5Rye6yizL_18ScCihFijntKE_x2qcGLlGLEDR_xCaPNV3_7S_yzN7v8jTE0geYrMH7NqJoLOtIq04yENzdOx5PIQw2DnTCY1IvG2fFzba-pyHXcnbV9ALA4FNloRtbAaMs2HkJ6NaBXSfzZ7SlNLcj7DBKf1o", + RefreshToken: "", + Scope: "repository:user/name:pull,push repository:user/name2:pull,push", + ExpiresIn: 900, + IssuedAt: "1970-01-15T13:24:54Z", + }, + }, + { // Note: this test relies on the internal behavior of DefaultRepositoryAuthorizer that returns requested scopes as passed to it + name: "scope actions are sorted", + formModifier: func(form url.Values) { + form.Add("scope", "repository:user/name:pull,push") form.Add("scope", "repository:user/name2:push,pull") }, expected: auth.OAuth2Response{ - Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1c2giLCJwdWxsIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVzaCIsInB1bGwiXX1dfQ.rqmd3HbDijaM2xe16mfRnU7Hd6-3NZY9qANL68-IjxCluKGztcoar2wLjPV5Iuh8_aM11D39z2mseRy1ZxX_bB_fMcekQL9DgI5FYiypfo6zhdIpvu3OYGx0m4y1_Uk_fe6_xHrck3rLi5Bjx6JZt-SjvQAANtfTq_g7alFNVYIJwZSLHh_TZ8EyQU7u9MbcQekmSiDuhvV_hDxnYrBLT6PACMr1COnpwFrftXeicEa4_3uD4y1EdBJn2NWUcSo7E9cD7G6tjM6lw6d6yTaJEeXFjooDU58HjfH5-8qnff5NTlTkGvukIRJSA67kdJx3IXlLAyi6soCmKVqUhPm4rDBnct1WMSjKozCveohuy974TCHBF2OpTMkpSr7ciNkOObOPEpw8_VfQc7t937ShYumXMn1ncT2kJe5DJ1NZhi6SVatoK-M08_rbnvxrQA2Vn6a6DNz10t8OIMIH0lzIVpdtvwuvpOLXt3qauBGrx-pvv90BQoo9hg1Ptr4OM3yBB7YOq3fhrm7LSbRNc1MEZDQAmmlSquJ9jMRoCy5s3kbyzZ3q8TsvpPng5-vm9SQg6ApoGSNZRrv1BnO3z-LqfxgpzQ0rYEPNZ64195a-SAxtL5HYb3kJ2Ef2dd2sbUC5icKrR0jbP59zdmyLMFAvBO8gXjsS0u9hGqxzSfj2bfA", + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.o01yTLqJ-C5Ql-m3TwybCm9_DE2dVOCuFhLKuP5dbA1sN1ZauIofzRR4lfNGcBNbqE83kscRbOgbKx03tLEYo-DfRyF29B63SLzWOF6FnYX1ByOshfO5T_fRjH39QCYF_hQwEVH2aFYf7m1KNS-lX8KvW-r4oV-_mg0LmvjY_kE3ja8qJvHrt3Fl82IsfoKjJksjpQQC6ZMM0zzc4L_-D5cELfNeqg_NLzzzJB8PqC5sUpnKlO3dL1wnv08OvB2GO7rWNOjKV-4AY6ZJBvt15QXCuAEBRBP0ymulMijNBqwthI8nmhtW1miD_orCPyQfhnE7F4b1R5W6Ch4OlIAP30QAa2u5omgXfrnbD_en7KG5TNBxjq0KVm5r4UaATN2w7renvLC3QFyWcod15G0_YKiX30D25kRVosbgZqVWYUhsS-WEaBS8FDdONVrhDjwRgNVA_bYHAMcR0Enu1smcVWeuCl-pPv6aP3yDU2UcKoTZLrBmWySKexBD8cRB7Lxwdi_r2NaXbnOFyC5Rye6yizL_18ScCihFijntKE_x2qcGLlGLEDR_xCaPNV3_7S_yzN7v8jTE0geYrMH7NqJoLOtIq04yENzdOx5PIQw2DnTCY1IvG2fFzba-pyHXcnbV9ALA4FNloRtbAaMs2HkJ6NaBXSfzZ7SlNLcj7DBKf1o", RefreshToken: "", - Scope: "repository:user/name:push,pull repository:user/name2:push,pull", + Scope: "repository:user/name:pull,push repository:user/name2:pull,push", ExpiresIn: 900, IssuedAt: "1970-01-15T13:24:54Z", }, @@ -382,14 +416,14 @@ func TestAuthorizationServer(t *testing.T) { { name: "non-empty scope with mixed access", formModifier: func(form url.Values) { - form.Add("scope", "repository:name:push,pull") - form.Add("scope", "repository:user/name:push,pull") - form.Add("scope", "repository:user/name2:push,pull") + form.Add("scope", "repository:name:pull,push") + form.Add("scope", "repository:user/name:pull,push") + form.Add("scope", "repository:user/name2:pull,push") }, expected: auth.OAuth2Response{ - Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1c2giLCJwdWxsIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVzaCIsInB1bGwiXX1dfQ.rqmd3HbDijaM2xe16mfRnU7Hd6-3NZY9qANL68-IjxCluKGztcoar2wLjPV5Iuh8_aM11D39z2mseRy1ZxX_bB_fMcekQL9DgI5FYiypfo6zhdIpvu3OYGx0m4y1_Uk_fe6_xHrck3rLi5Bjx6JZt-SjvQAANtfTq_g7alFNVYIJwZSLHh_TZ8EyQU7u9MbcQekmSiDuhvV_hDxnYrBLT6PACMr1COnpwFrftXeicEa4_3uD4y1EdBJn2NWUcSo7E9cD7G6tjM6lw6d6yTaJEeXFjooDU58HjfH5-8qnff5NTlTkGvukIRJSA67kdJx3IXlLAyi6soCmKVqUhPm4rDBnct1WMSjKozCveohuy974TCHBF2OpTMkpSr7ciNkOObOPEpw8_VfQc7t937ShYumXMn1ncT2kJe5DJ1NZhi6SVatoK-M08_rbnvxrQA2Vn6a6DNz10t8OIMIH0lzIVpdtvwuvpOLXt3qauBGrx-pvv90BQoo9hg1Ptr4OM3yBB7YOq3fhrm7LSbRNc1MEZDQAmmlSquJ9jMRoCy5s3kbyzZ3q8TsvpPng5-vm9SQg6ApoGSNZRrv1BnO3z-LqfxgpzQ0rYEPNZ64195a-SAxtL5HYb3kJ2Ef2dd2sbUC5icKrR0jbP59zdmyLMFAvBO8gXjsS0u9hGqxzSfj2bfA", + Token: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6IjdCVE06NllVRDpYSE00OjRNWUY6Qk1RWTo2N05YOkFTWVE6VVVBRjo2N1FaOlA3SjY6SktJMjpaT0FBIiwia3R5IjoiUlNBIiwibiI6Ind0bDROcC1YM3Z0cUotZU1oaXc5SWhkRzkyclR5Ukg1c05QVmZsZmZGUHlvZnMyLWtJT0R2bVlOWmFwckRMNHlBU2lvR2k2SkFHamlIcVV5d1JyMUtmTGhsX3RpWGt3YndNalBkZmxwUURuMXpjTC1uWjdkRU1VZVU4WTN0ekN3TVg2bHBVLVd2MDFmNERHNk85eFAzQXJnN0lCNVM0ZmdTXzhCTE5tREhZaUZmSFlzSHBhMFI2Wk10UV9VcG9yTXJDcDlnR0VaYkswbkVnTnZyWTFCel9ZRUtRUFZZNUxRTTdfZFoxMWcwS3hibGpBa3hmZnVoY0RUNE9rN1FTdnRGWHVTbFBINktNbDdtYjRJaERkaHRzbHU3YnExV3lkdmEwSmtwajQ5QlFuci13VkJHZU5ROFJHSUhXaGJqWE5uNzVMdF9rNGZCOUxnRGViQmRTNkpiSUlEUUNheHU3dmpnUE9EN2tDcUVxRVFYR0VjMHdzNlZ3MlAzLUF0NXhzNHJnVFhNYVU4NmdpVXExVXFGOE0zWFRDcEtXLTgyaHN6NjRIZk1IVUNpbVpiX2pnM205N3A2Wm9oU0tSaHlSWjRyLW05U0hzMnVBSXJkZmYzOGhLcEVGUWJCTWs1SkN5a05sTDViQWxNbjItZmpQZHdjMV9TWi1Db3hIQjlrVlhoZTRIRTdYU185bXJhTUdwZlVEOGY0OTBwZFZOVkd2NHVyenJSMDMxZ3RRbzg4SWRsb2ZkRTBGOFpBQWp6a3dUS1c3WGRpMzJXTUdRNlE1b3F6amxfc1V2OUV4Qy1pc2R6MklHX3RHU184M0gxN1N0RERsd0Jpa21iMEYxQUZNM2s2RzB1SzhzVFg5RElhS1pEVXFJU1BrM1ZaV1JCR0s1N3l1MEk5S3haeFRVIn0sInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyIiwiYXVkIjpbInNlcnZpY2UuZXhhbXBsZS5jb20iXSwiZXhwIjoxMjU4Nzk0LCJuYmYiOjEyNTc4OTQsImlhdCI6MTI1Nzg5NCwianRpIjoiYWNjZXNzLXRva2VuIiwiYWNjZXNzIjpbeyJ0eXBlIjoicmVwb3NpdG9yeSIsIm5hbWUiOiJ1c2VyL25hbWUiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoidXNlci9uYW1lMiIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.o01yTLqJ-C5Ql-m3TwybCm9_DE2dVOCuFhLKuP5dbA1sN1ZauIofzRR4lfNGcBNbqE83kscRbOgbKx03tLEYo-DfRyF29B63SLzWOF6FnYX1ByOshfO5T_fRjH39QCYF_hQwEVH2aFYf7m1KNS-lX8KvW-r4oV-_mg0LmvjY_kE3ja8qJvHrt3Fl82IsfoKjJksjpQQC6ZMM0zzc4L_-D5cELfNeqg_NLzzzJB8PqC5sUpnKlO3dL1wnv08OvB2GO7rWNOjKV-4AY6ZJBvt15QXCuAEBRBP0ymulMijNBqwthI8nmhtW1miD_orCPyQfhnE7F4b1R5W6Ch4OlIAP30QAa2u5omgXfrnbD_en7KG5TNBxjq0KVm5r4UaATN2w7renvLC3QFyWcod15G0_YKiX30D25kRVosbgZqVWYUhsS-WEaBS8FDdONVrhDjwRgNVA_bYHAMcR0Enu1smcVWeuCl-pPv6aP3yDU2UcKoTZLrBmWySKexBD8cRB7Lxwdi_r2NaXbnOFyC5Rye6yizL_18ScCihFijntKE_x2qcGLlGLEDR_xCaPNV3_7S_yzN7v8jTE0geYrMH7NqJoLOtIq04yENzdOx5PIQw2DnTCY1IvG2fFzba-pyHXcnbV9ALA4FNloRtbAaMs2HkJ6NaBXSfzZ7SlNLcj7DBKf1o", RefreshToken: "", - Scope: "repository:user/name:push,pull repository:user/name2:push,pull", + Scope: "repository:user/name:pull,push repository:user/name2:pull,push", ExpiresIn: 900, IssuedAt: "1970-01-15T13:24:54Z", }, @@ -425,6 +459,8 @@ func TestAuthorizationServer(t *testing.T) { testCase := testCase t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + request := request.Clone(context.Background()) form := maps.Clone(form) diff --git a/auth/service.go b/auth/service.go index 74546cd..b2e81b0 100644 --- a/auth/service.go +++ b/auth/service.go @@ -190,6 +190,11 @@ func (s AuthorizationServiceImpl) TokenHandler(ctx context.Context, r TokenReque return TokenResponse{}, err } + // Sort actions to make sure tokens are more consistent + for _, scope := range grantedScopes { + slices.Sort(scope.Actions) + } + token, err := s.TokenIssuer.IssueAccessToken(ctx, r.Service, subject, grantedScopes) if err != nil { return TokenResponse{}, err @@ -247,6 +252,11 @@ func (s AuthorizationServiceImpl) OAuth2Handler(ctx context.Context, r OAuth2Req return OAuth2Response{}, err } + // Sort actions to make sure tokens are more consistent + for _, scope := range grantedScopes { + slices.Sort(scope.Actions) + } + token, err := s.TokenIssuer.IssueAccessToken(ctx, r.Service, subject, grantedScopes) if err != nil { return OAuth2Response{}, err