diff --git a/registry/client/transport/authchallenge.go b/registry/client/auth/authchallenge.go similarity index 67% rename from registry/client/transport/authchallenge.go rename to registry/client/auth/authchallenge.go index fffd560b0..e3abfb118 100644 --- a/registry/client/transport/authchallenge.go +++ b/registry/client/auth/authchallenge.go @@ -1,6 +1,7 @@ -package transport +package auth import ( + "fmt" "net/http" "strings" ) @@ -8,10 +9,13 @@ import ( // Octet types from RFC 2616. type octetType byte -// authorizationChallenge carries information -// from a WWW-Authenticate response header. -type authorizationChallenge struct { - Scheme string +// Challenge carries information from a WWW-Authenticate response header. +// See RFC 2617. +type Challenge struct { + // Scheme is the auth-scheme according to RFC 2617 + Scheme string + + // Parameters are the auth-params according to RFC 2617 Parameters map[string]string } @@ -54,12 +58,44 @@ func init() { } } -func parseAuthHeader(header http.Header) map[string]authorizationChallenge { - challenges := map[string]authorizationChallenge{} +// Ping pings the provided endpoint to determine its required authorization challenges. +// If a version header is provided, the versions will be returned. +func Ping(client *http.Client, endpoint, versionHeader string) ([]Challenge, []string, error) { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return nil, nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + versions := []string{} + if versionHeader != "" { + for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey(versionHeader)] { + versions = append(versions, strings.Fields(supportedVersions)...) + } + } + + if resp.StatusCode == http.StatusUnauthorized { + // Parse the WWW-Authenticate Header and store the challenges + // on this endpoint object. + return parseAuthHeader(resp.Header), versions, nil + } else if resp.StatusCode != http.StatusOK { + return nil, versions, fmt.Errorf("unable to get valid ping response: %d", resp.StatusCode) + } + + return nil, versions, nil +} + +func parseAuthHeader(header http.Header) []Challenge { + challenges := []Challenge{} for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { v, p := parseValueAndParams(h) if v != "" { - challenges[v] = authorizationChallenge{Scheme: v, Parameters: p} + challenges = append(challenges, Challenge{Scheme: v, Parameters: p}) } } return challenges diff --git a/registry/client/transport/authchallenge_test.go b/registry/client/auth/authchallenge_test.go similarity index 95% rename from registry/client/transport/authchallenge_test.go rename to registry/client/auth/authchallenge_test.go index 45c932b9c..9b6a5adc9 100644 --- a/registry/client/transport/authchallenge_test.go +++ b/registry/client/auth/authchallenge_test.go @@ -1,4 +1,4 @@ -package transport +package auth import ( "net/http" @@ -13,7 +13,7 @@ func TestAuthChallengeParse(t *testing.T) { if len(challenges) != 1 { t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges)) } - challenge := challenges["bearer"] + challenge := challenges[0] if expected := "bearer"; challenge.Scheme != expected { t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected) diff --git a/registry/client/transport/session.go b/registry/client/auth/session.go similarity index 69% rename from registry/client/transport/session.go rename to registry/client/auth/session.go index 90c8082cc..5512a9a16 100644 --- a/registry/client/transport/session.go +++ b/registry/client/auth/session.go @@ -1,4 +1,4 @@ -package transport +package auth import ( "encoding/json" @@ -9,6 +9,8 @@ import ( "strings" "sync" "time" + + "github.com/docker/distribution/registry/client/transport" ) // AuthenticationHandler is an interface for authorizing a request from @@ -32,71 +34,24 @@ type CredentialStore interface { // 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(transport http.RoundTripper, handlers ...AuthenticationHandler) RequestModifier { - return &tokenAuthorizer{ - challenges: map[string]map[string]authorizationChallenge{}, +// methods should be first. The challengeMap holds a list of challenges for +// a given root API endpoint (for example "https://registry-1.docker.io/v2/"). +func NewAuthorizer(challengeMap map[string][]Challenge, handlers ...AuthenticationHandler) transport.RequestModifier { + return &endpointAuthorizer{ + challenges: challengeMap, handlers: handlers, - transport: transport, } } -type tokenAuthorizer struct { - challenges map[string]map[string]authorizationChallenge +type endpointAuthorizer struct { + challenges map[string][]Challenge handlers []AuthenticationHandler transport http.RoundTripper } -func (ta *tokenAuthorizer) ping(endpoint string) (map[string]authorizationChallenge, error) { - req, err := http.NewRequest("GET", endpoint, nil) - if err != nil { - return nil, err - } - - client := &http.Client{ - Transport: ta.transport, - // Ping should fail fast - Timeout: 5 * time.Second, - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - 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")] { - for _, versionName := range strings.Fields(supportedVersions) { - if versionName == "registry/2.0" { - supportsV2 = true - break HeaderLoop - } - } - } - - if !supportsV2 { - return nil, fmt.Errorf("%s does not appear to be a v2 registry endpoint", endpoint) - } - - if resp.StatusCode == http.StatusUnauthorized { - // Parse the WWW-Authenticate Header and store the challenges - // on this endpoint object. - return parseAuthHeader(resp.Header), nil - } else if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to get valid ping response: %d", resp.StatusCode) - } - - return nil, nil -} - -func (ta *tokenAuthorizer) ModifyRequest(req *http.Request) error { +func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { v2Root := strings.Index(req.URL.Path, "/v2/") - // Test if /v2/ does not exist or not at beginning - // TODO(dmcgowan) support v2 endpoints which have a prefix before /v2/ - if v2Root == -1 || v2Root > 0 { + if v2Root == -1 { return nil } @@ -108,19 +63,16 @@ func (ta *tokenAuthorizer) ModifyRequest(req *http.Request) error { pingEndpoint := ping.String() - challenges, ok := ta.challenges[pingEndpoint] + challenges, ok := ea.challenges[pingEndpoint] if !ok { - var err error - challenges, err = ta.ping(pingEndpoint) - if err != nil { - return err - } - ta.challenges[pingEndpoint] = challenges + return nil } - for _, handler := range ta.handlers { - challenge, ok := challenges[handler.Scheme()] - if ok { + for _, handler := range ea.handlers { + for _, challenge := range challenges { + if challenge.Scheme != handler.Scheme() { + continue + } if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { return err } @@ -133,7 +85,7 @@ func (ta *tokenAuthorizer) ModifyRequest(req *http.Request) error { type tokenHandler struct { header http.Header creds CredentialStore - scope TokenScope + scope tokenScope transport http.RoundTripper tokenLock sync.Mutex @@ -141,25 +93,29 @@ type tokenHandler struct { tokenExpiration time.Time } -// TokenScope represents the scope at which a token will be requested. +// tokenScope represents the scope at which a token will be requested. // This represents a specific action on a registry resource. -type TokenScope struct { +type tokenScope struct { Resource string Scope string Actions []string } -func (ts TokenScope) String() string { +func (ts tokenScope) String() string { return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) } // NewTokenHandler creates a new AuthenicationHandler which supports // fetching tokens from a remote token server. -func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope TokenScope) AuthenticationHandler { +func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { return &tokenHandler{ transport: transport, creds: creds, - scope: scope, + scope: tokenScope{ + Resource: "repository", + Scope: scope, + Actions: actions, + }, } } diff --git a/registry/client/transport/session_test.go b/registry/client/auth/session_test.go similarity index 84% rename from registry/client/transport/session_test.go rename to registry/client/auth/session_test.go index 374d6e799..f16836da3 100644 --- a/registry/client/transport/session_test.go +++ b/registry/client/auth/session_test.go @@ -1,4 +1,4 @@ -package transport +package auth import ( "encoding/base64" @@ -8,6 +8,7 @@ import ( "net/url" "testing" + "github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/testutil" ) @@ -67,17 +68,6 @@ func TestEndpointAuthorizeToken(t *testing.T) { repo2 := "other/registry" scope1 := fmt.Sprintf("repository:%s:pull,push", repo1) scope2 := fmt.Sprintf("repository:%s:pull,push", repo2) - tokenScope1 := TokenScope{ - Resource: "repository", - Scope: repo1, - Actions: []string{"pull", "push"}, - } - tokenScope2 := TokenScope{ - Resource: "repository", - Scope: repo2, - Actions: []string{"pull", "push"}, - } - tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ @@ -122,7 +112,14 @@ func TestEndpointAuthorizeToken(t *testing.T) { e, c := testServerWithAuth(m, authenicate, validCheck) defer c() - transport1 := NewTransport(nil, NewAuthorizer(nil, NewTokenHandler(nil, nil, tokenScope1))) + challenges1, _, err := Ping(&http.Client{}, e+"/v2/", "") + if err != nil { + t.Fatal(err) + } + challengeMap1 := map[string][]Challenge{ + e + "/v2/": challenges1, + } + transport1 := transport.NewTransport(nil, NewAuthorizer(challengeMap1, NewTokenHandler(nil, nil, repo1, "pull", "push"))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) @@ -141,7 +138,14 @@ func TestEndpointAuthorizeToken(t *testing.T) { e2, c2 := testServerWithAuth(m, authenicate, badCheck) defer c2() - transport2 := NewTransport(nil, NewAuthorizer(nil, NewTokenHandler(nil, nil, tokenScope2))) + challenges2, _, err := Ping(&http.Client{}, e+"/v2/", "") + if err != nil { + t.Fatal(err) + } + challengeMap2 := map[string][]Challenge{ + e + "/v2/": challenges2, + } + transport2 := transport.NewTransport(nil, NewAuthorizer(challengeMap2, NewTokenHandler(nil, nil, repo2, "pull", "push"))) client2 := &http.Client{Transport: transport2} req, _ = http.NewRequest("GET", e2+"/v2/hello", nil) @@ -166,11 +170,6 @@ func TestEndpointAuthorizeTokenBasic(t *testing.T) { scope := fmt.Sprintf("repository:%s:pull,push", repo) username := "tokenuser" password := "superSecretPa$$word" - tokenScope := TokenScope{ - Resource: "repository", - Scope: repo, - Actions: []string{"pull", "push"}, - } tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { @@ -216,7 +215,14 @@ func TestEndpointAuthorizeTokenBasic(t *testing.T) { password: password, } - transport1 := NewTransport(nil, NewAuthorizer(nil, NewTokenHandler(nil, creds, tokenScope), NewBasicHandler(creds))) + challenges, _, err := Ping(&http.Client{}, e+"/v2/", "") + if err != nil { + t.Fatal(err) + } + challengeMap := map[string][]Challenge{ + e + "/v2/": challenges, + } + transport1 := transport.NewTransport(nil, NewAuthorizer(challengeMap, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) @@ -256,7 +262,14 @@ func TestEndpointAuthorizeBasic(t *testing.T) { password: password, } - transport1 := NewTransport(nil, NewAuthorizer(nil, NewBasicHandler(creds))) + challenges, _, err := Ping(&http.Client{}, e+"/v2/", "") + if err != nil { + t.Fatal(err) + } + challengeMap := map[string][]Challenge{ + e + "/v2/": challenges, + } + transport1 := transport.NewTransport(nil, NewAuthorizer(challengeMap, NewBasicHandler(creds))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil)