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  }