Merge pull request #20970 from dmcgowan/login-oauth

OAuth support for registries
This commit is contained in:
Vincent Demeester 2016-03-14 15:49:44 +01:00
commit b9361f02da
30 changed files with 505 additions and 185 deletions

View File

@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
if len(u) > 0 { if len(u) > 0 {
fmt.Fprintf(cli.out, "Username: %v\n", u) 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 // Only output these warnings if the server does not support these features

View File

@ -57,12 +57,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
return err return err
} }
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err := storeCredentials(cli.configFile, authConfig); err != nil { if err := storeCredentials(cli.configFile, authConfig); err != nil {
return fmt.Errorf("Error saving credentials: %v", err) return fmt.Errorf("Error saving credentials: %v", err)
} }
if response.Status != "" { if response.Status != "" {
fmt.Fprintf(cli.out, "%s\n", response.Status) fmt.Fprintln(cli.out, response.Status)
} }
return nil return nil
} }
@ -120,6 +124,7 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, is
authconfig.Username = flUser authconfig.Username = flUser
authconfig.Password = flPassword authconfig.Password = flPassword
authconfig.ServerAddress = serverAddress authconfig.ServerAddress = serverAddress
authconfig.IdentityToken = ""
return authconfig, nil return authconfig, nil
} }

View File

@ -107,6 +107,13 @@ func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
return scs.auth.Username, scs.auth.Password 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 // getNotaryRepository returns a NotaryRepository which stores all the
// information needed to operate on a notary repository. // information needed to operate on a notary repository.
// It creates a HTTP transport providing authentication support. // It creates a HTTP transport providing authentication support.

View File

@ -13,5 +13,5 @@ type Backend interface {
SystemVersion() types.Version SystemVersion() types.Version
SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{}) SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
UnsubscribeFromEvents(chan interface{}) UnsubscribeFromEvents(chan interface{})
AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
} }

View File

@ -115,11 +115,12 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
if err != nil { if err != nil {
return err return err
} }
status, err := s.backend.AuthenticateToRegistry(config) status, token, err := s.backend.AuthenticateToRegistry(config)
if err != nil { if err != nil {
return err return err
} }
return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{ return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{
Status: status, Status: status,
IdentityToken: token,
}) })
} }

View File

