From 1e0b7538fa2aba4aa252e423362171f1bbfa166c Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 11 Jul 2016 17:20:17 -0700 Subject: [PATCH 1/2] Vendor distribution changes Signed-off-by: Derek McGowan (github: dmcgowan) --- hack/vendor.sh | 2 +- .../registry/client/auth/session.go | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/hack/vendor.sh b/hack/vendor.sh index 39fd7ff307..5e26d021e0 100755 --- a/hack/vendor.sh +++ b/hack/vendor.sh @@ -87,7 +87,7 @@ clone git github.com/boltdb/bolt v1.2.1 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7 # get graph and distribution packages -clone git github.com/docker/distribution 4e17ab5d319ac5b70b2769442947567a83386fbc +clone git github.com/docker/distribution 07f32ac1831ed0fc71960b7da5d6bb83cb6881b5 clone git github.com/vbatts/tar-split v0.9.11 # get go-zfs packages diff --git a/vendor/src/github.com/docker/distribution/registry/client/auth/session.go b/vendor/src/github.com/docker/distribution/registry/client/auth/session.go index f3497b17ad..d03d8ff0ed 100644 --- a/vendor/src/github.com/docker/distribution/registry/client/auth/session.go +++ b/vendor/src/github.com/docker/distribution/registry/client/auth/session.go @@ -72,15 +72,19 @@ type endpointAuthorizer struct { } func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { - v2Root := strings.Index(req.URL.Path, "/v2/") - if v2Root == -1 { + pingPath := req.URL.Path + if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 { + pingPath = pingPath[:v2Root+4] + } else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 { + pingPath = pingPath[:v1Root] + "/v2/" + } else { return nil } ping := url.URL{ Host: req.URL.Host, Scheme: req.URL.Scheme, - Path: req.URL.Path[:v2Root+4], + Path: pingPath, } challenges, err := ea.challenges.GetChallenges(ping) @@ -151,6 +155,19 @@ func (rs RepositoryScope) String() string { return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ",")) } +// RegistryScope represents a token scope for access +// to resources in the registry. +type RegistryScope struct { + Name string + Actions []string +} + +// String returns the string representation of the user +// using the scope grammar +func (rs RegistryScope) String() string { + return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ",")) +} + // TokenHandlerOptions is used to configure a new token handler type TokenHandlerOptions struct { Transport http.RoundTripper From 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 13 Jul 2016 13:30:24 -0700 Subject: [PATCH 2/2] Allow v1 search to use v2 auth with identity token Updates the v1 search endpoint to also support v2 auth when an identity token is given. Only search v1 endpoint is supported since there is not v2 search currently defined to replace it. Signed-off-by: Derek McGowan --- distribution/registry.go | 20 +--------- registry/auth.go | 86 +++++++++++++++++++++++++++++----------- registry/service.go | 40 +++++++++++++++++-- registry/session.go | 36 +++++++++++------ 4 files changed, 124 insertions(+), 58 deletions(-) diff --git a/distribution/registry.go b/distribution/registry.go index b2f3b52617..98b82fbaeb 100644 --- a/distribution/registry.go +++ b/distribution/registry.go @@ -4,7 +4,6 @@ import ( "fmt" "net" "net/http" - "net/url" "time" "github.com/docker/distribution" @@ -19,21 +18,6 @@ import ( "golang.org/x/net/context" ) -type dumbCredentialStore struct { - auth *types.AuthConfig -} - -func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { - return dcs.auth.Username, dcs.auth.Password -} - -func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string { - return dcs.auth.IdentityToken -} - -func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - // NewV2Repository returns a repository (v2 only). It creates an HTTP transport // providing timeout settings and authentication support, and also verifies the // remote API version. @@ -68,7 +52,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), metaHeaders) authTransport := transport.NewTransport(base, modifiers...) - challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport) + challengeManager, foundVersion, err := registry.PingV2Registry(endpoint.URL, authTransport) if err != nil { transportOK := false if responseErr, ok := err.(registry.PingResponseError); ok { @@ -86,7 +70,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) } else { - creds := dumbCredentialStore{auth: authConfig} + creds := registry.NewStaticCredentialStore(authConfig) tokenHandlerOptions := auth.TokenHandlerOptions{ Transport: authTransport, Credentials: creds, diff --git a/registry/auth.go b/registry/auth.go index c5663f58c6..0b5257182e 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -91,6 +91,35 @@ func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token strin lcs.authConfig.IdentityToken = token } +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) { +} + type fallbackError struct { err error } @@ -108,33 +137,14 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin modifiers := DockerHeaders(userAgent, nil) authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...) - challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport) - if err != nil { - if !foundV2 { - err = fallbackError{err: err} - } - return "", "", err - } - credentialAuthConfig := *authConfig creds := loginCredentialStore{ authConfig: &credentialAuthConfig, } - tokenHandlerOptions := auth.TokenHandlerOptions{ - Transport: authTransport, - Credentials: creds, - OfflineAccess: true, - ClientID: AuthClientID, - } - tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) - tr := transport.NewTransport(authTransport, modifiers...) - - loginClient := &http.Client{ - Transport: tr, - Timeout: 15 * time.Second, + loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil) + if err != nil { + return "", "", err } endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" @@ -168,6 +178,34 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin } +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 + +} + // ResolveAuthConfig matches an auth configuration to a server address or a URL func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { configKey := GetAuthConfigKey(index) @@ -215,7 +253,7 @@ func (err PingResponseError) Error() string { // 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) { +func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (auth.ChallengeManager, bool, error) { var ( foundV2 = false v2Version = auth.APIVersion{ @@ -228,7 +266,7 @@ func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.Cha Transport: transport, Timeout: 15 * time.Second, } - endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" + endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" req, err := http.NewRequest("GET", endpointStr, nil) if err != nil { return nil, false, err diff --git a/registry/service.go b/registry/service.go index 25b4990e80..dbc16284f0 100644 --- a/registry/service.go +++ b/registry/service.go @@ -10,6 +10,7 @@ import ( "golang.org/x/net/context" "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/reference" "github.com/docker/engine-api/types" registrytypes "github.com/docker/engine-api/types/registry" @@ -132,11 +133,44 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut return nil, err } - r, err := NewSession(endpoint.client, authConfig, endpoint) - if err != nil { - return nil, err + var client *http.Client + if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { + creds := NewStaticCredentialStore(authConfig) + scopes := []auth.Scope{ + auth.RegistryScope{ + Name: "catalog", + Actions: []string{"search"}, + }, + } + + modifiers := DockerHeaders(userAgent, nil) + v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes) + if err != nil { + if fErr, ok := err.(fallbackError); ok { + logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err) + } else { + return nil, err + } + } else if foundV2 { + // Copy non transport http client features + v2Client.Timeout = endpoint.client.Timeout + v2Client.CheckRedirect = endpoint.client.CheckRedirect + v2Client.Jar = endpoint.client.Jar + + logrus.Debugf("using v2 client for search to %s", endpoint.URL) + client = v2Client + } } + if client == nil { + client = endpoint.client + if err := authorizeClient(client, authConfig, endpoint); err != nil { + return nil, err + } + } + + r := newSession(client, authConfig, endpoint) + if index.Official { localName := remoteName if strings.HasPrefix(localName, "library/") { diff --git a/registry/session.go b/registry/session.go index bb51c7eb6e..d48b9e8d20 100644 --- a/registry/session.go +++ b/registry/session.go @@ -161,16 +161,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) { } } -// NewSession creates a new session -// TODO(tiborvass): remove authConfig param once registry client v2 is vendored -func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) { - r = &Session{ - authConfig: authConfig, - client: client, - indexEndpoint: endpoint, - id: stringid.GenerateRandomID(), - } - +func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { var alwaysSetBasicAuth bool // If we're working with a standalone private registry over HTTPS, send Basic Auth headers @@ -178,7 +169,7 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1E if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { info, err := endpoint.Ping() if err != nil { - return nil, err + return err } if info.Standalone && authConfig != nil { logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String()) @@ -192,11 +183,30 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1E jar, err := cookiejar.New(nil) if err != nil { - return nil, errors.New("cookiejar.New is not supposed to return an error") + return errors.New("cookiejar.New is not supposed to return an error") } client.Jar = jar - return r, nil + return nil +} + +func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session { + return &Session{ + authConfig: authConfig, + client: client, + indexEndpoint: endpoint, + id: stringid.GenerateRandomID(), + } +} + +// NewSession creates a new session +// TODO(tiborvass): remove authConfig param once registry client v2 is vendored +func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) { + if err := authorizeClient(client, authConfig, endpoint); err != nil { + return nil, err + } + + return newSession(client, authConfig, endpoint), nil } // ID returns this registry session's ID.