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 <derek@mcgstyle.net>
This commit is contained in:
Derek McGowan 2016-07-13 13:30:24 -07:00
parent 1e0b7538fa
commit 19d48f0b8b
4 changed files with 124 additions and 58 deletions

View File

@ -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,

View File

@ -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

View File

@ -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/") {

View File

@ -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.