github.com/nikkelma/oras-project_oras-go@v1.1.1-0.20220201001104-a75f6a419090/pkg/registry/remote/auth/scope.go (about) 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 }