Create authentication handler
Refactory authorizer to take a set of authentication handlers for different authentication schemes returned by an unauthorized HTTP requst. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)master
							parent
							
								
									d92e5b1096
								
							
						
					
					
						commit
						49f7f54d07
					
				|  | @ -54,12 +54,12 @@ func init() { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func parseAuthHeader(header http.Header) []authorizationChallenge { | ||||
| 	var challenges []authorizationChallenge | ||||
| func parseAuthHeader(header http.Header) map[string]authorizationChallenge { | ||||
| 	challenges := map[string]authorizationChallenge{} | ||||
| 	for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { | ||||
| 		v, p := parseValueAndParams(h) | ||||
| 		if v != "" { | ||||
| 			challenges = append(challenges, authorizationChallenge{Scheme: v, Parameters: p}) | ||||
| 			challenges[v] = authorizationChallenge{Scheme: v, Parameters: p} | ||||
| 		} | ||||
| 	} | ||||
| 	return challenges | ||||
|  |  | |||
|  | @ -13,25 +13,26 @@ func TestAuthChallengeParse(t *testing.T) { | |||
| 	if len(challenges) != 1 { | ||||
| 		t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges)) | ||||
| 	} | ||||
| 	challenge := challenges["bearer"] | ||||
| 
 | ||||
| 	if expected := "bearer"; challenges[0].Scheme != expected { | ||||
| 		t.Fatalf("Unexpected scheme: %s, expected: %s", challenges[0].Scheme, expected) | ||||
| 	if expected := "bearer"; challenge.Scheme != expected { | ||||
| 		t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected) | ||||
| 	} | ||||
| 
 | ||||
| 	if expected := "https://auth.example.com/token"; challenges[0].Parameters["realm"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["realm"], expected) | ||||
| 	if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected) | ||||
| 	} | ||||
| 
 | ||||
| 	if expected := "registry.example.com"; challenges[0].Parameters["service"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["service"], expected) | ||||
| 	if expected := "registry.example.com"; challenge.Parameters["service"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected) | ||||
| 	} | ||||
| 
 | ||||
| 	if expected := "fun"; challenges[0].Parameters["other"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["other"], expected) | ||||
| 	if expected := "fun"; challenge.Parameters["other"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected) | ||||
| 	} | ||||
| 
 | ||||
