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