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