github.com/lalkh/containerd@v1.4.3/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  		to.scopes = append(to.scopes, scope)
   201  	} else {
   202  		log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
   203  	}
   204  
   205  	if a.credentials != nil {
   206  		to.username, to.secret, err = a.credentials(host)
   207  		if err != nil {
   208  			return tokenOptions{}, err
   209  		}
   210  	}
   211  	return to, nil
   212  }
   213  
   214  // authResult is used to control limit rate.
   215  type authResult struct {
   216  	sync.WaitGroup
   217  	token string
   218  	err   error
   219  }
   220  
   221  // authHandler is used to handle auth request per registry server.
   222  type authHandler struct {
   223  	sync.Mutex
   224  
   225  	header http.Header
   226  
   227  	client *http.Client
   228  
   229  	// only support basic and bearer schemes
   230  	scheme authenticationScheme
   231  
   232  	// common contains common challenge answer
   233  	common tokenOptions
   234  
   235  	// scopedTokens caches token indexed by scopes, which used in
   236  	// bearer auth case
   237  	scopedTokens map[string]*authResult
   238  }
   239  
   240  func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
   241  	return &authHandler{
   242  		header:       hdr,
   243  		client:       client,
   244  		scheme:       scheme,
   245  		common:       opts,
   246  		scopedTokens: map[string]*authResult{},
   247  	}
   248  }
   249  
   250  func (ah *authHandler) authorize(ctx context.Context) (string, error) {
   251  	switch ah.scheme {
   252  	case basicAuth:
   253  		return ah.doBasicAuth(ctx)
   254  	case bearerAuth:
   255  		return ah.doBearerAuth(ctx)
   256  	default:
   257  		return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
   258  	}
   259  }
   260  
   261  func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
   262  	username, secret := ah.common.username, ah.common.secret
   263  
   264  	if username == "" || secret == "" {
   265  		return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
   266  	}
   267  
   268  	auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
   269  	return fmt.Sprintf("Basic %s", auth), nil
   270  }
   271  
   272  func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) {
   273  	// copy common tokenOptions
   274  	to := ah.common
   275  
   276  	to.scopes = GetTokenScopes(ctx, to.scopes)
   277  
   278  	// Docs: https://docs.docker.com/registry/spec/auth/scope
   279  	scoped := strings.Join(to.scopes, " ")
   280  
   281  	ah.Lock()
   282  	if r, exist := ah.scopedTokens[scoped]; exist {
   283  		ah.Unlock()
   284  		r.Wait()
   285  		return r.token, r.err
   286  	}
   287  
   288  	// only one fetch token job
   289  	r := new(authResult)
   290  	r.Add(1)
   291  	ah.scopedTokens[scoped] = r
   292  	ah.Unlock()
   293  
   294  	// fetch token for the resource scope
   295  	var (
   296  		token string
   297  		err   error
   298  	)
   299  	if to.secret != "" {
   300  		// credential information is provided, use oauth POST endpoint
   301  		token, err = ah.fetchTokenWithOAuth(ctx, to)
   302  		err = errors.Wrap(err, "failed to fetch oauth token")
   303  	} else {
   304  		// do request anonymously
   305  		token, err = ah.fetchToken(ctx, to)
   306  		err = errors.Wrap(err, "failed to fetch anonymous token")
   307  	}
   308  	token = fmt.Sprintf("Bearer %s", token)
   309  
   310  	r.token, r.err = token, err
   311  	r.Done()
   312  	return r.token, r.err
   313  }
   314  
   315  type tokenOptions struct {
   316  	realm    string
   317  	service  string
   318  	scopes   []string
   319  	username string
   320  	secret   string
   321  }
   322  
   323  type postTokenResponse struct {
   324  	AccessToken  string    `json:"access_token"`
   325  	RefreshToken string    `json:"refresh_token"`
   326  	ExpiresIn    int       `json:"expires_in"`
   327  	IssuedAt     time.Time `json:"issued_at"`
   328  	Scope        string    `json:"scope"`
   329  }
   330  
   331  func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
   332  	form := url.Values{}
   333  	if len(to.scopes) > 0 {
   334  		form.Set("scope", strings.Join(to.scopes, " "))
   335  	}
   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  }