Allow v1 search to use v2 auth with identity token
| ... | ... |
@@ -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 |