@ -13,7 +13,10 @@ import (
"github.com/docker/engine-api/types" "github.com/docker/engine-api/types"
) )
const remoteCredentialsPrefix = "docker-credential-" const (
remoteCredentialsPrefix = "docker-credential-"
tokenUsername = "<token>"
)
// Standarize the not found error, so every helper returns // Standarize the not found error, so every helper returns
// the same message and docker can handle it properly. // the same message and docker can handle it properly.
@ -29,14 +32,14 @@ type command interface {
type credentialsRequest struct { type credentialsRequest struct {
ServerURL string ServerURL string
Username string Username string
Password string Secret string
} }
// credentialsGetResponse is the information serialized from a remote store // credentialsGetResponse is the information serialized from a remote store
// when the plugin sends requests to get the user credentials. // when the plugin sends requests to get the user credentials.
type credentialsGetResponse struct { type credentialsGetResponse struct {
Username string Username string
Password string Secret string
} }
// nativeStore implements a credentials store // nativeStore implements a credentials store
@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
return auth, err return auth, err
} }
auth.Username = creds.Username auth.Username = creds.Username
auth.IdentityToken = creds.IdentityToken
auth.Password = creds.Password auth.Password = creds.Password
return auth, nil return auth, nil
@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
creds, _ := c.getCredentialsFromStore(s) creds, _ := c.getCredentialsFromStore(s)
ac.Username = creds.Username ac.Username = creds.Username
ac.Password = creds.Password ac.Password = creds.Password
ac.IdentityToken = creds.IdentityToken
auths[s] = ac auths[s] = ac
} }
@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
} }
authConfig.Username = "" authConfig.Username = ""
authConfig.Password = "" authConfig.Password = ""
authConfig.IdentityToken = ""
// Fallback to old credential in plain text to save only the email // Fallback to old credential in plain text to save only the email
return c.fileStore.Store(authConfig) return c.fileStore.Store(authConfig)
@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
creds := &credentialsRequest{ creds := &credentialsRequest{
ServerURL: config.ServerAddress, ServerURL: config.ServerAddress,
Username: config.Username, Username: config.Username,
Password: config.Password, Secret: config.Password,
}
if config.IdentityToken != "" {
creds.Username = tokenUsername
creds.Secret = config.IdentityToken
} }
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC
return ret, err return ret, err
} }
ret.Username = resp.Username if resp.Username == tokenUsername {
ret.Password = resp.Password ret.IdentityToken = resp.Secret
} else {
ret.Password = resp.Secret
ret.Username = resp.Username
}
ret.ServerAddress = serverAddress ret.ServerAddress = serverAddress
return ret, nil 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 { func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
cmd := c.commandFn("erase") cmd := c.commandFn("erase")
cmd.Input(strings.NewReader(serverURL)) cmd.Input(strings.NewReader(serverURL))

View File

@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) {
} }
case "get": case "get":
switch inS { switch inS {
case validServerAddress, validServerAddress2: case validServerAddress:
return []byte(`{"Username": "foo", "Password": "bar"}`), nil return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
case validServerAddress2:
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
case missingCredsAddress: case missingCredsAddress:
return []byte(errCredentialsNotFound.Error()), errCommandExited return []byte(errCredentialsNotFound.Error()), errCommandExited
case invalidServerAddress: case invalidServerAddress:
@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) {
if a.Password != "" { if a.Password != "" {
t.Fatalf("expected password to be empty, got %s", 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" { if a.Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com`, got %s", a.Email) t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
} }
@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) {
if a.Password != "bar" { if a.Password != "bar" {
t.Fatalf("expected password `bar`, got %s", a.Password) 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" { if a.Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com`, got %s", a.Email) 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) { func TestNativeStoreGetAll(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newConfigFile(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) {
if as[validServerAddress].Password != "bar" { if as[validServerAddress].Password != "bar" {
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) 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" { if as[validServerAddress].Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
} }
if as[validServerAddress2].Username != "foo" { if as[validServerAddress2].Username != "" {
t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username) t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
} }
if as[validServerAddress2].Password != "bar" { if as[validServerAddress2].Password != "" {
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, 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" { if as[validServerAddress2].Email != "foo@example2.com" {
t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email) t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email)

View File

@ -1519,7 +1519,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
} }
// AuthenticateToRegistry checks the validity of credentials in authConfig // 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()) return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
} }

View File

@ -26,6 +26,13 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
return dcs.auth.Username, dcs.auth.Password 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 // NewV2Repository returns a repository (v2 only). It creates a HTTP transport
// providing timeout settings and authentication support, and also verifies the // providing timeout settings and authentication support, and also verifies the
// remote API version. // remote API version.
@ -72,7 +79,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
} else { } else {
creds := dumbCredentialStore{auth: authConfig} 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) basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
} }

View File

@ -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. * `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. * `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. * `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 ### v1.22 API changes

View File

@ -1985,11 +1985,11 @@ Request Headers:
} }
``` ```
- Token based login: - Identity token based login:
``` ```
{ {
"registrytoken": "9cbaf023786cd7..." "identitytoken": "9cbaf023786cd7..."
} }
``` ```
@ -2119,7 +2119,8 @@ Status Codes:
`POST /auth` `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**: **Example request**:
@ -2127,9 +2128,8 @@ Get the default username and email
Content-Type: application/json Content-Type: application/json
{ {
"username":" hannibal", "username": "hannibal",
"password: "xxxx", "password": "xxxx",
"email": "hannibal@a-team.com",
"serveraddress": "https://index.docker.io/v1/" "serveraddress": "https://index.docker.io/v1/"
} }
@ -2137,6 +2137,11 @@ Get the default username and email
HTTP/1.1 200 OK HTTP/1.1 200 OK
{
"Status": "Login Succeeded",
"IdentityToken": "9cbaf023786cd7..."
}
Status Codes: Status Codes:
- **200** no error - **200** no error

View File

@ -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`. 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 `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. the server address, to identify the credential, the user name, and either a password
This is an example of that payload: or an identity token.
```json ```json
{ {
"ServerURL": "https://index.docker.io/v1", "ServerURL": "https://index.docker.io/v1",
"Username": "david", "Username": "david",
"Password": "passw0rd1" "Secret": "passw0rd1"
} }
``` ```
If the secret being stored is an identity token, the Username should be set to
`<token>`.
The `store` command can write error messages to `STDOUT` that the docker engine The `store` command can write error messages to `STDOUT` that the docker engine
will show if there was an issue. will show if there was an issue.
@ -102,7 +105,7 @@ and password from this payload:
```json ```json
{ {
"Username": "david", "Username": "david",
"Password": "passw0rd1" "Secret": "passw0rd1"
} }
``` ```

View File

@ -48,7 +48,7 @@ clone git github.com/boltdb/bolt v1.1.0
clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
# get graph and distribution packages # 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 clone git github.com/vbatts/tar-split v0.9.11
# get desired notary commit, might also need to be updated in Dockerfile # get desired notary commit, might also need to be updated in Dockerfile

View File

@ -8,8 +8,8 @@ case $1 in
server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}') server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}')
username=$(echo "$in" | jq --raw-output ".Username") username=$(echo "$in" | jq --raw-output ".Username")
password=$(echo "$in" | jq --raw-output ".Password") password=$(echo "$in" | jq --raw-output ".Secret")
echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server
;; ;;
"get") "get")
in=$(</dev/stdin) in=$(</dev/stdin)

View File

@ -15,11 +15,16 @@ import (
registrytypes "github.com/docker/engine-api/types/registry" 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. // 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) registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
if err != nil { if err != nil {
return "", err return "", "", err
} }
serverAddress := registryEndpoint.String() 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) logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
if serverAddress == "" { if serverAddress == "" {
return "", fmt.Errorf("Server Error: Server Address not set.") return "", "", fmt.Errorf("Server Error: Server Address not set.")
} }
loginAgainstOfficialIndex := serverAddress == IndexServer loginAgainstOfficialIndex := serverAddress == IndexServer
req, err := http.NewRequest("GET", serverAddress+"users/", nil) req, err := http.NewRequest("GET", serverAddress+"users/", nil)
if err != nil { if err != nil {
return "", err return "", "", err
} }
req.SetBasicAuth(authConfig.Username, authConfig.Password) req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := registryEndpoint.client.Do(req) resp, err := registryEndpoint.client.Do(req)
if err != nil { if err != nil {
// fallback when request could not be completed // fallback when request could not be completed
return "", fallbackError{ return "", "", fallbackError{
err: err, err: err,
} }
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return "", "", err
} }
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
return "Login Succeeded", nil return "Login Succeeded", "", nil
} else if resp.StatusCode == http.StatusUnauthorized { } else if resp.StatusCode == http.StatusUnauthorized {
if loginAgainstOfficialIndex { 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 { } else if resp.StatusCode == http.StatusForbidden {
if loginAgainstOfficialIndex { 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? // *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 } 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) logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
return "", fmt.Errorf("Internal Server Error") return "", "", fmt.Errorf("Internal Server Error")
} else {
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
} }
return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
} }
type loginCredentialStore struct { type loginCredentialStore struct {
@ -79,6 +83,14 @@ func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
return lcs.authConfig.Username, lcs.authConfig.Password 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 { type fallbackError struct {
err error err error
} }
@ -90,7 +102,7 @@ func (err fallbackError) Error() string {
// loginV2 tries to login to the v2 registry server. The given registry // loginV2 tries to login to the v2 registry server. The given registry
// endpoint will be pinged to get authorization challenges. These challenges // endpoint will be pinged to get authorization challenges. These challenges
// will be used to authenticate against the registry to validate credentials. // 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) logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
modifiers := DockerHeaders(userAgent, nil) modifiers := DockerHeaders(userAgent, nil)
@ -101,14 +113,21 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
if !foundV2 { if !foundV2 {
err = fallbackError{err: err} err = fallbackError{err: err}
} }
return "", err return "", "", err
} }
credentialAuthConfig := *authConfig
creds := loginCredentialStore{ 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) basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
tr := transport.NewTransport(authTransport, modifiers...) tr := transport.NewTransport(authTransport, modifiers...)
@ -124,7 +143,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
if !foundV2 { if !foundV2 {
err = fallbackError{err: err} err = fallbackError{err: err}
} }
return "", err return "", "", err
} }
resp, err := loginClient.Do(req) resp, err := loginClient.Do(req)
@ -132,7 +151,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
if !foundV2 { if !foundV2 {
err = fallbackError{err: err} err = fallbackError{err: err}
} }
return "", err return "", "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -142,10 +161,10 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
if !foundV2 { if !foundV2 {
err = fallbackError{err: err} err = fallbackError{err: err}
} }
return "", err return "", "", err
} }
return "Login Succeeded", nil return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
} }

