Skip to content

Commit

Permalink
Merge pull request moby#20832 from aaronlehmann/login-endpoint-refactor
Browse files Browse the repository at this point in the history
Update login to use token handling code from distribution
  • Loading branch information
thaJeztah committed Mar 3, 2016
2 parents ec79629 + f2d481a commit 17156ba
Show file tree
Hide file tree
Showing 16 changed files with 275 additions and 627 deletions.
2 changes: 1 addition & 1 deletion distribution/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
return err
}

endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo)
endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion distribution/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
return err
}

endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo)
endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo.Hostname())
if err != nil {
return err
}
Expand Down
45 changes: 7 additions & 38 deletions distribution/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net"
"net/http"
"net/url"
"strings"
"time"

"github.com/docker/distribution"
Expand Down Expand Up @@ -53,48 +52,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end

modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders)
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return nil, false, fallbackError{err: err}
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, false, fallbackError{err: err}
}
defer resp.Body.Close()

// We got a HTTP request through, so we're using the right TLS settings.
// From this point forward, set transportOK to true in any fallbackError
// we return.

v2Version := auth.APIVersion{
Type: "registry",
Version: "2.0",
}

versions := auth.APIVersions(resp, registry.DefaultRegistryVersionHeader)
for _, pingVersion := range versions {
if pingVersion == v2Version {
// The version header indicates we're definitely
// talking to a v2 registry. So don't allow future
// fallbacks to the v1 protocol.

foundVersion = true
break
challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)
if err != nil {
transportOK := false
if responseErr, ok := err.(registry.PingResponseError); ok {
transportOK = true
err = responseErr.Err
}
}

challengeManager := auth.NewSimpleChallengeManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, foundVersion, fallbackError{
err: err,
confirmedV2: foundVersion,
transportOK: true,
transportOK: transportOK,
}
}

Expand Down
7 changes: 3 additions & 4 deletions integration-cli/docker_cli_v2_only_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,19 @@ func (s *DockerRegistrySuite) TestV1(c *check.C) {
defer cleanup()

s.d.Cmd("build", "--file", dockerfileName, ".")
c.Assert(v1Repo, check.Not(check.Equals), 0, check.Commentf("Expected v1 repository access after build"))
c.Assert(v1Repo, check.Equals, 1, check.Commentf("Expected v1 repository access after build"))

repoName := fmt.Sprintf("%s/busybox", reg.hostport)
s.d.Cmd("run", repoName)
c.Assert(v1Repo, check.Not(check.Equals), 1, check.Commentf("Expected v1 repository access after run"))
c.Assert(v1Repo, check.Equals, 2, check.Commentf("Expected v1 repository access after run"))

s.d.Cmd("login", "-u", "richard", "-p", "testtest", reg.hostport)
c.Assert(v1Logins, check.Not(check.Equals), 0, check.Commentf("Expected v1 login attempt"))
c.Assert(v1Logins, check.Equals, 1, check.Commentf("Expected v1 login attempt"))

s.d.Cmd("tag", "busybox", repoName)
s.d.Cmd("push", repoName)

c.Assert(v1Repo, check.Equals, 2)
c.Assert(v1Pings, check.Equals, 1)

s.d.Cmd("pull", repoName)
c.Assert(v1Repo, check.Equals, 3, check.Commentf("Expected v1 repository access after pull"))
Expand Down
208 changes: 128 additions & 80 deletions registry/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,25 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
)

// Login tries to register/login to the registry server.
func Login(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
// Separates the v2 registry login logic from the v1 logic.
if registryEndpoint.Version == APIVersion2 {
return loginV2(authConfig, registryEndpoint, "" /* scope */)
// loginV1 tries to register/login to the v1 registry server.
func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
if err != nil {
return "", err
}
return loginV1(authConfig, registryEndpoint)
}

// loginV1 tries to register/login to the v1 registry server.
func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
var (
err error
serverAddress = authConfig.ServerAddress
)
serverAddress := registryEndpoint.String()

logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)

Expand All @@ -36,10 +33,16 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
loginAgainstOfficialIndex := serverAddress == IndexServer

req, err := http.NewRequest("GET", serverAddress+"users/", nil)
if err != nil {
return "", err
}
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := registryEndpoint.client.Do(req)
if err != nil {
return "", err
// fallback when request could not be completed
return "", fallbackError{
err: err,
}
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
Expand Down Expand Up @@ -68,97 +71,82 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
}
}

// loginV2 tries to login to the v2 registry server. The given registry endpoint has been
// pinged or setup with a list of authorization challenges. Each of these challenges are
// tried until one of them succeeds. Currently supported challenge schemes are:
// HTTP Basic Authorization
// Token Authorization with a separate token issuing server
// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
// now, users should create their account through other means like directly from a web page
// served by the v2 registry service provider. Whether this will be supported in the future
// is to be determined.
func loginV2(authConfig *types.AuthConfig, registryEndpoint *Endpoint, scope string) (string, error) {
logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
var (
err error
allErrors []error
)

for _, challenge := range registryEndpoint.AuthChallenges {
params := make(map[string]string, len(challenge.Parameters)+1)
for k, v := range challenge.Parameters {
params[k] = v
}
params["scope"] = scope
logrus.Debugf("trying %q auth challenge with params %v", challenge.Scheme, params)

switch strings.ToLower(challenge.Scheme) {
case "basic":
err = tryV2BasicAuthLogin(authConfig, params, registryEndpoint)
case "bearer":
err = tryV2TokenAuthLogin(authConfig, params, registryEndpoint)
default:
// Unsupported challenge types are explicitly skipped.
err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
}

if err == nil {
return "Login Succeeded", nil
}
type loginCredentialStore struct {
authConfig *types.AuthConfig
}

logrus.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
return lcs.authConfig.Username, lcs.authConfig.Password
}

allErrors = append(allErrors, err)
}
type fallbackError struct {
err error
}

return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
func (err fallbackError) Error() string {
return err.err.Error()
}

func tryV2BasicAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
if err != nil {
return err
}
// loginV2 tries to login to the v2 registry server. The given registry
// endpoint will be pinged to get authorization challenges. These challenges
// will be used to authenticate against the registry to validate credentials.
func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)

req.SetBasicAuth(authConfig.Username, authConfig.Password)
modifiers := DockerHeaders(userAgent, nil)
authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)

resp, err := registryEndpoint.client.Do(req)
challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
if err != nil {
return err
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
creds := loginCredentialStore{
authConfig: authConfig,
}

return nil
}
tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
tr := transport.NewTransport(authTransport, modifiers...)

func tryV2TokenAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint)
if err != nil {
return err
loginClient := &http.Client{
Transport: tr,
Timeout: 15 * time.Second,
}

req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return err
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

resp, err := registryEndpoint.client.Do(req)
resp, err := loginClient.Do(req)
if err != nil {
return err
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
if !foundV2 {
err = fallbackError{err: err}
}
return "", err
}

return nil
return "Login Succeeded", nil

}

// ResolveAuthConfig matches an auth configuration to a server address or a URL
Expand Down Expand Up @@ -193,3 +181,63 @@ func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registryt
// When all else fails, return an empty auth config
return types.AuthConfig{}
}

// PingResponseError is used when the response from a ping
// was received but invalid.
type PingResponseError struct {
Err error
}

func (err PingResponseError) Error() string {
return err.Error()
}

// PingV2Registry attempts to ping a v2 registry and on success return a
// challenge manager for the supported authentication types and
// whether v2 was confirmed by the response. If a response is received but
// cannot be interpreted a PingResponseError will be returned.
func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
var (
foundV2 = false
v2Version = auth.APIVersion{
Type: "registry",
Version: "2.0",
}
)

pingClient := &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return nil, false, err
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()

versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
for _, pingVersion := range versions {
if pingVersion == v2Version {
// The version header indicates we're definitely
// talking to a v2 registry. So don't allow future
// fallbacks to the v1 protocol.

foundV2 = true
break
}
}

challengeManager := auth.NewSimpleChallengeManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, foundV2, PingResponseError{
Err: err,
}
}

return challengeManager, foundV2, nil
}
Loading

0 comments on commit 17156ba

Please sign in to comment.