github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/client/auth/session.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/distribution/registry/client"
    15  	"github.com/docker/distribution/registry/client/transport"
    16  )
    17  
    18  // AuthenticationHandler is an interface for authorizing a request from
    19  // params from a "WWW-Authenicate" header for a single scheme.
    20  type AuthenticationHandler interface {
    21  	// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
    22  	Scheme() string
    23  
    24  	// AuthorizeRequest adds the authorization header to a request (if needed)
    25  	// using the parameters from "WWW-Authenticate" method. The parameters
    26  	// values depend on the scheme.
    27  	AuthorizeRequest(req *http.Request, params map[string]string) error
    28  }
    29  
    30  // CredentialStore is an interface for getting credentials for
    31  // a given URL
    32  type CredentialStore interface {
    33  	// Basic returns basic auth for the given URL
    34  	Basic(*url.URL) (string, string)
    35  }
    36  
    37  // NewAuthorizer creates an authorizer which can handle multiple authentication
    38  // schemes. The handlers are tried in order, the higher priority authentication
    39  // methods should be first. The challengeMap holds a list of challenges for
    40  // a given root API endpoint (for example "https://registry-1.docker.io/v2/").
    41  func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier {
    42  	return &endpointAuthorizer{
    43  		challenges: manager,
    44  		handlers:   handlers,
    45  	}
    46  }
    47  
    48  type endpointAuthorizer struct {
    49  	challenges ChallengeManager
    50  	handlers   []AuthenticationHandler
    51  	transport  http.RoundTripper
    52  }
    53  
    54  func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
    55  	v2Root := strings.Index(req.URL.Path, "/v2/")
    56  	if v2Root == -1 {
    57  		return nil
    58  	}
    59  
    60  	ping := url.URL{
    61  		Host:   req.URL.Host,
    62  		Scheme: req.URL.Scheme,
    63  		Path:   req.URL.Path[:v2Root+4],
    64  	}
    65  
    66  	pingEndpoint := ping.String()
    67  
    68  	challenges, err := ea.challenges.GetChallenges(pingEndpoint)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	if len(challenges) > 0 {
    74  		for _, handler := range ea.handlers {
    75  			for _, challenge := range challenges {
    76  				if challenge.Scheme != handler.Scheme() {
    77  					continue
    78  				}
    79  				if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
    80  					return err
    81  				}
    82  			}
    83  		}
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  // This is the minimum duration a token can last (in seconds).
    90  // A token must not live less than 60 seconds because older versions
    91  // of the Docker client didn't read their expiration from the token
    92  // response and assumed 60 seconds.  So to remain compatible with
    93  // those implementations, a token must live at least this long.
    94  const minimumTokenLifetimeSeconds = 60
    95  
    96  // Private interface for time used by this package to enable tests to provide their own implementation.
    97  type clock interface {
    98  	Now() time.Time
    99  }
   100  
   101  type tokenHandler struct {
   102  	header    http.Header
   103  	creds     CredentialStore
   104  	scope     tokenScope
   105  	transport http.RoundTripper
   106  	clock     clock
   107  
   108  	tokenLock       sync.Mutex
   109  	tokenCache      string
   110  	tokenExpiration time.Time
   111  
   112  	additionalScopes map[string]struct{}
   113  }
   114  
   115  // tokenScope represents the scope at which a token will be requested.
   116  // This represents a specific action on a registry resource.
   117  type tokenScope struct {
   118  	Resource string
   119  	Scope    string
   120  	Actions  []string
   121  }
   122  
   123  func (ts tokenScope) String() string {
   124  	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
   125  }
   126  
   127  // An implementation of clock for providing real time data.
   128  type realClock struct{}
   129  
   130  // Now implements clock
   131  func (realClock) Now() time.Time { return time.Now() }
   132  
   133  // NewTokenHandler creates a new AuthenicationHandler which supports
   134  // fetching tokens from a remote token server.
   135  func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
   136  	return newTokenHandler(transport, creds, realClock{}, scope, actions...)
   137  }
   138  
   139  // newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
   140  func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
   141  	return &tokenHandler{
   142  		transport: transport,
   143  		creds:     creds,
   144  		clock:     c,
   145  		scope: tokenScope{
   146  			Resource: "repository",
   147  			Scope:    scope,
   148  			Actions:  actions,
   149  		},
   150  		additionalScopes: map[string]struct{}{},
   151  	}
   152  }
   153  
   154  func (th *tokenHandler) client() *http.Client {
   155  	return &http.Client{
   156  		Transport: th.transport,
   157  		Timeout:   15 * time.Second,
   158  	}
   159  }
   160  
   161  func (th *tokenHandler) Scheme() string {
   162  	return "bearer"
   163  }
   164  
   165  func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
   166  	var additionalScopes []string
   167  	if fromParam := req.URL.Query().Get("from"); fromParam != "" {
   168  		additionalScopes = append(additionalScopes, tokenScope{
   169  			Resource: "repository",
   170  			Scope:    fromParam,
   171  			Actions:  []string{"pull"},
   172  		}.String())
   173  	}
   174  	if err := th.refreshToken(params, additionalScopes...); err != nil {
   175  		return err
   176  	}
   177  
   178  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
   179  
   180  	return nil
   181  }
   182  
   183  func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
   184  	th.tokenLock.Lock()
   185  	defer th.tokenLock.Unlock()
   186  	var addedScopes bool
   187  	for _, scope := range additionalScopes {
   188  		if _, ok := th.additionalScopes[scope]; !ok {
   189  			th.additionalScopes[scope] = struct{}{}
   190  			addedScopes = true
   191  		}
   192  	}
   193  	now := th.clock.Now()
   194  	if now.After(th.tokenExpiration) || addedScopes {
   195  		tr, err := th.fetchToken(params)
   196  		if err != nil {
   197  			return err
   198  		}
   199  		th.tokenCache = tr.Token
   200  		th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  type tokenResponse struct {
   207  	Token       string    `json:"token"`
   208  	AccessToken string    `json:"access_token"`
   209  	ExpiresIn   int       `json:"expires_in"`
   210  	IssuedAt    time.Time `json:"issued_at"`
   211  }
   212  
   213  func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
   214  	//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
   215  	realm, ok := params["realm"]
   216  	if !ok {
   217  		return nil, errors.New("no realm specified for token auth challenge")
   218  	}
   219  
   220  	// TODO(dmcgowan): Handle empty scheme
   221  
   222  	realmURL, err := url.Parse(realm)
   223  	if err != nil {
   224  		return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
   225  	}
   226  
   227  	req, err := http.NewRequest("GET", realmURL.String(), nil)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	reqParams := req.URL.Query()
   233  	service := params["service"]
   234  	scope := th.scope.String()
   235  
   236  	if service != "" {
   237  		reqParams.Add("service", service)
   238  	}
   239  
   240  	for _, scopeField := range strings.Fields(scope) {
   241  		reqParams.Add("scope", scopeField)
   242  	}
   243  
   244  	for scope := range th.additionalScopes {
   245  		reqParams.Add("scope", scope)
   246  	}
   247  
   248  	if th.creds != nil {
   249  		username, password := th.creds.Basic(realmURL)
   250  		if username != "" && password != "" {
   251  			reqParams.Add("account", username)
   252  			req.SetBasicAuth(username, password)
   253  		}
   254  	}
   255  
   256  	req.URL.RawQuery = reqParams.Encode()
   257  
   258  	resp, err := th.client().Do(req)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	defer resp.Body.Close()
   263  
   264  	if !client.SuccessStatus(resp.StatusCode) {
   265  		err := client.HandleErrorResponse(resp)
   266  		return nil, err
   267  	}
   268  
   269  	decoder := json.NewDecoder(resp.Body)
   270  
   271  	tr := new(tokenResponse)
   272  	if err = decoder.Decode(tr); err != nil {
   273  		return nil, fmt.Errorf("unable to decode token response: %s", err)
   274  	}
   275  
   276  	// `access_token` is equivalent to `token` and if both are specified
   277  	// the choice is undefined.  Canonicalize `access_token` by sticking
   278  	// things in `token`.
   279  	if tr.AccessToken != "" {
   280  		tr.Token = tr.AccessToken
   281  	}
   282  
   283  	if tr.Token == "" {
   284  		return nil, errors.New("authorization server did not include a token in the response")
   285  	}
   286  
   287  	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
   288  		logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
   289  		// The default/minimum lifetime.
   290  		tr.ExpiresIn = minimumTokenLifetimeSeconds
   291  	}
   292  
   293  	if tr.IssuedAt.IsZero() {
   294  		// issued_at is optional in the token response.
   295  		tr.IssuedAt = th.clock.Now()
   296  	}
   297  
   298  	return tr, nil
   299  }
   300  
   301  type basicHandler struct {
   302  	creds CredentialStore
   303  }
   304  
   305  // NewBasicHandler creaters a new authentiation handler which adds
   306  // basic authentication credentials to a request.
   307  func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
   308  	return &basicHandler{
   309  		creds: creds,
   310  	}
   311  }
   312  
   313  func (*basicHandler) Scheme() string {
   314  	return "basic"
   315  }
   316  
   317  func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
   318  	if bh.creds != nil {
   319  		username, password := bh.creds.Basic(req.URL)
   320  		if username != "" && password != "" {
   321  			req.SetBasicAuth(username, password)
   322  			return nil
   323  		}
   324  	}
   325  	return errors.New("no basic auth credentials")
   326  }