github.com/demonoid81/containerd@v1.3.4/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  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"net/url"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/errdefs"
    33  	"github.com/containerd/containerd/log"
    34  	"github.com/pkg/errors"
    35  	"github.com/sirupsen/logrus"
    36  	"golang.org/x/net/context/ctxhttp"
    37  )
    38  
    39  type dockerAuthorizer struct {
    40  	credentials func(string) (string, string, error)
    41  
    42  	client *http.Client
    43  	header http.Header
    44  	mu     sync.Mutex
    45  
    46  	// indexed by host name
    47  	handlers map[string]*authHandler
    48  }
    49  
    50  // NewAuthorizer creates a Docker authorizer using the provided function to
    51  // get credentials for the token server or basic auth.
    52  // Deprecated: Use NewDockerAuthorizer
    53  func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
    54  	return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
    55  }
    56  
    57  type authorizerConfig struct {
    58  	credentials func(string) (string, string, error)
    59  	client      *http.Client
    60  	header      http.Header
    61  }
    62  
    63  // AuthorizerOpt configures an authorizer
    64  type AuthorizerOpt func(*authorizerConfig)
    65  
    66  // WithAuthClient provides the HTTP client for the authorizer
    67  func WithAuthClient(client *http.Client) AuthorizerOpt {
    68  	return func(opt *authorizerConfig) {
    69  		opt.client = client
    70  	}
    71  }
    72  
    73  // WithAuthCreds provides a credential function to the authorizer
    74  func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
    75  	return func(opt *authorizerConfig) {
    76  		opt.credentials = creds
    77  	}
    78  }
    79  
    80  // WithAuthHeader provides HTTP headers for authorization
    81  func WithAuthHeader(hdr http.Header) AuthorizerOpt {
    82  	return func(opt *authorizerConfig) {
    83  		opt.header = hdr
    84  	}
    85  }
    86  
    87  // NewDockerAuthorizer creates an authorizer using Docker's registry
    88  // authentication spec.
    89  // See https://docs.docker.com/registry/spec/auth/
    90  func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
    91  	var ao authorizerConfig
    92  	for _, opt := range opts {
    93  		opt(&ao)
    94  	}
    95  
    96  	if ao.client == nil {
    97  		ao.client = http.DefaultClient
    98  	}
    99  
   100  	return &dockerAuthorizer{
   101  		credentials: ao.credentials,
   102  		client:      ao.client,
   103  		header:      ao.header,
   104  		handlers:    make(map[string]*authHandler),
   105  	}
   106  }
   107  
   108  // Authorize handles auth request.
   109  func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
   110  	// skip if there is no auth handler
   111  	ah := a.getAuthHandler(req.URL.Host)
   112  	if ah == nil {
   113  		return nil
   114  	}
   115  
   116  	auth, err := ah.authorize(ctx)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	req.Header.Set("Authorization", auth)
   122  	return nil
   123  }
   124  
   125  func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler {
   126  	a.mu.Lock()
   127  	defer a.mu.Unlock()
   128  
   129  	return a.handlers[host]
   130  }
   131  
   132  func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
   133  	last := responses[len(responses)-1]
   134  	host := last.Request.URL.Host
   135  
   136  	a.mu.Lock()
   137  	defer a.mu.Unlock()
   138  	for _, c := range parseAuthHeader(last.Header) {
   139  		if c.scheme == bearerAuth {
   140  			if err := invalidAuthorization(c, responses); err != nil {
   141  				delete(a.handlers, host)
   142  				return err
   143  			}
   144  
   145  			// reuse existing handler
   146  			//
   147  			// assume that one registry will return the common
   148  			// challenge information, including realm and service.
   149  			// and the resource scope is only different part
   150  			// which can be provided by each request.
   151  			if _, ok := a.handlers[host]; ok {
   152  				return nil
   153  			}
   154  
   155  			common, err := a.generateTokenOptions(ctx, host, c)
   156  			if err != nil {
   157  				return err
   158  			}
   159  
   160  			a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
   161  			return nil
   162  		} else if c.scheme == basicAuth && a.credentials != nil {
   163  			username, secret, err := a.credentials(host)
   164  			if err != nil {
   165  				return err
   166  			}
   167  
   168  			if username != "" && secret != "" {
   169  				common := tokenOptions{
   170  					username: username,
   171  					secret:   secret,
   172  				}
   173  
   174  				a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
   175  				return nil
   176  			}
   177  		}
   178  	}
   179  	return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
   180  }
   181  
   182  func (a *dockerAuthorizer) generateTokenOptions(ctx context.Context, host string, c challenge) (tokenOptions, error) {
   183  	realm, ok := c.parameters["realm"]
   184  	if !ok {
   185  		return tokenOptions{}, errors.New("no realm specified for token auth challenge")
   186  	}
   187  
   188  	realmURL, err := url.Parse(realm)
   189  	if err != nil {
   190  		return tokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
   191  	}
   192  
   193  	to := tokenOptions{
   194  		realm:   realmURL.String(),
   195  		service: c.parameters["service"],
   196  	}
   197  
   198  	scope, ok := c.parameters["scope"]
   199  	if !ok {
   200  		return tokenOptions{}, errors.Errorf("no scope specified for token auth challenge")
   201  	}
   202  	to.scopes = append(to.scopes, scope)
   203  
   204  	if a.credentials != nil {
   205  		to.username, to.secret, err = a.credentials(host)
   206  		if err != nil {
   207  			return tokenOptions{}, err
   208  		}
   209  	}
   210  	return to, nil
   211  }
   212  
   213  // authResult is used to control limit rate.
   214  type authResult struct {
   215  	sync.WaitGroup
   216  	token string
   217  	err   error
   218  }
   219  
   220  // authHandler is used to handle auth request per registry server.
   221  type authHandler struct {
   222  	sync.Mutex
   223  
   224  	header http.Header
   225  
   226  	client *http.Client
   227  
   228  	// only support basic and bearer schemes
   229  	scheme authenticationScheme
   230  
   231  	// common contains common challenge answer
   232  	common tokenOptions
   233  
   234  	// scopedTokens caches token indexed by scopes, which used in
   235  	// bearer auth case
   236  	scopedTokens map[string]*authResult
   237  }
   238  
   239  func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
   240  	return &authHandler{
   241  		header:       hdr,
   242  		client:       client,
   243  		scheme:       scheme,
   244  		common:       opts,
   245  		scopedTokens: map[string]*authResult{},
   246  	}
   247  }
   248  
   249  func (ah *authHandler) authorize(ctx context.Context) (string, error) {
   250  	switch ah.scheme {
   251  	case basicAuth:
   252  		return ah.doBasicAuth(ctx)
   253  	case bearerAuth:
   254  		return ah.doBearerAuth(ctx)
   255  	default:
   256  		return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
   257  	}
   258  }
   259  
   260  func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
   261  	username, secret := ah.common.username, ah.common.secret
   262  
   263  	if username == "" || secret == "" {
   264  		return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
   265  	}
   266  
   267  	auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
   268  	return fmt.Sprintf("Basic %s", auth), nil
   269  }
   270  
   271  func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) {
   272  	// copy common tokenOptions
   273  	to := ah.common
   274  
   275  	to.scopes = getTokenScopes(ctx, to.scopes)
   276  	if len(to.scopes) == 0 {
   277  		return "", errors.Errorf("no scope specified for token auth challenge")
   278  	}
   279  
   280  	// Docs: https://docs.docker.com/registry/spec/auth/scope
   281  	scoped := strings.Join(to.scopes, " ")
   282  
   283  	ah.Lock()
   284  	if r, exist := ah.scopedTokens[scoped]; exist {
   285  		ah.Unlock()
   286  		r.Wait()
   287  		return r.token, r.err
   288  	}
   289  
   290  	// only one fetch token job
   291  	r := new(authResult)
   292  	r.Add(1)
   293  	ah.scopedTokens[scoped] = r
   294  	ah.Unlock()
   295  
   296  	// fetch token for the resource scope
   297  	var (
   298  		token string
   299  		err   error
   300  	)
   301  	if to.secret != "" {
   302  		// credential information is provided, use oauth POST endpoint
   303  		token, err = ah.fetchTokenWithOAuth(ctx, to)
   304  		err = errors.Wrap(err, "failed to fetch oauth token")
   305  	} else {
   306  		// do request anonymously
   307  		token, err = ah.fetchToken(ctx, to)
   308  		err = errors.Wrap(err, "failed to fetch anonymous token")
   309  	}
   310  	token = fmt.Sprintf("Bearer %s", token)
   311  
   312  	r.token, r.err = token, err
   313  	r.Done()
   314  	return r.token, r.err
   315  }
   316  
   317  type tokenOptions struct {
   318  	realm    string
   319  	service  string
   320  	scopes   []string
   321  	username string
   322  	secret   string
   323  }
   324  
   325  type postTokenResponse struct {
   326  	AccessToken  string    `json:"access_token"`
   327  	RefreshToken string    `json:"refresh_token"`
   328  	ExpiresIn    int       `json:"expires_in"`
   329  	IssuedAt     time.Time `json:"issued_at"`
   330  	Scope        string    `json:"scope"`
   331  }
   332  
   333  func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
   334  	form := url.Values{}
   335  	form.Set("scope", strings.Join(to.scopes, " "))
   336  	form.Set("service", to.service)
   337  	// TODO: Allow setting client_id
   338  	form.Set("client_id", "containerd-client")
   339  
   340  	if to.username == "" {
   341  		form.Set("grant_type", "refresh_token")
   342  		form.Set("refresh_token", to.secret)
   343  	} else {
   344  		form.Set("grant_type", "password")
   345  		form.Set("username", to.username)
   346  		form.Set("password", to.secret)
   347  	}
   348  
   349  	req, err := http.NewRequest("POST", to.realm, strings.NewReader(form.Encode()))
   350  	if err != nil {
   351  		return "", err
   352  	}
   353  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
   354  	if ah.header != nil {
   355  		for k, v := range ah.header {
   356  			req.Header[k] = append(req.Header[k], v...)
   357  		}
   358  	}
   359  
   360  	resp, err := ctxhttp.Do(ctx, ah.client, req)
   361  	if err != nil {
   362  		return "", err
   363  	}
   364  	defer resp.Body.Close()
   365  
   366  	// Registries without support for POST may return 404 for POST /v2/token.
   367  	// As of September 2017, GCR is known to return 404.
   368  	// As of February 2018, JFrog Artifactory is known to return 401.
   369  	if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
   370  		return ah.fetchToken(ctx, to)
   371  	} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
   372  		b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
   373  		log.G(ctx).WithFields(logrus.Fields{
   374  			"status": resp.Status,
   375  			"body":   string(b),
   376  		}).Debugf("token request failed")
   377  		// TODO: handle error body and write debug output
   378  		return "", errors.Errorf("unexpected status: %s", resp.Status)
   379  	}
   380  
   381  	decoder := json.NewDecoder(resp.Body)
   382  
   383  	var tr postTokenResponse
   384  	if err = decoder.Decode(&tr); err != nil {
   385  		return "", fmt.Errorf("unable to decode token response: %s", err)
   386  	}
   387  
   388  	return tr.AccessToken, nil
   389  }
   390  
   391  type getTokenResponse struct {
   392  	Token        string    `json:"token"`
   393  	AccessToken  string    `json:"access_token"`
   394  	ExpiresIn    int       `json:"expires_in"`
   395  	IssuedAt     time.Time `json:"issued_at"`
   396  	RefreshToken string    `json:"refresh_token"`
   397  }
   398  
   399  // fetchToken fetches a token using a GET request
   400  func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
   401  	req, err := http.NewRequest("GET", to.realm, nil)
   402  	if err != nil {
   403  		return "", err
   404  	}
   405  
   406  	if ah.header != nil {
   407  		for k, v := range ah.header {
   408  			req.Header[k] = append(req.Header[k], v...)
   409  		}
   410  	}
   411  
   412  	reqParams := req.URL.Query()
   413  
   414  	if to.service != "" {
   415  		reqParams.Add("service", to.service)
   416  	}
   417  
   418  	for _, scope := range to.scopes {
   419  		reqParams.Add("scope", scope)
   420  	}
   421  
   422  	if to.secret != "" {
   423  		req.SetBasicAuth(to.username, to.secret)
   424  	}
   425  
   426  	req.URL.RawQuery = reqParams.Encode()
   427  
   428  	resp, err := ctxhttp.Do(ctx, ah.client, req)
   429  	if err != nil {
   430  		return "", err
   431  	}
   432  	defer resp.Body.Close()
   433  
   434  	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
   435  		// TODO: handle error body and write debug output
   436  		return "", errors.Errorf("unexpected status: %s", resp.Status)
   437  	}
   438  
   439  	decoder := json.NewDecoder(resp.Body)
   440  
   441  	var tr getTokenResponse
   442  	if err = decoder.Decode(&tr); err != nil {
   443  		return "", fmt.Errorf("unable to decode token response: %s", err)
   444  	}
   445  
   446  	// `access_token` is equivalent to `token` and if both are specified
   447  	// the choice is undefined.  Canonicalize `access_token` by sticking
   448  	// things in `token`.
   449  	if tr.AccessToken != "" {
   450  		tr.Token = tr.AccessToken
   451  	}
   452  
   453  	if tr.Token == "" {
   454  		return "", ErrNoToken
   455  	}
   456  
   457  	return tr.Token, nil
   458  }
   459  
   460  func invalidAuthorization(c challenge, responses []*http.Response) error {
   461  	errStr := c.parameters["error"]
   462  	if errStr == "" {
   463  		return nil
   464  	}
   465  
   466  	n := len(responses)
   467  	if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
   468  		return nil
   469  	}
   470  
   471  	return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
   472  }
   473  
   474  func sameRequest(r1, r2 *http.Request) bool {
   475  	if r1.Method != r2.Method {
   476  		return false
   477  	}
   478  	if *r1.URL != *r2.URL {
   479  		return false
   480  	}
   481  	return true
   482  }