From 5730259f324a59a6dddf63a950ab8eb9cde3ca0c Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 9 Mar 2016 13:47:27 -0800 Subject: [PATCH 1/5] Vendor updates to distribution Pull in changes for refresh token in the registry client Signed-off-by: Derek McGowan (github: dmcgowan) --- hack/vendor.sh | 2 +- .../docker/distribution/CONTRIBUTING.md | 2 +- .../github.com/docker/distribution/Dockerfile | 2 +- .../github.com/docker/distribution/Makefile | 4 +- .../github.com/docker/distribution/README.md | 2 +- .../github.com/docker/distribution/blobs.go | 5 + .../github.com/docker/distribution/errors.go | 4 + .../docker/distribution/manifests.go | 16 +- .../distribution/reference/reference.go | 2 +- .../docker/distribution/registry.go | 11 + .../registry/api/v2/descriptors.go | 4 +- .../registry/client/auth/session.go | 356 ++++++++++++------ .../registry/client/repository.go | 24 +- .../registry/client/transport/http_reader.go | 2 +- 14 files changed, 313 insertions(+), 123 deletions(-) diff --git a/hack/vendor.sh b/hack/vendor.sh index 68db3d5517..4d83799e8f 100755 --- a/hack/vendor.sh +++ b/hack/vendor.sh @@ -48,7 +48,7 @@ clone git github.com/boltdb/bolt v1.1.0 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7 # get graph and distribution packages -clone git github.com/docker/distribution 7b66c50bb7e0e4b3b83f8fd134a9f6ea4be08b57 +clone git github.com/docker/distribution db17a23b961978730892e12a0c6051d43a31aab3 clone git github.com/vbatts/tar-split v0.9.11 # get desired notary commit, might also need to be updated in Dockerfile diff --git a/vendor/src/github.com/docker/distribution/CONTRIBUTING.md b/vendor/src/github.com/docker/distribution/CONTRIBUTING.md index 1a9ecb7442..545137f6fa 100644 --- a/vendor/src/github.com/docker/distribution/CONTRIBUTING.md +++ b/vendor/src/github.com/docker/distribution/CONTRIBUTING.md @@ -90,7 +90,7 @@ It's mandatory to: Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry. -Have a look at a great, succesful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443) +Have a look at a great, successful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443) ## Coding Style diff --git a/vendor/src/github.com/docker/distribution/Dockerfile b/vendor/src/github.com/docker/distribution/Dockerfile index 5329cee718..bf4e0d7fd2 100644 --- a/vendor/src/github.com/docker/distribution/Dockerfile +++ b/vendor/src/github.com/docker/distribution/Dockerfile @@ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries VOLUME ["/var/lib/registry"] EXPOSE 5000 ENTRYPOINT ["registry"] -CMD ["/etc/docker/registry/config.yml"] +CMD ["serve", "/etc/docker/registry/config.yml"] diff --git a/vendor/src/github.com/docker/distribution/Makefile b/vendor/src/github.com/docker/distribution/Makefile index 4604a39a02..2ba01083a7 100644 --- a/vendor/src/github.com/docker/distribution/Makefile +++ b/vendor/src/github.com/docker/distribution/Makefile @@ -14,8 +14,8 @@ endif GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)" .PHONY: clean all fmt vet lint build test binaries -.DEFAULT: default -all: AUTHORS clean fmt vet fmt lint build test binaries +.DEFAULT: all +all: fmt vet fmt lint build test binaries AUTHORS: .mailmap .git/HEAD git log --format='%aN <%aE>' | sort -fu > $@ diff --git a/vendor/src/github.com/docker/distribution/README.md b/vendor/src/github.com/docker/distribution/README.md index e8262133a1..cc55884e61 100644 --- a/vendor/src/github.com/docker/distribution/README.md +++ b/vendor/src/github.com/docker/distribution/README.md @@ -128,4 +128,4 @@ avenues are available for support: ## License -This project is distributed under [Apache License, Version 2.0](LICENSE.md). +This project is distributed under [Apache License, Version 2.0](LICENSE). diff --git a/vendor/src/github.com/docker/distribution/blobs.go b/vendor/src/github.com/docker/distribution/blobs.go index ce43ea2ef6..e80800f853 100644 --- a/vendor/src/github.com/docker/distribution/blobs.go +++ b/vendor/src/github.com/docker/distribution/blobs.go @@ -97,6 +97,11 @@ type BlobDeleter interface { Delete(ctx context.Context, dgst digest.Digest) error } +// BlobEnumerator enables iterating over blobs from storage +type BlobEnumerator interface { + Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error +} + // BlobDescriptorService manages metadata about a blob by digest. Most // implementations will not expose such an interface explicitly. Such mappings // should be maintained by interacting with the BlobIngester. Hence, this is diff --git a/vendor/src/github.com/docker/distribution/errors.go b/vendor/src/github.com/docker/distribution/errors.go index 77bd096ec8..c20f28113c 100644 --- a/vendor/src/github.com/docker/distribution/errors.go +++ b/vendor/src/github.com/docker/distribution/errors.go @@ -8,6 +8,10 @@ import ( "github.com/docker/distribution/digest" ) +// ErrAccessDenied is returned when an access to a requested resource is +// denied. +var ErrAccessDenied = errors.New("access denied") + // ErrManifestNotModified is returned when a conditional manifest GetByTag // returns nil due to the client indicating it has the latest version var ErrManifestNotModified = errors.New("manifest not modified") diff --git a/vendor/src/github.com/docker/distribution/manifests.go b/vendor/src/github.com/docker/distribution/manifests.go index 40c5622f08..3bf912a659 100644 --- a/vendor/src/github.com/docker/distribution/manifests.go +++ b/vendor/src/github.com/docker/distribution/manifests.go @@ -53,12 +53,18 @@ type ManifestService interface { // Delete removes the manifest specified by the given digest. Deleting // a manifest that doesn't exist will return ErrManifestNotFound Delete(ctx context.Context, dgst digest.Digest) error +} - // Enumerate fills 'manifests' with the manifests in this service up - // to the size of 'manifests' and returns 'n' for the number of entries - // which were filled. 'last' contains an offset in the manifest set - // and can be used to resume iteration. - //Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error) +// ManifestEnumerator enables iterating over manifests +type ManifestEnumerator interface { + // Enumerate calls ingester for each manifest. + Enumerate(ctx context.Context, ingester func(digest.Digest) error) error +} + +// SignaturesGetter provides an interface for getting the signatures of a schema1 manifest. If the digest +// referred to is not a schema1 manifest, an error should be returned. +type SignaturesGetter interface { + GetSignatures(ctx context.Context, manifestDigest digest.Digest) ([]digest.Digest, error) } // Describable is an interface for descriptors diff --git a/vendor/src/github.com/docker/distribution/reference/reference.go b/vendor/src/github.com/docker/distribution/reference/reference.go index 6f079cbb1a..bb09fa25da 100644 --- a/vendor/src/github.com/docker/distribution/reference/reference.go +++ b/vendor/src/github.com/docker/distribution/reference/reference.go @@ -3,7 +3,7 @@ // // Grammar // -// reference := repository [ ":" tag ] [ "@" digest ] +// reference := name [ ":" tag ] [ "@" digest ] // name := [hostname '/'] component ['/' component]* // hostname := hostcomponent ['.' hostcomponent]* [':' port-number] // hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ diff --git a/vendor/src/github.com/docker/distribution/registry.go b/vendor/src/github.com/docker/distribution/registry.go index 37dbb49be9..fbe605e0a5 100644 --- a/vendor/src/github.com/docker/distribution/registry.go +++ b/vendor/src/github.com/docker/distribution/registry.go @@ -40,6 +40,17 @@ type Namespace interface { // which were filled. 'last' contains an offset in the catalog, and 'err' will be // set to io.EOF if there are no more entries to obtain. Repositories(ctx context.Context, repos []string, last string) (n int, err error) + + // Blobs returns a blob enumerator to access all blobs + Blobs() BlobEnumerator + + // BlobStatter returns a BlobStatter to control + BlobStatter() BlobStatter +} + +// RepositoryEnumerator describes an operation to enumerate repositories +type RepositoryEnumerator interface { + Enumerate(ctx context.Context, ingester func(string) error) error } // ManifestServiceOption is a function argument for Manifest Service methods diff --git a/vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go b/vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go index 7549ccc322..582799948a 100644 --- a/vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go +++ b/vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go @@ -514,7 +514,7 @@ var routeDescriptors = []RouteDescriptor{ digestHeader, }, Body: BodyDescriptor{ - ContentType: "application/json; charset=utf-8", + ContentType: "", Format: manifestBody, }, }, @@ -553,7 +553,7 @@ var routeDescriptors = []RouteDescriptor{ referenceParameterDescriptor, }, Body: BodyDescriptor{ - ContentType: "application/json; charset=utf-8", + ContentType: "", Format: manifestBody, }, Successes: []ResponseDescriptor{ 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 f4c7ade41f..058a87b9c9 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 @@ -19,6 +19,8 @@ import ( // basic auth due to lack of credentials. var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials") +const defaultClientID = "registry-client" + // AuthenticationHandler is an interface for authorizing a request from // params from a "WWW-Authenicate" header for a single scheme. type AuthenticationHandler interface { @@ -36,6 +38,14 @@ type AuthenticationHandler interface { type CredentialStore interface { // Basic returns basic auth for the given URL Basic(*url.URL) (string, string) + + // RefreshToken returns a refresh token for the + // given URL and service + RefreshToken(*url.URL, string) string + + // SetRefreshToken sets the refresh token if none + // is provided for the given url and service + SetRefreshToken(realm *url.URL, service, token string) } // NewAuthorizer creates an authorizer which can handle multiple authentication @@ -105,27 +115,47 @@ type clock interface { type tokenHandler struct { header http.Header creds CredentialStore - scope tokenScope transport http.RoundTripper clock clock + offlineAccess bool + forceOAuth bool + clientID string + scopes []Scope + tokenLock sync.Mutex tokenCache string tokenExpiration time.Time - - additionalScopes map[string]struct{} } -// tokenScope represents the scope at which a token will be requested. -// This represents a specific action on a registry resource. -type tokenScope struct { - Resource string - Scope string - Actions []string +// Scope is a type which is serializable to a string +// using the allow scope grammar. +type Scope interface { + String() string } -func (ts tokenScope) String() string { - return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) +// RepositoryScope represents a token scope for access +// to a repository. +type RepositoryScope struct { + Repository string + Actions []string +} + +// String returns the string representation of the repository +// using the scope grammar +func (rs RepositoryScope) String() string { + return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ",")) +} + +// TokenHandlerOptions is used to configure a new token handler +type TokenHandlerOptions struct { + Transport http.RoundTripper + Credentials CredentialStore + + OfflineAccess bool + ForceOAuth bool + ClientID string + Scopes []Scope } // An implementation of clock for providing real time data. @@ -137,22 +167,33 @@ func (realClock) Now() time.Time { return time.Now() } // NewTokenHandler creates a new AuthenicationHandler which supports // fetching tokens from a remote token server. func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { - return newTokenHandler(transport, creds, realClock{}, scope, actions...) + // Create options... + return NewTokenHandlerWithOptions(TokenHandlerOptions{ + Transport: transport, + Credentials: creds, + Scopes: []Scope{ + RepositoryScope{ + Repository: scope, + Actions: actions, + }, + }, + }) } -// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing. -func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler { - return &tokenHandler{ - transport: transport, - creds: creds, - clock: c, - scope: tokenScope{ - Resource: "repository", - Scope: scope, - Actions: actions, - }, - additionalScopes: map[string]struct{}{}, +// NewTokenHandlerWithOptions creates a new token handler using the provided +// options structure. +func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler { + handler := &tokenHandler{ + transport: options.Transport, + creds: options.Credentials, + offlineAccess: options.OfflineAccess, + forceOAuth: options.ForceOAuth, + clientID: options.ClientID, + scopes: options.Scopes, + clock: realClock{}, } + + return handler } func (th *tokenHandler) client() *http.Client { @@ -169,123 +210,110 @@ func (th *tokenHandler) Scheme() string { func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { var additionalScopes []string if fromParam := req.URL.Query().Get("from"); fromParam != "" { - additionalScopes = append(additionalScopes, tokenScope{ - Resource: "repository", - Scope: fromParam, - Actions: []string{"pull"}, + additionalScopes = append(additionalScopes, RepositoryScope{ + Repository: fromParam, + Actions: []string{"pull"}, }.String()) } - if err := th.refreshToken(params, additionalScopes...); err != nil { + + token, err := th.getToken(params, additionalScopes...) + if err != nil { return err } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) return nil } -func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error { +func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) { th.tokenLock.Lock() defer th.tokenLock.Unlock() + scopes := make([]string, 0, len(th.scopes)+len(additionalScopes)) + for _, scope := range th.scopes { + scopes = append(scopes, scope.String()) + } var addedScopes bool for _, scope := range additionalScopes { - if _, ok := th.additionalScopes[scope]; !ok { - th.additionalScopes[scope] = struct{}{} - addedScopes = true - } + scopes = append(scopes, scope) + addedScopes = true } + now := th.clock.Now() if now.After(th.tokenExpiration) || addedScopes { - tr, err := th.fetchToken(params) + token, expiration, err := th.fetchToken(params, scopes) if err != nil { - return err + return "", err } - th.tokenCache = tr.Token - th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second) + + // do not update cache for added scope tokens + if !addedScopes { + th.tokenCache = token + th.tokenExpiration = expiration + } + + return token, nil } - return nil + return th.tokenCache, nil } -type tokenResponse struct { - Token string `json:"token"` - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - IssuedAt time.Time `json:"issued_at"` +type postTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + IssuedAt time.Time `json:"issued_at"` + Scope string `json:"scope"` } -func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) { - //log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username) - realm, ok := params["realm"] - if !ok { - return nil, errors.New("no realm specified for token auth challenge") +func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) { + form := url.Values{} + form.Set("scope", strings.Join(scopes, " ")) + form.Set("service", service) + + clientID := th.clientID + if clientID == "" { + // Use default client, this is a required field + clientID = defaultClientID + } + form.Set("client_id", clientID) + + if refreshToken != "" { + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", refreshToken) + } else if th.creds != nil { + form.Set("grant_type", "password") + username, password := th.creds.Basic(realm) + form.Set("username", username) + form.Set("password", password) + + // attempt to get a refresh token + form.Set("access_type", "offline") + } else { + // refuse to do oauth without a grant type + return "", time.Time{}, fmt.Errorf("no supported grant type") } - // TODO(dmcgowan): Handle empty scheme - - realmURL, err := url.Parse(realm) + resp, err := th.client().PostForm(realm.String(), form) if err != nil { - return nil, fmt.Errorf("invalid token auth challenge realm: %s", err) - } - - req, err := http.NewRequest("GET", realmURL.String(), nil) - if err != nil { - return nil, err - } - - reqParams := req.URL.Query() - service := params["service"] - scope := th.scope.String() - - if service != "" { - reqParams.Add("service", service) - } - - for _, scopeField := range strings.Fields(scope) { - reqParams.Add("scope", scopeField) - } - - for scope := range th.additionalScopes { - reqParams.Add("scope", scope) - } - - if th.creds != nil { - username, password := th.creds.Basic(realmURL) - if username != "" && password != "" { - reqParams.Add("account", username) - req.SetBasicAuth(username, password) - } - } - - req.URL.RawQuery = reqParams.Encode() - - resp, err := th.client().Do(req) - if err != nil { - return nil, err + return "", time.Time{}, err } defer resp.Body.Close() if !client.SuccessStatus(resp.StatusCode) { err := client.HandleErrorResponse(resp) - return nil, err + return "", time.Time{}, err } decoder := json.NewDecoder(resp.Body) - tr := new(tokenResponse) - if err = decoder.Decode(tr); err != nil { - return nil, fmt.Errorf("unable to decode token response: %s", err) + var tr postTokenResponse + if err = decoder.Decode(&tr); err != nil { + return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) } - // `access_token` is equivalent to `token` and if both are specified - // the choice is undefined. Canonicalize `access_token` by sticking - // things in `token`. - if tr.AccessToken != "" { - tr.Token = tr.AccessToken - } - - if tr.Token == "" { - return nil, errors.New("authorization server did not include a token in the response") + if tr.RefreshToken != "" && tr.RefreshToken != refreshToken { + th.creds.SetRefreshToken(realm, service, tr.RefreshToken) } if tr.ExpiresIn < minimumTokenLifetimeSeconds { @@ -296,10 +324,128 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon if tr.IssuedAt.IsZero() { // issued_at is optional in the token response. - tr.IssuedAt = th.clock.Now() + tr.IssuedAt = th.clock.Now().UTC() } - return tr, nil + return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil +} + +type getTokenResponse struct { + Token string `json:"token"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + IssuedAt time.Time `json:"issued_at"` + RefreshToken string `json:"refresh_token"` +} + +func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) { + + req, err := http.NewRequest("GET", realm.String(), nil) + if err != nil { + return "", time.Time{}, err + } + + reqParams := req.URL.Query() + + if service != "" { + reqParams.Add("service", service) + } + + for _, scope := range scopes { + reqParams.Add("scope", scope) + } + + if th.offlineAccess { + reqParams.Add("offline_token", "true") + clientID := th.clientID + if clientID == "" { + clientID = defaultClientID + } + reqParams.Add("client_id", clientID) + } + + if th.creds != nil { + username, password := th.creds.Basic(realm) + if username != "" && password != "" { + reqParams.Add("account", username) + req.SetBasicAuth(username, password) + } + } + + req.URL.RawQuery = reqParams.Encode() + + resp, err := th.client().Do(req) + if err != nil { + return "", time.Time{}, err + } + defer resp.Body.Close() + + if !client.SuccessStatus(resp.StatusCode) { + err := client.HandleErrorResponse(resp) + return "", time.Time{}, err + } + + decoder := json.NewDecoder(resp.Body) + + var tr getTokenResponse + if err = decoder.Decode(&tr); err != nil { + return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) + } + + if tr.RefreshToken != "" && th.creds != nil { + th.creds.SetRefreshToken(realm, service, tr.RefreshToken) + } + + // `access_token` is equivalent to `token` and if both are specified + // the choice is undefined. Canonicalize `access_token` by sticking + // things in `token`. + if tr.AccessToken != "" { + tr.Token = tr.AccessToken + } + + if tr.Token == "" { + return "", time.Time{}, errors.New("authorization server did not include a token in the response") + } + + if tr.ExpiresIn < minimumTokenLifetimeSeconds { + // The default/minimum lifetime. + tr.ExpiresIn = minimumTokenLifetimeSeconds + logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) + } + + if tr.IssuedAt.IsZero() { + // issued_at is optional in the token response. + tr.IssuedAt = th.clock.Now().UTC() + } + + return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil +} + +func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) { + realm, ok := params["realm"] + if !ok { + return "", time.Time{}, errors.New("no realm specified for token auth challenge") + } + + // TODO(dmcgowan): Handle empty scheme and relative realm + realmURL, err := url.Parse(realm) + if err != nil { + return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err) + } + + service := params["service"] + + var refreshToken string + + if th.creds != nil { + refreshToken = th.creds.RefreshToken(realmURL, service) + } + + if refreshToken != "" || th.forceOAuth { + return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) + } + + return th.fetchTokenWithBasicAuth(realmURL, service, scopes) } type basicHandler struct { diff --git a/vendor/src/github.com/docker/distribution/registry/client/repository.go b/vendor/src/github.com/docker/distribution/registry/client/repository.go index ebf44d4733..830749f1b3 100644 --- a/vendor/src/github.com/docker/distribution/registry/client/repository.go +++ b/vendor/src/github.com/docker/distribution/registry/client/repository.go @@ -292,9 +292,18 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er if err != nil { return distribution.Descriptor{}, err } - var attempts int - resp, err := t.client.Head(u) + req, err := http.NewRequest("HEAD", u, nil) + if err != nil { + return distribution.Descriptor{}, err + } + + for _, t := range distribution.ManifestMediaTypes() { + req.Header.Add("Accept", t) + } + + var attempts int + resp, err := t.client.Do(req) check: if err != nil { return distribution.Descriptor{}, err @@ -304,7 +313,16 @@ check: case resp.StatusCode >= 200 && resp.StatusCode < 400: return descriptorFromResponse(resp) case resp.StatusCode == http.StatusMethodNotAllowed: - resp, err = t.client.Get(u) + req, err = http.NewRequest("GET", u, nil) + if err != nil { + return distribution.Descriptor{}, err + } + + for _, t := range distribution.ManifestMediaTypes() { + req.Header.Add("Accept", t) + } + + resp, err = t.client.Do(req) attempts++ if attempts > 1 { return distribution.Descriptor{}, err diff --git a/vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go b/vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go index 22b0b9d69a..e1b17a03a0 100644 --- a/vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go +++ b/vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go @@ -66,7 +66,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) { return 0, hrs.err } - // If we seeked to a different position, we need to reset the + // If we sought to a different position, we need to reset the // connection. This logic is here instead of Seek so that if // a seek is undone before the next read, the connection doesn't // need to be closed and reopened. A common example of this is From e896d1d7c4459c4b357efdd780e9fb9dd9bc90e0 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 23 Feb 2016 15:18:04 -0800 Subject: [PATCH 2/5] Add support for identity token with token handler Use token handler options for initialization. Update auth endpoint to set identity token in response. Update credential store to match distribution interface changes. Signed-off-by: Derek McGowan (github: dmcgowan) --- api/client/trust.go | 7 +++ api/server/router/system/backend.go | 2 +- api/server/router/system/system_routes.go | 5 +- daemon/daemon.go | 2 +- distribution/registry.go | 20 ++++++- registry/auth.go | 65 +++++++++++++++-------- registry/service.go | 22 +++++--- registry/service_v2.go | 2 +- 8 files changed, 90 insertions(+), 35 deletions(-) diff --git a/api/client/trust.go b/api/client/trust.go index 88cfb3244b..e61c8225b6 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -107,6 +107,13 @@ func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { return scs.auth.Username, scs.auth.Password } +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + // getNotaryRepository returns a NotaryRepository which stores all the // information needed to operate on a notary repository. // It creates a HTTP transport providing authentication support. diff --git a/api/server/router/system/backend.go b/api/server/router/system/backend.go index 8a270027ff..c842ce68cc 100644 --- a/api/server/router/system/backend.go +++ b/api/server/router/system/backend.go @@ -13,5 +13,5 @@ type Backend interface { SystemVersion() types.Version SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{}) UnsubscribeFromEvents(chan interface{}) - AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) + AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) } diff --git a/api/server/router/system/system_routes.go b/api/server/router/system/system_routes.go index 41b531129d..1819747f7d 100644 --- a/api/server/router/system/system_routes.go +++ b/api/server/router/system/system_routes.go @@ -115,11 +115,12 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h if err != nil { return err } - status, err := s.backend.AuthenticateToRegistry(config) + status, token, err := s.backend.AuthenticateToRegistry(config) if err != nil { return err } return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{ - Status: status, + Status: status, + IdentityToken: token, }) } diff --git a/daemon/daemon.go b/daemon/daemon.go index 892e83dac0..e1e487d8c1 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1518,7 +1518,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, } // AuthenticateToRegistry checks the validity of credentials in authConfig -func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) { +func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) { return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent()) } diff --git a/distribution/registry.go b/distribution/registry.go index 4946c0b573..a4de299530 100644 --- a/distribution/registry.go +++ b/distribution/registry.go @@ -26,6 +26,13 @@ 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 a HTTP transport // providing timeout settings and authentication support, and also verifies the // remote API version. @@ -72,7 +79,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) } else { creds := dumbCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + Scopes: []auth.Scope{ + auth.RepositoryScope{ + Repository: repoName, + Actions: actions, + }, + }, + ClientID: registry.AuthClientID, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) } diff --git a/registry/auth.go b/registry/auth.go index a8fdb675c1..8351cd91c3 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -15,11 +15,16 @@ import ( registrytypes "github.com/docker/engine-api/types/registry" ) +const ( + // AuthClientID is used the ClientID used for the token server + AuthClientID = "docker" +) + // loginV1 tries to register/login to the v1 registry server. -func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) { +func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) { registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil) if err != nil { - return "", err + return "", "", err } serverAddress := registryEndpoint.String() @@ -27,48 +32,47 @@ func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent st logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint) if serverAddress == "" { - return "", fmt.Errorf("Server Error: Server Address not set.") + return "", "", fmt.Errorf("Server Error: Server Address not set.") } loginAgainstOfficialIndex := serverAddress == IndexServer req, err := http.NewRequest("GET", serverAddress+"users/", nil) if err != nil { - return "", err + return "", "", err } req.SetBasicAuth(authConfig.Username, authConfig.Password) resp, err := registryEndpoint.client.Do(req) if err != nil { // fallback when request could not be completed - return "", fallbackError{ + return "", "", fallbackError{ err: err, } } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return "", err + return "", "", err } if resp.StatusCode == http.StatusOK { - return "Login Succeeded", nil + return "Login Succeeded", "", nil } else if resp.StatusCode == http.StatusUnauthorized { if loginAgainstOfficialIndex { - return "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com") + return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com") } - return "", fmt.Errorf("Wrong login/password, please try again") + return "", "", fmt.Errorf("Wrong login/password, please try again") } else if resp.StatusCode == http.StatusForbidden { if loginAgainstOfficialIndex { - return "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.") + return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.") } // *TODO: Use registry configuration to determine what this says, if anything? - return "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) + return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) } else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326 logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body) - return "", fmt.Errorf("Internal Server Error") - } else { - return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, - resp.StatusCode, resp.Header) + return "", "", fmt.Errorf("Internal Server Error") } + return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, + resp.StatusCode, resp.Header) } type loginCredentialStore struct { @@ -79,6 +83,14 @@ func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { return lcs.authConfig.Username, lcs.authConfig.Password } +func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { + return lcs.authConfig.IdentityToken +} + +func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { + lcs.authConfig.IdentityToken = token +} + type fallbackError struct { err error } @@ -90,7 +102,7 @@ func (err fallbackError) Error() string { // loginV2 tries to login to the v2 registry server. The given registry // endpoint will be pinged to get authorization challenges. These challenges // will be used to authenticate against the registry to validate credentials. -func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) { +func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) { logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint) modifiers := DockerHeaders(userAgent, nil) @@ -101,14 +113,21 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin if !foundV2 { err = fallbackError{err: err} } - return "", err + return "", "", err } + credentialAuthConfig := *authConfig creds := loginCredentialStore{ - authConfig: authConfig, + authConfig: &credentialAuthConfig, } - tokenHandler := auth.NewTokenHandler(authTransport, creds, "") + 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...) @@ -124,7 +143,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin if !foundV2 { err = fallbackError{err: err} } - return "", err + return "", "", err } resp, err := loginClient.Do(req) @@ -132,7 +151,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin if !foundV2 { err = fallbackError{err: err} } - return "", err + return "", "", err } defer resp.Body.Close() @@ -142,10 +161,10 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin if !foundV2 { err = fallbackError{err: err} } - return "", err + return "", "", err } - return "Login Succeeded", nil + return "Login Succeeded", credentialAuthConfig.IdentityToken, nil } diff --git a/registry/service.go b/registry/service.go index 2124da6d9f..830c2bf69a 100644 --- a/registry/service.go +++ b/registry/service.go @@ -2,6 +2,7 @@ package registry import ( "crypto/tls" + "fmt" "net/http" "net/url" "strings" @@ -29,10 +30,19 @@ func NewService(options *Options) *Service { // Auth contacts the public registry with the provided credentials, // and returns OK if authentication was successful. // It can be used to verify the validity of a client's credentials. -func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) { - endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress) +func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { + serverAddress := authConfig.ServerAddress + if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { + serverAddress = "https://" + serverAddress + } + u, err := url.Parse(serverAddress) if err != nil { - return "", err + return "", "", fmt.Errorf("unable to parse server address: %v", err) + } + + endpoints, err := s.LookupPushEndpoints(u.Host) + if err != nil { + return "", "", err } for _, endpoint := range endpoints { @@ -41,7 +51,7 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s login = loginV1 } - status, err = login(authConfig, endpoint, userAgent) + status, token, err = login(authConfig, endpoint, userAgent) if err == nil { return } @@ -50,10 +60,10 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err) continue } - return "", err + return "", "", err } - return "", err + return "", "", err } // splitReposSearchTerm breaks a search term into an index name and remote name diff --git a/registry/service_v2.go b/registry/service_v2.go index 9c909f186e..0c8f04c5a9 100644 --- a/registry/service_v2.go +++ b/registry/service_v2.go @@ -10,7 +10,7 @@ import ( func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { var cfg = tlsconfig.ServerDefault tlsConfig := &cfg - if hostname == DefaultNamespace { + if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host { // v2 mirrors for _, mirror := range s.Config.Mirrors { if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { From a6d0c66b4c923cddeaea09f5b41ad353ea7a9b5f Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 23 Feb 2016 16:29:16 -0800 Subject: [PATCH 3/5] Change Docker client to support OAuth-based flow for login This mechanism exchanges basic auth credentials for an identity token. The identity token is used going forward to request scoped-down tokens to use for registry operations. Signed-off-by: Aaron Lehmann --- api/client/login.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/client/login.go b/api/client/login.go index 6696feb8cf..3fc14743de 100644 --- a/api/client/login.go +++ b/api/client/login.go @@ -57,12 +57,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error { return err } + if response.IdentityToken != "" { + authConfig.Password = "" + authConfig.IdentityToken = response.IdentityToken + } if err := storeCredentials(cli.configFile, authConfig); err != nil { return fmt.Errorf("Error saving credentials: %v", err) } if response.Status != "" { - fmt.Fprintf(cli.out, "%s\n", response.Status) + fmt.Fprintln(cli.out, response.Status) } return nil } @@ -120,6 +124,7 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, is authconfig.Username = flUser authconfig.Password = flPassword authconfig.ServerAddress = serverAddress + authconfig.IdentityToken = "" return authconfig, nil } From ba0aa5311aa27fe77166f03d8bcc0174e2985913 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 4 Mar 2016 12:00:18 -0800 Subject: [PATCH 4/5] Add support for identity tokens in client credentials store Update unit test and documentation to handle the new case where Username is set to to indicate an identity token is involved. Change the "Password" field in communications with the credential helper to "Secret" to make clear it has a more generic purpose. Signed-off-by: Aaron Lehmann --- api/client/info.go | 2 +- cliconfig/credentials/native_store.go | 30 +++++++--- cliconfig/credentials/native_store_test.go | 57 +++++++++++++++++-- docs/reference/commandline/login.md | 11 ++-- .../auth/docker-credential-shell-test | 4 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/api/client/info.go b/api/client/info.go index 2d02af3a58..2959424378 100644 --- a/api/client/info.go +++ b/api/client/info.go @@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error { u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username if len(u) > 0 { fmt.Fprintf(cli.out, "Username: %v\n", u) - fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) } + fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) } // Only output these warnings if the server does not support these features diff --git a/cliconfig/credentials/native_store.go b/cliconfig/credentials/native_store.go index 2da041d4b2..9b8997dd64 100644 --- a/cliconfig/credentials/native_store.go +++ b/cliconfig/credentials/native_store.go @@ -13,7 +13,10 @@ import ( "github.com/docker/engine-api/types" ) -const remoteCredentialsPrefix = "docker-credential-" +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) // Standarize the not found error, so every helper returns // the same message and docker can handle it properly. @@ -29,14 +32,14 @@ type command interface { type credentialsRequest struct { ServerURL string Username string - Password string + Secret string } // credentialsGetResponse is the information serialized from a remote store // when the plugin sends requests to get the user credentials. type credentialsGetResponse struct { Username string - Password string + Secret string } // nativeStore implements a credentials store @@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) { return auth, err } auth.Username = creds.Username + auth.IdentityToken = creds.IdentityToken auth.Password = creds.Password return auth, nil @@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { creds, _ := c.getCredentialsFromStore(s) ac.Username = creds.Username ac.Password = creds.Password + ac.IdentityToken = creds.IdentityToken auths[s] = ac } @@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error { } authConfig.Username = "" authConfig.Password = "" + authConfig.IdentityToken = "" // Fallback to old credential in plain text to save only the email return c.fileStore.Store(authConfig) @@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error { creds := &credentialsRequest{ ServerURL: config.ServerAddress, Username: config.Username, - Password: config.Password, + Secret: config.Password, + } + + if config.IdentityToken != "" { + creds.Username = tokenUsername + creds.Secret = config.IdentityToken } buffer := new(bytes.Buffer) @@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC return ret, err } - ret.Username = resp.Username - ret.Password = resp.Password + if resp.Username == tokenUsername { + ret.IdentityToken = resp.Secret + } else { + ret.Password = resp.Secret + ret.Username = resp.Username + } + ret.ServerAddress = serverAddress return ret, nil } -// eraseCredentialsFromStore executes the command to remove the server redentails from the native store. +// eraseCredentialsFromStore executes the command to remove the server credentails from the native store. func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error { cmd := c.commandFn("erase") cmd.Input(strings.NewReader(serverURL)) diff --git a/cliconfig/credentials/native_store_test.go b/cliconfig/credentials/native_store_test.go index 454fd0bd91..354221027e 100644 --- a/cliconfig/credentials/native_store_test.go +++ b/cliconfig/credentials/native_store_test.go @@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) { } case "get": switch inS { - case validServerAddress, validServerAddress2: - return []byte(`{"Username": "foo", "Password": "bar"}`), nil + case validServerAddress: + return []byte(`{"Username": "foo", "Secret": "bar"}`), nil + case validServerAddress2: + return []byte(`{"Username": "", "Secret": "abcd1234"}`), nil case missingCredsAddress: return []byte(errCredentialsNotFound.Error()), errCommandExited case invalidServerAddress: @@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) { if a.Password != "" { t.Fatalf("expected password to be empty, got %s", a.Password) } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } if a.Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com`, got %s", a.Email) } @@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) { if a.Password != "bar" { t.Fatalf("expected password `bar`, got %s", a.Password) } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } if a.Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com`, got %s", a.Email) } } +func TestNativeStoreGetIdentityToken(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress2: { + Email: "foo@example2.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress2) + if err != nil { + t.Fatal(err) + } + + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken) + } + if a.Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", a.Email) + } +} + func TestNativeStoreGetAll(t *testing.T) { f := newConfigFile(map[string]types.AuthConfig{ validServerAddress: { @@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) { if as[validServerAddress].Password != "bar" { t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) } + if as[validServerAddress].IdentityToken != "" { + t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) + } if as[validServerAddress].Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) } - if as[validServerAddress2].Username != "foo" { - t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username) + if as[validServerAddress2].Username != "" { + t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) } - if as[validServerAddress2].Password != "bar" { - t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + if as[validServerAddress2].Password != "" { + t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + } + if as[validServerAddress2].IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) } if as[validServerAddress2].Email != "foo@example2.com" { t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email) diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index 34a7228427..c92c97156f 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -78,17 +78,20 @@ The helpers always use the first argument in the command to identify the action. There are only three possible values for that argument: `store`, `get`, and `erase`. The `store` command takes a JSON payload from the standard input. That payload carries -the server address, to identify the credential, the user name and the password. -This is an example of that payload: +the server address, to identify the credential, the user name, and either a password +or an identity token. ```json { "ServerURL": "https://index.docker.io/v1", "Username": "david", - "Password": "passw0rd1" + "Secret": "passw0rd1" } ``` +If the secret being stored is an identity token, the Username should be set to +``. + The `store` command can write error messages to `STDOUT` that the docker engine will show if there was an issue. @@ -102,7 +105,7 @@ and password from this payload: ```json { "Username": "david", - "Password": "passw0rd1" + "Secret": "passw0rd1" } ``` diff --git a/integration-cli/fixtures/auth/docker-credential-shell-test b/integration-cli/fixtures/auth/docker-credential-shell-test index 0c94bcd216..1980bb1803 100755 --- a/integration-cli/fixtures/auth/docker-credential-shell-test +++ b/integration-cli/fixtures/auth/docker-credential-shell-test @@ -8,8 +8,8 @@ case $1 in server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}') username=$(echo "$in" | jq --raw-output ".Username") - password=$(echo "$in" | jq --raw-output ".Password") - echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server + password=$(echo "$in" | jq --raw-output ".Secret") + echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server ;; "get") in=$( Date: Fri, 4 Mar 2016 13:31:40 -0800 Subject: [PATCH 5/5] Update docs to mention identity token usage for /auth Signed-off-by: Derek McGowan (github: dmcgowan) --- docs/reference/api/docker_remote_api.md | 1 + docs/reference/api/docker_remote_api_v1.23.md | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 75edd928bf..5aee96c53a 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -125,6 +125,7 @@ This section lists each version from latest to oldest. Each listing includes a * `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported. * `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported. * `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported. +* `POST /auth` now returns an `IdentityToken` when supported by a registry. ### v1.22 API changes diff --git a/docs/reference/api/docker_remote_api_v1.23.md b/docs/reference/api/docker_remote_api_v1.23.md index c2902dac06..c9cbf859ff 100644 --- a/docs/reference/api/docker_remote_api_v1.23.md +++ b/docs/reference/api/docker_remote_api_v1.23.md @@ -1957,11 +1957,11 @@ Request Headers: } ``` - - Token based login: + - Identity token based login: ``` { - "registrytoken": "9cbaf023786cd7..." + "identitytoken": "9cbaf023786cd7..." } ``` @@ -2091,7 +2091,8 @@ Status Codes: `POST /auth` -Get the default username and email +Validate credentials for a registry and get identity token, +if available, for accessing the registry without password. **Example request**: @@ -2099,9 +2100,8 @@ Get the default username and email Content-Type: application/json { - "username":" hannibal", - "password: "xxxx", - "email": "hannibal@a-team.com", + "username": "hannibal", + "password": "xxxx", "serveraddress": "https://index.docker.io/v1/" } @@ -2109,6 +2109,11 @@ Get the default username and email HTTP/1.1 200 OK + { + "Status": "Login Succeeded", + "IdentityToken": "9cbaf023786cd7..." + } + Status Codes: - **200** – no error