Merge pull request #20970 from dmcgowan/login-oauth
OAuth support for registries
This commit is contained in:
commit
b9361f02da
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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://") {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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 > $@
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue