registry/auth.go
4f0d95fa
 package registry // import "github.com/docker/docker/registry"
be20f3c5
 
 import (
 	"io/ioutil"
 	"net/http"
f2d481a2
 	"net/url"
be20f3c5
 	"strings"
f2d481a2
 	"time"
4e725484
 
f2d481a2
 	"github.com/docker/distribution/registry/client/auth"
c85eb008
 	"github.com/docker/distribution/registry/client/auth/challenge"
f2d481a2
 	"github.com/docker/distribution/registry/client/transport"
91e197d6
 	"github.com/docker/docker/api/types"
 	registrytypes "github.com/docker/docker/api/types/registry"
d453fe35
 	"github.com/docker/docker/errdefs"
ebcb7d6b
 	"github.com/pkg/errors"
1009e6a4
 	"github.com/sirupsen/logrus"
be20f3c5
 )
 
e896d1d7
 const (
 	// AuthClientID is used the ClientID used for the token server
 	AuthClientID = "docker"
 )
 
f2d481a2
 // loginV1 tries to register/login to the v1 registry server.
e896d1d7
 func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) {
ebcb7d6b
 	registryEndpoint := apiEndpoint.ToV1Endpoint(userAgent, nil)
f2d481a2
 	serverAddress := registryEndpoint.String()
fcee6056
 
e42f8508
 	logrus.Debugf("attempting v1 login to registry endpoint %s", serverAddress)
41e20cec
 
fcee6056
 	if serverAddress == "" {
87a12421
 		return "", "", errdefs.System(errors.New("server Error: Server Address not set"))
fcee6056
 	}
 
aee260d4
 	req, err := http.NewRequest("GET", serverAddress+"users/", nil)
f2d481a2
 	if err != nil {
e896d1d7
 		return "", "", err
f2d481a2
 	}
aee260d4
 	req.SetBasicAuth(authConfig.Username, authConfig.Password)
 	resp, err := registryEndpoint.client.Do(req)
0a35db8f
 	if err != nil {
f2d481a2
 		// fallback when request could not be completed
e896d1d7
 		return "", "", fallbackError{
f2d481a2
 			err: err,
 		}
0a35db8f
 	}
aee260d4
 	defer resp.Body.Close()
 	body, err := ioutil.ReadAll(resp.Body)
f1cf5074
 	if err != nil {
87a12421
 		return "", "", errdefs.System(err)
be20f3c5
 	}
ebcb7d6b
 
 	switch resp.StatusCode {
 	case http.StatusOK:
e896d1d7
 		return "Login Succeeded", "", nil
ebcb7d6b
 	case http.StatusUnauthorized:
87a12421
 		return "", "", errdefs.Unauthorized(errors.New("Wrong login/password, please try again"))
ebcb7d6b
 	case http.StatusForbidden:
aee260d4
 		// *TODO: Use registry configuration to determine what this says, if anything?
87a12421
 		return "", "", errdefs.Forbidden(errors.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress))
ebcb7d6b
 	case http.StatusInternalServerError:
aee260d4
 		logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
87a12421
 		return "", "", errdefs.System(errors.New("Internal Server Error"))
be20f3c5
 	}
87a12421
 	return "", "", errdefs.System(errors.Errorf("Login: %s (Code: %d; Headers: %s)", body,
 		resp.StatusCode, resp.Header))
be20f3c5
 }
fcee6056
 
f2d481a2
 type loginCredentialStore struct {
 	authConfig *types.AuthConfig
 }
41e20cec
 
f2d481a2
 func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
 	return lcs.authConfig.Username, lcs.authConfig.Password
 }
41e20cec
 
e896d1d7
 func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
 	return lcs.authConfig.IdentityToken
 }
 
 func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
 	lcs.authConfig.IdentityToken = token
 }
 
19d48f0b
 type staticCredentialStore struct {
 	auth *types.AuthConfig
 }
 
 // NewStaticCredentialStore returns a credential store
 // which always returns the same credential values.
 func NewStaticCredentialStore(auth *types.AuthConfig) auth.CredentialStore {
 	return staticCredentialStore{
 		auth: auth,
 	}
 }
 
 func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
 	if scs.auth == nil {
 		return "", ""
 	}
 	return scs.auth.Username, scs.auth.Password
 }
 
 func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
 	if scs.auth == nil {
 		return ""
 	}
 	return scs.auth.IdentityToken
 }
 
 func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
 }
 
f2d481a2
 type fallbackError struct {
 	err error
 }
41e20cec
 
f2d481a2
 func (err fallbackError) Error() string {
 	return err.err.Error()
41e20cec
 }
 
f2d481a2
 // 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.
e896d1d7
 func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) {
e42f8508
 	logrus.Debugf("attempting v2 login to registry endpoint %s", strings.TrimRight(endpoint.URL.String(), "/")+"/v2/")
41e20cec
 
de5c80b4
 	modifiers := Headers(userAgent, nil)
f2d481a2
 	authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
41e20cec
 
e896d1d7
 	credentialAuthConfig := *authConfig
f2d481a2
 	creds := loginCredentialStore{
e896d1d7
 		authConfig: &credentialAuthConfig,
41e20cec
 	}
 
19d48f0b
 	loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil)
 	if err != nil {
 		return "", "", err
41e20cec
 	}
 
f2d481a2
 	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
 	req, err := http.NewRequest("GET", endpointStr, nil)
41e20cec
 	if err != nil {
f2d481a2
 		if !foundV2 {
 			err = fallbackError{err: err}
 		}
e896d1d7
 		return "", "", err
41e20cec
 	}
 
f2d481a2
 	resp, err := loginClient.Do(req)
41e20cec
 	if err != nil {
ebcb7d6b
 		err = translateV2AuthError(err)
f2d481a2
 		if !foundV2 {
 			err = fallbackError{err: err}
 		}
ebcb7d6b
 
e896d1d7
 		return "", "", err
41e20cec
 	}
 	defer resp.Body.Close()
 
ebcb7d6b
 	if resp.StatusCode == http.StatusOK {
 		return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
41e20cec
 	}
 
ebcb7d6b
 	// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
 	err = errors.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
41e20cec
 }
 
19d48f0b
 func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, bool, error) {
 	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
 	if err != nil {
 		if !foundV2 {
 			err = fallbackError{err: err}
 		}
 		return nil, foundV2, err
 	}
 
 	tokenHandlerOptions := auth.TokenHandlerOptions{
 		Transport:     authTransport,
 		Credentials:   creds,
 		OfflineAccess: true,
 		ClientID:      AuthClientID,
 		Scopes:        scopes,
 	}
 	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
 	basicHandler := auth.NewBasicHandler(creds)
 	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
 	tr := transport.NewTransport(authTransport, modifiers...)
 
 	return &http.Client{
 		Transport: tr,
 		Timeout:   15 * time.Second,
 	}, foundV2, nil
 
 }
 
67d752ac
 // ConvertToHostname converts a registry url which has http|https prepended
 // to just an hostname.
 func ConvertToHostname(url string) string {
 	stripped := url
 	if strings.HasPrefix(url, "http://") {
 		stripped = strings.TrimPrefix(url, "http://")
 	} else if strings.HasPrefix(url, "https://") {
 		stripped = strings.TrimPrefix(url, "https://")
 	}
 
 	nameParts := strings.SplitN(stripped, "/", 2)
 
 	return nameParts[0]
 }
 
4fcb9ac4
 // ResolveAuthConfig matches an auth configuration to a server address or a URL
96c10098
 func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
 	configKey := GetAuthConfigKey(index)
90b0cce0
 	// First try the happy case
920ea135
 	if c, found := authConfigs[configKey]; found || index.Official {
90b0cce0
 		return c
fcee6056
 	}
 
90b0cce0
 	// Maybe they have a legacy config file, we will iterate the keys converting
 	// them to the new format and testing
920ea135
 	for registry, ac := range authConfigs {
67d752ac
 		if configKey == ConvertToHostname(registry) {
bb9da6ba
 			return ac
90b0cce0
 		}
fcee6056
 	}
90b0cce0
 
 	// When all else fails, return an empty auth config
5b321e32
 	return types.AuthConfig{}
18c9b6c6
 }
f2d481a2
 
 // PingResponseError is used when the response from a ping
 // was received but invalid.
 type PingResponseError struct {
 	Err error
 }
 
 func (err PingResponseError) Error() string {
271b1f27
 	return err.Err.Error()
f2d481a2
 }
 
 // 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.
c85eb008
 func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, bool, error) {
f2d481a2
 	var (
 		foundV2   = false
 		v2Version = auth.APIVersion{
 			Type:    "registry",
 			Version: "2.0",
 		}
 	)
 
 	pingClient := &http.Client{
 		Transport: transport,
 		Timeout:   15 * time.Second,
 	}
19d48f0b
 	endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
f2d481a2
 	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
 		}
 	}
 
c85eb008
 	challengeManager := challenge.NewSimpleManager()
f2d481a2
 	if err := challengeManager.AddResponse(resp); err != nil {
 		return nil, foundV2, PingResponseError{
 			Err: err,
 		}
 	}
 
 	return challengeManager, foundV2, nil
 }