...

Source file src/oras.land/oras-go/pkg/registry/remote/auth/scope.go

Documentation: oras.land/oras-go/pkg/registry/remote/auth

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  package auth
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  	"strings"
    21  )
    22  
    23  // Actions used in scopes.
    24  // Reference: https://docs.docker.com/registry/spec/auth/scope/
    25  const (
    26  	// ActionPull represents generic read access for resources of the repository
    27  	// type.
    28  	ActionPull = "pull"
    29  
    30  	// ActionPush represents generic write access for resources of the
    31  	// repository type.
    32  	ActionPush = "push"
    33  
    34  	// ActionDelete represents the delete permission for resources of the
    35  	// repository type.
    36  	ActionDelete = "delete"
    37  )
    38  
    39  // ScopeRegistryCatalog is the scope for registry catalog access.
    40  const ScopeRegistryCatalog = "registry:catalog:*"
    41  
    42  // ScopeRepository returns a repository scope with given actions.
    43  // Reference: https://docs.docker.com/registry/spec/auth/scope/
    44  func ScopeRepository(repository string, actions ...string) string {
    45  	actions = cleanActions(actions)
    46  	if repository == "" || len(actions) == 0 {
    47  		return ""
    48  	}
    49  	return strings.Join([]string{
    50  		"repository",
    51  		repository,
    52  		strings.Join(actions, ","),
    53  	}, ":")
    54  }
    55  
    56  // scopesContextKey is the context key for scopes.
    57  type scopesContextKey struct{}
    58  
    59  // WithScopes returns a context with scopes added. Scopes are de-duplicated.
    60  // Scopes are used as hints for the auth client to fetch bearer tokens with
    61  // larger scopes.
    62  // For example, uploading blob to the repository "hello-world" does HEAD request
    63  // first then POST and PUT. The HEAD request will return a challenge for scope
    64  // `repository:hello-world:pull`, and the auth client will fetch a token for
    65  // that challenge. Later, the POST request will return a challenge for scope
    66  // `repository:hello-world:push`, and the auth client will fetch a token for
    67  // that challenge again. By invoking `WithScopes()` with the scope
    68  // `repository:hello-world:pull,push`, the auth client with cache is hinted to
    69  // fetch a token via a single token fetch request for all the HEAD, POST, PUT
    70  // requests.
    71  // Passing an empty list of scopes will virtually remove the scope hints in the
    72  // context.
    73  // Reference: https://docs.docker.com/registry/spec/auth/scope/
    74  func WithScopes(ctx context.Context, scopes ...string) context.Context {
    75  	scopes = CleanScopes(scopes)
    76  	return context.WithValue(ctx, scopesContextKey{}, scopes)
    77  }
    78  
    79  // AppendScopes appends additional scopes to the existing scopes in the context
    80  // and returns a new context. The resulted scopes are de-duplicated.
    81  // The append operation does modify the existing scope in the context passed in.
    82  func AppendScopes(ctx context.Context, scopes ...string) context.Context {
    83  	if len(scopes) == 0 {
    84  		return ctx
    85  	}
    86  	return WithScopes(ctx, append(GetScopes(ctx), scopes...)...)
    87  }
    88  
    89  // GetScopes returns the scopes in the context.
    90  func GetScopes(ctx context.Context) []string {
    91  	if scopes, ok := ctx.Value(scopesContextKey{}).([]string); ok {
    92  		return append([]string(nil), scopes...)
    93  	}
    94  	return nil
    95  }
    96  
    97  // CleanScopes merges and sort the actions in ascending order if the scopes have
    98  // the same resource type and name. The final scopes are sorted in ascending
    99  // order. In other words, the scopes passed in are de-duplicated and sorted.
   100  // Therefore, the output of this function is deterministic.
   101  // If there is a wildcard `*` in the action, other actions in the same resource
   102  // type and name are ignored.
   103  func CleanScopes(scopes []string) []string {
   104  	// fast paths
   105  	switch len(scopes) {
   106  	case 0:
   107  		return nil
   108  	case 1:
   109  		scope := scopes[0]
   110  		i := strings.LastIndex(scope, ":")
   111  		if i == -1 {
   112  			return []string{scope}
   113  		}
   114  		actionList := strings.Split(scope[i+1:], ",")
   115  		actionList = cleanActions(actionList)
   116  		if len(actionList) == 0 {
   117  			return nil
   118  		}
   119  		actions := strings.Join(actionList, ",")
   120  		scope = scope[:i+1] + actions
   121  		return []string{scope}
   122  	}
   123  
   124  	// slow path
   125  	var result []string
   126  
   127  	// merge recognizable scopes
   128  	resourceTypes := make(map[string]map[string]map[string]struct{})
   129  	for _, scope := range scopes {
   130  		// extract resource type
   131  		i := strings.Index(scope, ":")
   132  		if i == -1 {
   133  			result = append(result, scope)
   134  			continue
   135  		}
   136  		resourceType := scope[:i]
   137  
   138  		// extract resource name and actions
   139  		rest := scope[i+1:]
   140  		i = strings.LastIndex(rest, ":")
   141  		if i == -1 {
   142  			result = append(result, scope)
   143  			continue
   144  		}
   145  		resourceName := rest[:i]
   146  		actions := rest[i+1:]
   147  		if actions == "" {
   148  			// drop scope since no action found
   149  			continue
   150  		}
   151  
   152  		// add to the intermediate map for de-duplication
   153  		namedActions := resourceTypes[resourceType]
   154  		if namedActions == nil {
   155  			namedActions = make(map[string]map[string]struct{})
   156  			resourceTypes[resourceType] = namedActions
   157  		}
   158  		actionSet := namedActions[resourceName]
   159  		if actionSet == nil {
   160  			actionSet = make(map[string]struct{})
   161  			namedActions[resourceName] = actionSet
   162  		}
   163  		for _, action := range strings.Split(actions, ",") {
   164  			if action != "" {
   165  				actionSet[action] = struct{}{}
   166  			}
   167  		}
   168  	}
   169  
   170  	// reconstruct scopes
   171  	for resourceType, namedActions := range resourceTypes {
   172  		for resourceName, actionSet := range namedActions {
   173  			if len(actionSet) == 0 {
   174  				continue
   175  			}
   176  			var actions []string
   177  			for action := range actionSet {
   178  				if action == "*" {
   179  					actions = []string{"*"}
   180  					break
   181  				}
   182  				actions = append(actions, action)
   183  			}
   184  			sort.Strings(actions)
   185  			scope := resourceType + ":" + resourceName + ":" + strings.Join(actions, ",")
   186  			result = append(result, scope)
   187  		}
   188  	}
   189  
   190  	// sort and return
   191  	sort.Strings(result)
   192  	return result
   193  }
   194  
   195  // cleanActions removes the duplicated actions and sort in ascending order.
   196  // If there is a wildcard `*` in the action, other actions are ignored.
   197  func cleanActions(actions []string) []string {
   198  	// fast paths
   199  	switch len(actions) {
   200  	case 0:
   201  		return nil
   202  	case 1:
   203  		if actions[0] == "" {
   204  			return nil
   205  		}
   206  		return actions
   207  	}
   208  
   209  	// slow path
   210  	sort.Strings(actions)
   211  	n := 0
   212  	for i := 0; i < len(actions); i++ {
   213  		if actions[i] == "*" {
   214  			return []string{"*"}
   215  		}
   216  		if actions[i] != actions[n] {
   217  			n++
   218  			if n != i {
   219  				actions[n] = actions[i]
   220  			}
   221  		}
   222  	}
   223  	n++
   224  	if actions[0] == "" {
   225  		if n == 1 {
   226  			return nil
   227  		}
   228  		return actions[1:n]
   229  	}
   230  	return actions[:n]
   231  }
   232  

View as plain text