View File

@ -2,6 +2,7 @@ package registry
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -34,10 +35,19 @@ func (s *Service) ServiceConfig() *registrytypes.ServiceConfig {
// Auth contacts the public registry with the provided credentials, // Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful. // and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials. // 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) { func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress) serverAddress := authConfig.ServerAddress
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
serverAddress = "https://" + serverAddress
}
u, err := url.Parse(serverAddress)
if err != nil { 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 { for _, endpoint := range endpoints {
@ -46,7 +56,7 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s
login = loginV1 login = loginV1
} }
status, err = login(authConfig, endpoint, userAgent) status, token, err = login(authConfig, endpoint, userAgent)
if err == nil { if err == nil {
return return
} }
@ -55,10 +65,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) logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
continue continue
} }
return "", err return "", "", err
} }
return "", err return "", "", err
} }
// splitReposSearchTerm breaks a search term into an index name and remote name // splitReposSearchTerm breaks a search term into an index name and remote name

View File

@ -10,7 +10,7 @@ import (
func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg tlsConfig := &cfg
if hostname == DefaultNamespace { if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {
// v2 mirrors // v2 mirrors
for _, mirror := range s.config.Mirrors { for _, mirror := range s.config.Mirrors {
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {

View File

@ -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. 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 ## Coding Style

View File

@ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries
VOLUME ["/var/lib/registry"] VOLUME ["/var/lib/registry"]
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["registry"] ENTRYPOINT ["registry"]
CMD ["/etc/docker/registry/config.yml"] CMD ["serve", "/etc/docker/registry/config.yml"]

View File

@ -14,8 +14,8 @@ endif
GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)" GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)"
.PHONY: clean all fmt vet lint build test binaries .PHONY: clean all fmt vet lint build test binaries
.DEFAULT: default .DEFAULT: all
all: AUTHORS clean fmt vet fmt lint build test binaries all: fmt vet fmt lint build test binaries
AUTHORS: .mailmap .git/HEAD AUTHORS: .mailmap .git/HEAD
git log --format='%aN <%aE>' | sort -fu > $@ git log --format='%aN <%aE>' | sort -fu > $@

View File

@ -128,4 +128,4 @@ avenues are available for support:
## License ## License
This project is distributed under [Apache License, Version 2.0](LICENSE.md). This project is distributed under [Apache License, Version 2.0](LICENSE).

View File

@ -97,6 +97,11 @@ type BlobDeleter interface {
Delete(ctx context.Context, dgst digest.Digest) error 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 // BlobDescriptorService manages metadata about a blob by digest. Most
// implementations will not expose such an interface explicitly. Such mappings // implementations will not expose such an interface explicitly. Such mappings
// should be maintained by interacting with the BlobIngester. Hence, this is // should be maintained by interacting with the BlobIngester. Hence, this is

View File

@ -8,6 +8,10 @@ import (
"github.com/docker/distribution/digest" "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 // ErrManifestNotModified is returned when a conditional manifest GetByTag
// returns nil due to the client indicating it has the latest version // returns nil due to the client indicating it has the latest version
var ErrManifestNotModified = errors.New("manifest not modified") var ErrManifestNotModified = errors.New("manifest not modified")

View File

@ -53,12 +53,18 @@ type ManifestService interface {
// Delete removes the manifest specified by the given digest. Deleting // Delete removes the manifest specified by the given digest. Deleting
// a manifest that doesn't exist will return ErrManifestNotFound // a manifest that doesn't exist will return ErrManifestNotFound
Delete(ctx context.Context, dgst digest.Digest) error Delete(ctx context.Context, dgst digest.Digest) error
}
// Enumerate fills 'manifests' with the manifests in this service up // ManifestEnumerator enables iterating over manifests
// to the size of 'manifests' and returns 'n' for the number of entries type ManifestEnumerator interface {
// which were filled. 'last' contains an offset in the manifest set // Enumerate calls ingester for each manifest.
// and can be used to resume iteration. Enumerate(ctx context.Context, ingester func(digest.Digest) error) error
//Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err 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 // Describable is an interface for descriptors

View File

@ -3,7 +3,7 @@
// //
// Grammar // Grammar
// //
// reference := repository [ ":" tag ] [ "@" digest ] // reference := name [ ":" tag ] [ "@" digest ]
// name := [hostname '/'] component ['/' component]* // name := [hostname '/'] component ['/' component]*
// hostname := hostcomponent ['.' hostcomponent]* [':' port-number] // hostname := hostcomponent ['.' hostcomponent]* [':' port-number]
// hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/

View File

@ -40,6 +40,17 @@ type Namespace interface {
// which were filled. 'last' contains an offset in the catalog, and 'err' will be // 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. // set to io.EOF if there are no more entries to obtain.
Repositories(ctx context.Context, repos []string, last string) (n int, err error) 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 // ManifestServiceOption is a function argument for Manifest Service methods

View File

@ -514,7 +514,7 @@ var routeDescriptors = []RouteDescriptor{
digestHeader, digestHeader,
}, },
Body: BodyDescriptor{ Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8", ContentType: "<media type of manifest>",
Format: manifestBody, Format: manifestBody,
}, },
}, },
@ -553,7 +553,7 @@ var routeDescriptors = []RouteDescriptor{
referenceParameterDescriptor, referenceParameterDescriptor,
}, },
Body: BodyDescriptor{ Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8", ContentType: "<media type of manifest>",
Format: manifestBody, Format: manifestBody,
}, },
Successes: []ResponseDescriptor{ Successes: []ResponseDescriptor{

View File

@ -19,6 +19,8 @@ import (
// basic auth due to lack of credentials. // basic auth due to lack of credentials.
var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials") var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
const defaultClientID = "registry-client"
// AuthenticationHandler is an interface for authorizing a request from // AuthenticationHandler is an interface for authorizing a request from
// params from a "WWW-Authenicate" header for a single scheme. // params from a "WWW-Authenicate" header for a single scheme.
type AuthenticationHandler interface { type AuthenticationHandler interface {
@ -36,6 +38,14 @@ type AuthenticationHandler interface {
type CredentialStore interface { type CredentialStore interface {
// Basic returns basic auth for the given URL // Basic returns basic auth for the given URL
Basic(*url.URL) (string, string) 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 // NewAuthorizer creates an authorizer which can handle multiple authentication
@ -105,27 +115,47 @@ type clock interface {
type tokenHandler struct { type tokenHandler struct {
header http.Header header http.Header
creds CredentialStore creds CredentialStore
scope tokenScope
transport http.RoundTripper transport http.RoundTripper
clock clock clock clock
offlineAccess bool
forceOAuth bool
clientID string
scopes []Scope
tokenLock sync.Mutex tokenLock sync.Mutex
tokenCache string tokenCache string
tokenExpiration time.Time tokenExpiration time.Time
additionalScopes map[string]struct{}
} }
// tokenScope represents the scope at which a token will be requested. // Scope is a type which is serializable to a string
// This represents a specific action on a registry resource. // using the allow scope grammar.
type tokenScope struct { type Scope interface {
Resource string String() string
Scope string
Actions []string
} }
func (ts tokenScope) String() string { // RepositoryScope represents a token scope for access
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) // 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. // 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 // NewTokenHandler creates a new AuthenicationHandler which supports
// fetching tokens from a remote token server. // fetching tokens from a remote token server.
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { 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. // NewTokenHandlerWithOptions creates a new token handler using the provided
func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler { // options structure.
return &tokenHandler{ func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
transport: transport, handler := &tokenHandler{
creds: creds, transport: options.Transport,
clock: c, creds: options.Credentials,
scope: tokenScope{ offlineAccess: options.OfflineAccess,
Resource: "repository", forceOAuth: options.ForceOAuth,
Scope: scope, clientID: options.ClientID,
Actions: actions, scopes: options.Scopes,
}, clock: realClock{},
additionalScopes: map[string]struct{}{},
} }
return handler
} }
func (th *tokenHandler) client() *http.Client { 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 { func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
var additionalScopes []string var additionalScopes []string
if fromParam := req.URL.Query().Get("from"); fromParam != "" { if fromParam := req.URL.Query().Get("from"); fromParam != "" {
additionalScopes = append(additionalScopes, tokenScope{ additionalScopes = append(additionalScopes, RepositoryScope{
Resource: "repository", Repository: fromParam,
Scope: fromParam, Actions: []string{"pull"},
Actions: []string{"pull"},
}.String()) }.String())
} }
if err := th.refreshToken(params, additionalScopes...); err != nil {
token, err := th.getToken(params, additionalScopes...)
if err != nil {
return err return err
} }
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return nil 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() th.tokenLock.Lock()
defer th.tokenLock.Unlock() 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 var addedScopes bool
for _, scope := range additionalScopes { for _, scope := range additionalScopes {
if _, ok := th.additionalScopes[scope]; !ok { scopes = append(scopes, scope)
th.additionalScopes[scope] = struct{}{} addedScopes = true
addedScopes = true
}
} }
now := th.clock.Now() now := th.clock.Now()
if now.After(th.tokenExpiration) || addedScopes { if now.After(th.tokenExpiration) || addedScopes {
tr, err := th.fetchToken(params) token, expiration, err := th.fetchToken(params, scopes)
if err != nil { 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 { type postTokenResponse struct {
Token string `json:"token"` AccessToken string `json:"access_token"`
AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"` IssuedAt time.Time `json:"issued_at"`
Scope string `json:"scope"`
} }
func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) { func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username) form := url.Values{}
realm, ok := params["realm"] form.Set("scope", strings.Join(scopes, " "))
if !ok { form.Set("service", service)
return nil, errors.New("no realm specified for token auth challenge")
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 resp, err := th.client().PostForm(realm.String(), form)
realmURL, err := url.Parse(realm)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid token auth challenge realm: %s", err) return "", time.Time{}, 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
} }
defer resp.Body.Close() defer resp.Body.Close()
if !client.SuccessStatus(resp.StatusCode) { if !client.SuccessStatus(resp.StatusCode) {
err := client.HandleErrorResponse(resp) err := client.HandleErrorResponse(resp)
return nil, err return "", time.Time{}, err
} }
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
tr := new(tokenResponse) var tr postTokenResponse
if err = decoder.Decode(tr); err != nil { if err = decoder.Decode(&tr); err != nil {
return nil, fmt.Errorf("unable to decode token response: %s", err) return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
} }
// `access_token` is equivalent to `token` and if both are specified if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
// the choice is undefined. Canonicalize `access_token` by sticking th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
// 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.ExpiresIn < minimumTokenLifetimeSeconds { if tr.ExpiresIn < minimumTokenLifetimeSeconds {
@ -296,10 +324,128 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
if tr.IssuedAt.IsZero() { if tr.IssuedAt.IsZero() {
// issued_at is optional in the token response. // 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 { type basicHandler struct {

View File

@ -292,9 +292,18 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er
if err != nil { if err != nil {
return distribution.Descriptor{}, err 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: check:
if err != nil { if err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err
@ -304,7 +313,16 @@ check:
case resp.StatusCode >= 200 && resp.StatusCode < 400: case resp.StatusCode >= 200 && resp.StatusCode < 400:
return descriptorFromResponse(resp) return descriptorFromResponse(resp)
case resp.StatusCode == http.StatusMethodNotAllowed: 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++ attempts++
if attempts > 1 { if attempts > 1 {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err

View File

@ -66,7 +66,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
return 0, hrs.err 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 // connection. This logic is here instead of Seek so that if
// a seek is undone before the next read, the connection doesn't // a seek is undone before the next read, the connection doesn't
// need to be closed and reopened. A common example of this is // need to be closed and reopened. A common example of this is