Browse code

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>

Derek McGowan authored on 2016/07/14 05:30:24
Showing 4 changed files
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"fmt"
5 5
 	"net"
6 6
 	"net/http"
7
-	"net/url"
8 7
 	"time"
9 8
 
10 9
 	"github.com/docker/distribution"
... ...
@@ -19,21 +18,6 @@ import (
19 19
 	"golang.org/x/net/context"
20 20
 )
21 21
 
22
-type dumbCredentialStore struct {
23
-	auth *types.AuthConfig
24
-}
25
-
26
-func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
27
-	return dcs.auth.Username, dcs.auth.Password
28
-}
29
-
30
-func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string {
31
-	return dcs.auth.IdentityToken
32
-}
33
-
34
-func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
35
-}
36
-
37 22
 // NewV2Repository returns a repository (v2 only). It creates an HTTP transport
38 23
 // providing timeout settings and authentication support, and also verifies the
39 24
 // remote API version.
... ...
@@ -68,7 +52,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
68 68
 	modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), metaHeaders)
69 69
 	authTransport := transport.NewTransport(base, modifiers...)
70 70
 
71
-	challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)
71
+	challengeManager, foundVersion, err := registry.PingV2Registry(endpoint.URL, authTransport)
72 72
 	if err != nil {
73 73
 		transportOK := false
74 74
 		if responseErr, ok := err.(registry.PingResponseError); ok {
... ...
@@ -86,7 +70,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
86 86
 		passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
87 87
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
88 88
 	} else {
89
-		creds := dumbCredentialStore{auth: authConfig}
89
+		creds := registry.NewStaticCredentialStore(authConfig)
90 90
 		tokenHandlerOptions := auth.TokenHandlerOptions{
91 91
 			Transport:   authTransport,
92 92
 			Credentials: creds,
... ...
@@ -91,6 +91,35 @@ func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token strin
91 91
 	lcs.authConfig.IdentityToken = token
92 92
 }
93 93
 
94
+type staticCredentialStore struct {
95
+	auth *types.AuthConfig
96
+}
97
+
98
+// NewStaticCredentialStore returns a credential store
99
+// which always returns the same credential values.
100
+func NewStaticCredentialStore(auth *types.AuthConfig) auth.CredentialStore {
101
+	return staticCredentialStore{
102
+		auth: auth,
103
+	}
104
+}
105
+
106
+func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
107
+	if scs.auth == nil {
108
+		return "", ""
109
+	}
110
+	return scs.auth.Username, scs.auth.Password
111
+}
112
+
113
+func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
114
+	if scs.auth == nil {
115
+		return ""
116
+	}
117
+	return scs.auth.IdentityToken
118
+}
119
+
120
+func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
121
+}
122
+
94 123
 type fallbackError struct {
95 124
 	err error
96 125
 }
... ...
@@ -108,33 +137,14 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
108 108
 	modifiers := DockerHeaders(userAgent, nil)
109 109
 	authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
110 110
 
111
-	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
112
-	if err != nil {
113
-		if !foundV2 {
114
-			err = fallbackError{err: err}
115
-		}
116
-		return "", "", err
117
-	}
118
-
119 111
 	credentialAuthConfig := *authConfig
120 112
 	creds := loginCredentialStore{
121 113
 		authConfig: &credentialAuthConfig,
122 114
 	}
123 115
 
124
-	tokenHandlerOptions := auth.TokenHandlerOptions{
125
-		Transport:     authTransport,
126
-		Credentials:   creds,
127
-		OfflineAccess: true,
128
-		ClientID:      AuthClientID,
129
-	}
130
-	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
131
-	basicHandler := auth.NewBasicHandler(creds)
132
-	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
133
-	tr := transport.NewTransport(authTransport, modifiers...)
134
-
135
-	loginClient := &http.Client{
136
-		Transport: tr,
137
-		Timeout:   15 * time.Second,
116
+	loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil)
117
+	if err != nil {
118
+		return "", "", err
138 119
 	}
139 120
 
140 121
 	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
... ...
@@ -168,6 +178,34 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
168 168
 
169 169
 }
170 170
 
171
+func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, bool, error) {
172
+	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
173
+	if err != nil {
174
+		if !foundV2 {
175
+			err = fallbackError{err: err}
176
+		}
177
+		return nil, foundV2, err
178
+	}
179
+
180
+	tokenHandlerOptions := auth.TokenHandlerOptions{
181
+		Transport:     authTransport,
182
+		Credentials:   creds,
183
+		OfflineAccess: true,
184
+		ClientID:      AuthClientID,
185
+		Scopes:        scopes,
186
+	}
187
+	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
188
+	basicHandler := auth.NewBasicHandler(creds)
189
+	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
190
+	tr := transport.NewTransport(authTransport, modifiers...)
191
+
192
+	return &http.Client{
193
+		Transport: tr,
194
+		Timeout:   15 * time.Second,
195
+	}, foundV2, nil
196
+
197
+}
198
+
171 199
 // ResolveAuthConfig matches an auth configuration to a server address or a URL
172 200
 func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
173 201
 	configKey := GetAuthConfigKey(index)
... ...
@@ -215,7 +253,7 @@ func (err PingResponseError) Error() string {
215 215
 // challenge manager for the supported authentication types and
216 216
 // whether v2 was confirmed by the response. If a response is received but
217 217
 // cannot be interpreted a PingResponseError will be returned.
218
-func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
218
+func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
219 219
 	var (
220 220
 		foundV2   = false
221 221
 		v2Version = auth.APIVersion{
... ...
@@ -228,7 +266,7 @@ func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.Cha
228 228
 		Transport: transport,
229 229
 		Timeout:   15 * time.Second,
230 230
 	}
231
-	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
231
+	endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
232 232
 	req, err := http.NewRequest("GET", endpointStr, nil)
233 233
 	if err != nil {
234 234
 		return nil, false, err
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"golang.org/x/net/context"
11 11
 
12 12
 	"github.com/Sirupsen/logrus"
13
+	"github.com/docker/distribution/registry/client/auth"
13 14
 	"github.com/docker/docker/reference"
14 15
 	"github.com/docker/engine-api/types"
15 16
 	registrytypes "github.com/docker/engine-api/types/registry"
... ...
@@ -132,11 +133,44 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut
132 132
 		return nil, err
133 133
 	}
134 134
 
135
-	r, err := NewSession(endpoint.client, authConfig, endpoint)
136
-	if err != nil {
137
-		return nil, err
135
+	var client *http.Client
136
+	if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
137
+		creds := NewStaticCredentialStore(authConfig)
138
+		scopes := []auth.Scope{
139
+			auth.RegistryScope{
140
+				Name:    "catalog",
141
+				Actions: []string{"search"},
142
+			},
143
+		}
144
+
145
+		modifiers := DockerHeaders(userAgent, nil)
146
+		v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes)
147
+		if err != nil {
148
+			if fErr, ok := err.(fallbackError); ok {
149
+				logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err)
150
+			} else {
151
+				return nil, err
152
+			}
153
+		} else if foundV2 {
154
+			// Copy non transport http client features
155
+			v2Client.Timeout = endpoint.client.Timeout
156
+			v2Client.CheckRedirect = endpoint.client.CheckRedirect
157
+			v2Client.Jar = endpoint.client.Jar
158
+
159
+			logrus.Debugf("using v2 client for search to %s", endpoint.URL)
160
+			client = v2Client
161
+		}
138 162
 	}
139 163
 
164
+	if client == nil {
165
+		client = endpoint.client
166
+		if err := authorizeClient(client, authConfig, endpoint); err != nil {
167
+			return nil, err
168
+		}
169
+	}
170
+
171
+	r := newSession(client, authConfig, endpoint)
172
+
140 173
 	if index.Official {
141 174
 		localName := remoteName
142 175
 		if strings.HasPrefix(localName, "library/") {
... ...
@@ -161,16 +161,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
161 161
 	}
162 162
 }
163 163
 
164
-// NewSession creates a new session
165
-// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
166
-func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) {
167
-	r = &Session{
168
-		authConfig:    authConfig,
169
-		client:        client,
170
-		indexEndpoint: endpoint,
171
-		id:            stringid.GenerateRandomID(),
172
-	}
173
-
164
+func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error {
174 165
 	var alwaysSetBasicAuth bool
175 166
 
176 167
 	// 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
178 178
 	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
179 179
 		info, err := endpoint.Ping()
180 180
 		if err != nil {
181
-			return nil, err
181
+			return err
182 182
 		}
183 183
 		if info.Standalone && authConfig != nil {
184 184
 			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
192 192
 
193 193
 	jar, err := cookiejar.New(nil)
194 194
 	if err != nil {
195
-		return nil, errors.New("cookiejar.New is not supposed to return an error")
195
+		return errors.New("cookiejar.New is not supposed to return an error")
196 196
 	}
197 197
 	client.Jar = jar
198 198
 
199
-	return r, nil
199
+	return nil
200
+}
201
+
202
+func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session {
203
+	return &Session{
204
+		authConfig:    authConfig,
205
+		client:        client,
206
+		indexEndpoint: endpoint,
207
+		id:            stringid.GenerateRandomID(),
208
+	}
209
+}
210
+
211
+// NewSession creates a new session
212
+// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
213
+func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) {
214
+	if err := authorizeClient(client, authConfig, endpoint); err != nil {
215
+		return nil, err
216
+	}
217
+
218
+	return newSession(client, authConfig, endpoint), nil
200 219
 }
201 220
 
202 221
 // ID returns this registry session's ID.