Browse code

Merge pull request #24568 from dmcgowan/search-v2-auth

Allow v1 search to use v2 auth with identity token

Sebastiaan van Stijn authored on 2016/07/15 02:11:33
Showing 6 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,
... ...
@@ -87,7 +87,7 @@ clone git github.com/boltdb/bolt v1.2.1
87 87
 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
88 88
 
89 89
 # get graph and distribution packages
90
-clone git github.com/docker/distribution 4e17ab5d319ac5b70b2769442947567a83386fbc
90
+clone git github.com/docker/distribution 07f32ac1831ed0fc71960b7da5d6bb83cb6881b5
91 91
 clone git github.com/vbatts/tar-split v0.9.11
92 92
 
93 93
 # get go-zfs packages
... ...
@@ -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.
... ...
@@ -72,15 +72,19 @@ type endpointAuthorizer struct {
72 72
 }
73 73
 
74 74
 func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
75
-	v2Root := strings.Index(req.URL.Path, "/v2/")
76
-	if v2Root == -1 {
75
+	pingPath := req.URL.Path
76
+	if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 {
77
+		pingPath = pingPath[:v2Root+4]
78
+	} else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 {
79
+		pingPath = pingPath[:v1Root] + "/v2/"
80
+	} else {
77 81
 		return nil
78 82
 	}
79 83
 
80 84
 	ping := url.URL{
81 85
 		Host:   req.URL.Host,
82 86
 		Scheme: req.URL.Scheme,
83
-		Path:   req.URL.Path[:v2Root+4],
87
+		Path:   pingPath,
84 88
 	}
85 89
 
86 90
 	challenges, err := ea.challenges.GetChallenges(ping)
... ...
@@ -151,6 +155,19 @@ func (rs RepositoryScope) String() string {
151 151
 	return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
152 152
 }
153 153
 
154
+// RegistryScope represents a token scope for access
155
+// to resources in the registry.
156
+type RegistryScope struct {
157
+	Name    string
158
+	Actions []string
159
+}
160
+
161
+// String returns the string representation of the user
162
+// using the scope grammar
163
+func (rs RegistryScope) String() string {
164
+	return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ","))
165
+}
166
+
154 167
 // TokenHandlerOptions is used to configure a new token handler
155 168
 type TokenHandlerOptions struct {
156 169
 	Transport   http.RoundTripper