github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/authorizer.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package docker 18 19 import ( 20 "context" 21 "encoding/base64" 22 "fmt" 23 "net/http" 24 "strings" 25 "sync" 26 27 "github.com/containerd/containerd/errdefs" 28 "github.com/containerd/containerd/log" 29 "github.com/containerd/containerd/remotes/docker/auth" 30 remoteerrors "github.com/containerd/containerd/remotes/errors" 31 "github.com/pkg/errors" 32 "github.com/sirupsen/logrus" 33 ) 34 35 type dockerAuthorizer struct { 36 credentials func(string) (string, string, error) 37 38 client *http.Client 39 header http.Header 40 mu sync.Mutex 41 42 // indexed by host name 43 handlers map[string]*authHandler 44 } 45 46 // NewAuthorizer creates a Docker authorizer using the provided function to 47 // get credentials for the token server or basic auth. 48 // Deprecated: Use NewDockerAuthorizer 49 func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer { 50 return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f)) 51 } 52 53 type authorizerConfig struct { 54 credentials func(string) (string, string, error) 55 client *http.Client 56 header http.Header 57 } 58 59 // AuthorizerOpt configures an authorizer 60 type AuthorizerOpt func(*authorizerConfig) 61 62 // WithAuthClient provides the HTTP client for the authorizer 63 func WithAuthClient(client *http.Client) AuthorizerOpt { 64 return func(opt *authorizerConfig) { 65 opt.client = client 66 } 67 } 68 69 // WithAuthCreds provides a credential function to the authorizer 70 func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt { 71 return func(opt *authorizerConfig) { 72 opt.credentials = creds 73 } 74 } 75 76 // WithAuthHeader provides HTTP headers for authorization 77 func WithAuthHeader(hdr http.Header) AuthorizerOpt { 78 return func(opt *authorizerConfig) { 79 opt.header = hdr 80 } 81 } 82 83 // NewDockerAuthorizer creates an authorizer using Docker's registry 84 // authentication spec. 85 // See https://docs.docker.com/registry/spec/auth/ 86 func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer { 87 var ao authorizerConfig 88 for _, opt := range opts { 89 opt(&ao) 90 } 91 92 if ao.client == nil { 93 ao.client = http.DefaultClient 94 } 95 96 return &dockerAuthorizer{ 97 credentials: ao.credentials, 98 client: ao.client, 99 header: ao.header, 100 handlers: make(map[string]*authHandler), 101 } 102 } 103 104 // Authorize handles auth request. 105 func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error { 106 // skip if there is no auth handler 107 ah := a.getAuthHandler(req.URL.Host) 108 if ah == nil { 109 return nil 110 } 111 112 auth, err := ah.authorize(ctx) 113 if err != nil { 114 return err 115 } 116 117 req.Header.Set("Authorization", auth) 118 return nil 119 } 120 121 func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler { 122 a.mu.Lock() 123 defer a.mu.Unlock() 124 125 return a.handlers[host] 126 } 127 128 func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error { 129 last := responses[len(responses)-1] 130 host := last.Request.URL.Host 131 132 a.mu.Lock() 133 defer a.mu.Unlock() 134 for _, c := range auth.ParseAuthHeader(last.Header) { 135 if c.Scheme == auth.BearerAuth { 136 if err := invalidAuthorization(c, responses); err != nil { 137 delete(a.handlers, host) 138 return err 139 } 140 141 // reuse existing handler 142 // 143 // assume that one registry will return the common 144 // challenge information, including realm and service. 145 // and the resource scope is only different part 146 // which can be provided by each request. 147 if _, ok := a.handlers[host]; ok { 148 return nil 149 } 150 151 var username, secret string 152 if a.credentials != nil { 153 var err error 154 username, secret, err = a.credentials(host) 155 if err != nil { 156 return err 157 } 158 } 159 160 common, err := auth.GenerateTokenOptions(ctx, host, username, secret, c) 161 if err != nil { 162 return err 163 } 164 165 a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common) 166 return nil 167 } else if c.Scheme == auth.BasicAuth && a.credentials != nil { 168 username, secret, err := a.credentials(host) 169 if err != nil { 170 return err 171 } 172 173 if username != "" && secret != "" { 174 common := auth.TokenOptions{ 175 Username: username, 176 Secret: secret, 177 } 178 179 a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common) 180 return nil 181 } 182 } 183 } 184 return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme") 185 } 186 187 // authResult is used to control limit rate. 188 type authResult struct { 189 sync.WaitGroup 190 token string 191 err error 192 } 193 194 // authHandler is used to handle auth request per registry server. 195 type authHandler struct { 196 sync.Mutex 197 198 header http.Header 199 200 client *http.Client 201 202 // only support basic and bearer schemes 203 scheme auth.AuthenticationScheme 204 205 // common contains common challenge answer 206 common auth.TokenOptions 207 208 // scopedTokens caches token indexed by scopes, which used in 209 // bearer auth case 210 scopedTokens map[string]*authResult 211 } 212 213 func newAuthHandler(client *http.Client, hdr http.Header, scheme auth.AuthenticationScheme, opts auth.TokenOptions) *authHandler { 214 return &authHandler{ 215 header: hdr, 216 client: client, 217 scheme: scheme, 218 common: opts, 219 scopedTokens: map[string]*authResult{}, 220 } 221 } 222 223 func (ah *authHandler) authorize(ctx context.Context) (string, error) { 224 switch ah.scheme { 225 case auth.BasicAuth: 226 return ah.doBasicAuth(ctx) 227 case auth.BearerAuth: 228 return ah.doBearerAuth(ctx) 229 default: 230 return "", errors.Wrapf(errdefs.ErrNotImplemented, "failed to find supported auth scheme: %s", string(ah.scheme)) 231 } 232 } 233 234 func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) { 235 username, secret := ah.common.Username, ah.common.Secret 236 237 if username == "" || secret == "" { 238 return "", fmt.Errorf("failed to handle basic auth because missing username or secret") 239 } 240 241 auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret)) 242 return fmt.Sprintf("Basic %s", auth), nil 243 } 244 245 func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err error) { 246 // copy common tokenOptions 247 to := ah.common 248 249 to.Scopes = GetTokenScopes(ctx, to.Scopes) 250 251 // Docs: https://docs.docker.com/registry/spec/auth/scope 252 scoped := strings.Join(to.Scopes, " ") 253 254 ah.Lock() 255 if r, exist := ah.scopedTokens[scoped]; exist { 256 ah.Unlock() 257 r.Wait() 258 return r.token, r.err 259 } 260 261 // only one fetch token job 262 r := new(authResult) 263 r.Add(1) 264 ah.scopedTokens[scoped] = r 265 ah.Unlock() 266 267 defer func() { 268 token = fmt.Sprintf("Bearer %s", token) 269 r.token, r.err = token, err 270 r.Done() 271 }() 272 273 // fetch token for the resource scope 274 if to.Secret != "" { 275 defer func() { 276 err = errors.Wrap(err, "failed to fetch oauth token") 277 }() 278 // credential information is provided, use oauth POST endpoint 279 // TODO: Allow setting client_id 280 resp, err := auth.FetchTokenWithOAuth(ctx, ah.client, ah.header, "containerd-client", to) 281 if err != nil { 282 var errStatus remoteerrors.ErrUnexpectedStatus 283 if errors.As(err, &errStatus) { 284 // Registries without support for POST may return 404 for POST /v2/token. 285 // As of September 2017, GCR is known to return 404. 286 // As of February 2018, JFrog Artifactory is known to return 401. 287 if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 { 288 resp, err := auth.FetchToken(ctx, ah.client, ah.header, to) 289 if err != nil { 290 return "", err 291 } 292 return resp.Token, nil 293 } 294 log.G(ctx).WithFields(logrus.Fields{ 295 "status": errStatus.Status, 296 "body": string(errStatus.Body), 297 }).Debugf("token request failed") 298 } 299 return "", err 300 } 301 return resp.AccessToken, nil 302 } 303 // do request anonymously 304 resp, err := auth.FetchToken(ctx, ah.client, ah.header, to) 305 if err != nil { 306 return "", errors.Wrap(err, "failed to fetch anonymous token") 307 } 308 return resp.Token, nil 309 } 310 311 func invalidAuthorization(c auth.Challenge, responses []*http.Response) error { 312 errStr := c.Parameters["error"] 313 if errStr == "" { 314 return nil 315 } 316 317 n := len(responses) 318 if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) { 319 return nil 320 } 321 322 return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr) 323 } 324 325 func sameRequest(r1, r2 *http.Request) bool { 326 if r1.Method != r2.Method { 327 return false 328 } 329 if *r1.URL != *r2.URL { 330 return false 331 } 332 return true 333 }