| 	if expected := "he\"llo"; challenges[0].Parameters["slashed"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["slashed"], expected) | ||||
| 	if expected := "he\"llo"; challenge.Parameters["slashed"] != expected { | ||||
| 		t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -6,44 +6,9 @@ import ( | |||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/registry/api/v2" | ||||
| ) | ||||
| 
 | ||||
| // RepositoryNotFoundError is returned when making an operation against a
 | ||||
| // repository that does not exist in the registry.
 | ||||
| type RepositoryNotFoundError struct { | ||||
| 	Name string | ||||
| } | ||||
| 
 | ||||
| func (e *RepositoryNotFoundError) Error() string { | ||||
| 	return fmt.Sprintf("No repository found with Name: %s", e.Name) | ||||
| } | ||||
| 
 | ||||
| // ImageManifestNotFoundError is returned when making an operation against a
 | ||||
| // given image manifest that does not exist in the registry.
 | ||||
| type ImageManifestNotFoundError struct { | ||||
| 	Name string | ||||
| 	Tag  string | ||||
| } | ||||
| 
 | ||||
| func (e *ImageManifestNotFoundError) Error() string { | ||||
| 	return fmt.Sprintf("No manifest found with Name: %s, Tag: %s", | ||||
| 		e.Name, e.Tag) | ||||
| } | ||||
| 
 | ||||
| // BlobNotFoundError is returned when making an operation against a given image
 | ||||
| // layer that does not exist in the registry.
 | ||||
| type BlobNotFoundError struct { | ||||
| 	Name   string | ||||
| 	Digest digest.Digest | ||||
| } | ||||
| 
 | ||||
| func (e *BlobNotFoundError) Error() string { | ||||
| 	return fmt.Sprintf("No blob found with Name: %s, Digest: %s", | ||||
| 		e.Name, e.Digest) | ||||
| } | ||||
| 
 | ||||
| // BlobUploadNotFoundError is returned when making a blob upload operation against an
 | ||||
| // invalid blob upload location url.
 | ||||
| // This may be the result of using a cancelled, completed, or stale upload
 | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ func (hl *httpLayer) Read(p []byte) (n int, err error) { | |||
| 	n, err = rd.Read(p) | ||||
| 	hl.offset += int64(n) | ||||
| 
 | ||||
| 	// Simulate io.EOR error if we reach filesize.
 | ||||
| 	// Simulate io.EOF error if we reach filesize.
 | ||||
| 	if err == nil && hl.offset >= hl.size { | ||||
| 		err = io.EOF | ||||
| 	} | ||||
|  |  | |||
|  | @ -17,6 +17,13 @@ type Authorizer interface { | |||
| 	Authorize(req *http.Request) error | ||||
| } | ||||
| 
 | ||||
| // AuthenticationHandler is an interface for authorizing a request from
 | ||||
| // params from a "WWW-Authenicate" header for a single scheme.
 | ||||
| type AuthenticationHandler interface { | ||||
| 	Scheme() string | ||||
| 	AuthorizeRequest(req *http.Request, params map[string]string) error | ||||
| } | ||||
| 
 | ||||
| // CredentialStore is an interface for getting credentials for
 | ||||
| // a given URL
 | ||||
| type CredentialStore interface { | ||||
|  | @ -48,18 +55,6 @@ func (rc *RepositoryConfig) HTTPClient() (*http.Client, error) { | |||
| 	return client, nil | ||||
| } | ||||
| 
 | ||||
| // TokenScope represents the scope at which a token will be requested.
 | ||||
| // This represents a specific action on a registry resource.
 | ||||
| type TokenScope struct { | ||||
| 	Resource string | ||||
| 	Scope    string | ||||
| 	Actions  []string | ||||
| } | ||||
| 
 | ||||
| func (ts TokenScope) String() string { | ||||
| 	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) | ||||
| } | ||||
| 
 | ||||
| // NewTokenAuthorizer returns an authorizer which is capable of getting a token
 | ||||
| // from a token server. The expected authorization method will be discovered
 | ||||
| // by the authorizer, getting the token server endpoint from the URL being
 | ||||
|  | @ -69,24 +64,37 @@ func (ts TokenScope) String() string { | |||
| func NewTokenAuthorizer(creds CredentialStore, header http.Header, scope TokenScope) Authorizer { | ||||
| 	return &tokenAuthorizer{ | ||||
| 		header:     header, | ||||
| 		creds:      creds, | ||||
| 		scope:      scope, | ||||
| 		challenges: map[string][]authorizationChallenge{}, | ||||
| 		challenges: map[string]map[string]authorizationChallenge{}, | ||||
| 		handlers: []AuthenticationHandler{ | ||||
| 			NewTokenHandler(creds, scope, header), | ||||
| 			NewBasicHandler(creds), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewAuthorizer creates an authorizer which can handle multiple authentication
 | ||||
| // schemes. The handlers are tried in order, the higher priority authentication
 | ||||
| // methods should be first.
 | ||||
| func NewAuthorizer(header http.Header, handlers ...AuthenticationHandler) Authorizer { | ||||
| 	return &tokenAuthorizer{ | ||||
| 		header:     header, | ||||
| 		challenges: map[string]map[string]authorizationChallenge{}, | ||||
| 		handlers:   handlers, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type tokenAuthorizer struct { | ||||
| 	header     http.Header | ||||
| 	challenges map[string][]authorizationChallenge | ||||
| 	creds      CredentialStore | ||||
| 	scope      TokenScope | ||||
| 
 | ||||
| 	tokenLock       sync.Mutex | ||||
| 	tokenCache      string | ||||
| 	tokenExpiration time.Time | ||||
| 	challenges map[string]map[string]authorizationChallenge | ||||
| 	handlers   []AuthenticationHandler | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, error) { | ||||
| func (ta *tokenAuthorizer) client() *http.Client { | ||||
| 	// TODO(dmcgowan): Use same transport which has properly configured TLS
 | ||||
| 	return &http.Client{Transport: &Transport{ExtraHeader: ta.header}} | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) ping(endpoint string) (map[string]authorizationChallenge, error) { | ||||
| 	req, err := http.NewRequest("GET", endpoint, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  | @ -98,6 +106,7 @@ func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, erro | |||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	// TODO(dmcgowan): Add version string which would allow skipping this section
 | ||||
| 	var supportsV2 bool | ||||
| HeaderLoop: | ||||
| 	for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { | ||||
|  | @ -148,59 +157,80 @@ func (ta *tokenAuthorizer) Authorize(req *http.Request) error { | |||
| 		ta.challenges[pingEndpoint] = challenges | ||||
| 	} | ||||
| 
 | ||||
| 	return ta.setAuth(challenges, req) | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) client() *http.Client { | ||||
| 	// TODO(dmcgowan): Use same transport which has properly configured TLS
 | ||||
| 	return &http.Client{Transport: &Transport{ExtraHeader: ta.header}} | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) setAuth(challenges []authorizationChallenge, req *http.Request) error { | ||||
| 	var useBasic bool | ||||
| 	for _, challenge := range challenges { | ||||
| 		switch strings.ToLower(challenge.Scheme) { | ||||
| 		case "basic": | ||||
| 			useBasic = true | ||||
| 		case "bearer": | ||||
| 			if err := ta.refreshToken(challenge); err != nil { | ||||
| 	for _, handler := range ta.handlers { | ||||
| 		challenge, ok := challenges[handler.Scheme()] | ||||
| 		if ok { | ||||
| 			if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ta.tokenCache)) | ||||
| 
 | ||||
| 			return nil | ||||
| 		default: | ||||
| 			//log.Infof("Unsupported auth scheme: %q", challenge.Scheme)
 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Only use basic when no token auth challenges found
 | ||||
| 	if useBasic { | ||||
| 		if ta.creds != nil { | ||||
| 			username, password := ta.creds.Basic(req.URL) | ||||
| 			if username != "" && password != "" { | ||||
| 				req.SetBasicAuth(username, password) | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		return errors.New("no basic auth credentials") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) refreshToken(challenge authorizationChallenge) error { | ||||
| 	ta.tokenLock.Lock() | ||||
| 	defer ta.tokenLock.Unlock() | ||||
| type tokenHandler struct { | ||||
| 	header http.Header | ||||
| 	creds  CredentialStore | ||||
| 	scope  TokenScope | ||||
| 
 | ||||
| 	tokenLock       sync.Mutex | ||||
| 	tokenCache      string | ||||
| 	tokenExpiration time.Time | ||||
| } | ||||
| 
 | ||||
| // TokenScope represents the scope at which a token will be requested.
 | ||||
| // This represents a specific action on a registry resource.
 | ||||
| type TokenScope struct { | ||||
| 	Resource string | ||||
| 	Scope    string | ||||
| 	Actions  []string | ||||
| } | ||||
| 
 | ||||
| // NewTokenHandler creates a new AuthenicationHandler which supports
 | ||||
| // fetching tokens from a remote token server.
 | ||||
| func NewTokenHandler(creds CredentialStore, scope TokenScope, header http.Header) AuthenticationHandler { | ||||
| 	return &tokenHandler{ | ||||
| 		header: header, | ||||
| 		creds:  creds, | ||||
| 		scope:  scope, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ts TokenScope) String() string { | ||||
| 	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) | ||||
| } | ||||
| 
 | ||||
| func (ts *tokenHandler) client() *http.Client { | ||||
| 	// TODO(dmcgowan): Use same transport which has properly configured TLS
 | ||||
| 	return &http.Client{Transport: &Transport{ExtraHeader: ts.header}} | ||||
| } | ||||
| 
 | ||||
| func (ts *tokenHandler) Scheme() string { | ||||
| 	return "bearer" | ||||
| } | ||||
| 
 | ||||
| func (ts *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { | ||||
| 	if err := ts.refreshToken(params); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.tokenCache)) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (ts *tokenHandler) refreshToken(params map[string]string) error { | ||||
| 	ts.tokenLock.Lock() | ||||
| 	defer ts.tokenLock.Unlock() | ||||
| 	now := time.Now() | ||||
| 	if now.After(ta.tokenExpiration) { | ||||
| 		token, err := ta.fetchToken(challenge) | ||||
| 	if now.After(ts.tokenExpiration) { | ||||
| 		token, err := ts.fetchToken(params) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		ta.tokenCache = token | ||||
| 		ta.tokenExpiration = now.Add(time.Minute) | ||||
| 		ts.tokenCache = token | ||||
| 		ts.tokenExpiration = now.Add(time.Minute) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
|  | @ -210,26 +240,20 @@ type tokenResponse struct { | |||
| 	Token string `json:"token"` | ||||
| } | ||||
| 
 | ||||
| func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token string, err error) { | ||||
| func (ts *tokenHandler) fetchToken(params map[string]string) (token string, err error) { | ||||
| 	//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
 | ||||
| 	params := map[string]string{} | ||||
| 	for k, v := range challenge.Parameters { | ||||
| 		params[k] = v | ||||
| 	} | ||||
| 	params["scope"] = ta.scope.String() | ||||
| 
 | ||||
| 	realm, ok := params["realm"] | ||||
| 	if !ok { | ||||
| 		return "", errors.New("no realm specified for token auth challenge") | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(dmcgowan): Handle empty scheme
 | ||||
| 
 | ||||
| 	realmURL, err := url.Parse(realm) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("invalid token auth challenge realm: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(dmcgowan): Handle empty scheme
 | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", realmURL.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
|  | @ -237,7 +261,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s | |||
| 
 | ||||
| 	reqParams := req.URL.Query() | ||||
| 	service := params["service"] | ||||
| 	scope := params["scope"] | ||||
| 	scope := ts.scope.String() | ||||
| 
 | ||||
| 	if service != "" { | ||||
| 		reqParams.Add("service", service) | ||||
|  | @ -247,8 +271,8 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s | |||
| 		reqParams.Add("scope", scopeField) | ||||
| 	} | ||||
| 
 | ||||
| 	if ta.creds != nil { | ||||
| 		username, password := ta.creds.Basic(realmURL) | ||||
| 	if ts.creds != nil { | ||||
| 		username, password := ts.creds.Basic(realmURL) | ||||
| 		if username != "" && password != "" { | ||||
| 			reqParams.Add("account", username) | ||||
| 			req.SetBasicAuth(username, password) | ||||
|  | @ -257,7 +281,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s | |||
| 
 | ||||
| 	req.URL.RawQuery = reqParams.Encode() | ||||
| 
 | ||||
| 	resp, err := ta.client().Do(req) | ||||
| 	resp, err := ts.client().Do(req) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | @ -280,3 +304,30 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s | |||
| 
 | ||||
| 	return tr.Token, nil | ||||
| } | ||||
| 
 | ||||
| type basicHandler struct { | ||||
| 	creds CredentialStore | ||||
| } | ||||
| 
 | ||||
| // NewBasicHandler creaters a new authentiation handler which adds
 | ||||
| // basic authentication credentials to a request.
 | ||||
| func NewBasicHandler(creds CredentialStore) AuthenticationHandler { | ||||
| 	return &basicHandler{ | ||||
| 		creds: creds, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (*basicHandler) Scheme() string { | ||||
| 	return "basic" | ||||
| } | ||||
| 
 | ||||
| func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { | ||||
| 	if bh.creds != nil { | ||||
| 		username, password := bh.creds.Basic(req.URL) | ||||
| 		if username != "" && password != "" { | ||||
| 			req.SetBasicAuth(username, password) | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	return errors.New("no basic auth credentials") | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue