From ba0aa5311aa27fe77166f03d8bcc0174e2985913 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 4 Mar 2016 12:00:18 -0800 Subject: [PATCH] Add support for identity tokens in client credentials store Update unit test and documentation to handle the new case where Username is set to to indicate an identity token is involved. Change the "Password" field in communications with the credential helper to "Secret" to make clear it has a more generic purpose. Signed-off-by: Aaron Lehmann --- api/client/info.go | 2 +- cliconfig/credentials/native_store.go | 30 +++++++--- cliconfig/credentials/native_store_test.go | 57 +++++++++++++++++-- docs/reference/commandline/login.md | 11 ++-- .../auth/docker-credential-shell-test | 4 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/api/client/info.go b/api/client/info.go index 2d02af3a58..2959424378 100644 --- a/api/client/info.go +++ b/api/client/info.go @@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error { u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username if len(u) > 0 { fmt.Fprintf(cli.out, "Username: %v\n", u) - fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) } + fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) } // Only output these warnings if the server does not support these features diff --git a/cliconfig/credentials/native_store.go b/cliconfig/credentials/native_store.go index 2da041d4b2..9b8997dd64 100644 --- a/cliconfig/credentials/native_store.go +++ b/cliconfig/credentials/native_store.go @@ -13,7 +13,10 @@ import ( "github.com/docker/engine-api/types" ) -const remoteCredentialsPrefix = "docker-credential-" +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) // Standarize the not found error, so every helper returns // the same message and docker can handle it properly. @@ -29,14 +32,14 @@ type command interface { type credentialsRequest struct { ServerURL string Username string - Password string + Secret string } // credentialsGetResponse is the information serialized from a remote store // when the plugin sends requests to get the user credentials. type credentialsGetResponse struct { Username string - Password string + Secret string } // nativeStore implements a credentials store @@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) { return auth, err } auth.Username = creds.Username + auth.IdentityToken = creds.IdentityToken auth.Password = creds.Password return auth, nil @@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { creds, _ := c.getCredentialsFromStore(s) ac.Username = creds.Username ac.Password = creds.Password + ac.IdentityToken = creds.IdentityToken auths[s] = ac } @@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error { } authConfig.Username = "" authConfig.Password = "" + authConfig.IdentityToken = "" // Fallback to old credential in plain text to save only the email return c.fileStore.Store(authConfig) @@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error { creds := &credentialsRequest{ ServerURL: config.ServerAddress, Username: config.Username, - Password: config.Password, + Secret: config.Password, + } + + if config.IdentityToken != "" { + creds.Username = tokenUsername + creds.Secret = config.IdentityToken } buffer := new(bytes.Buffer) @@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC return ret, err } - ret.Username = resp.Username - ret.Password = resp.Password + if resp.Username == tokenUsername { + ret.IdentityToken = resp.Secret + } else { + ret.Password = resp.Secret + ret.Username = resp.Username + } + ret.ServerAddress = serverAddress return ret, nil } -// eraseCredentialsFromStore executes the command to remove the server redentails from the native store. +// eraseCredentialsFromStore executes the command to remove the server credentails from the native store. func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error { cmd := c.commandFn("erase") cmd.Input(strings.NewReader(serverURL)) diff --git a/cliconfig/credentials/native_store_test.go b/cliconfig/credentials/native_store_test.go index 454fd0bd91..354221027e 100644 --- a/cliconfig/credentials/native_store_test.go +++ b/cliconfig/credentials/native_store_test.go @@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) { } case "get": switch inS { - case validServerAddress, validServerAddress2: - return []byte(`{"Username": "foo", "Password": "bar"}`), nil + case validServerAddress: + return []byte(`{"Username": "foo", "Secret": "bar"}`), nil + case validServerAddress2: + return []byte(`{"Username": "", "Secret": "abcd1234"}`), nil case missingCredsAddress: return []byte(errCredentialsNotFound.Error()), errCommandExited case invalidServerAddress: @@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) { if a.Password != "" { t.Fatalf("expected password to be empty, got %s", a.Password) } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } if a.Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com`, got %s", a.Email) } @@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) { if a.Password != "bar" { t.Fatalf("expected password `bar`, got %s", a.Password) } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } if a.Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com`, got %s", a.Email) } } +func TestNativeStoreGetIdentityToken(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress2: { + Email: "foo@example2.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + commandFn: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress2) + if err != nil { + t.Fatal(err) + } + + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken) + } + if a.Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", a.Email) + } +} + func TestNativeStoreGetAll(t *testing.T) { f := newConfigFile(map[string]types.AuthConfig{ validServerAddress: { @@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) { if as[validServerAddress].Password != "bar" { t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) } + if as[validServerAddress].IdentityToken != "" { + t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) + } if as[validServerAddress].Email != "foo@example.com" { t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) } - if as[validServerAddress2].Username != "foo" { - t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username) + if as[validServerAddress2].Username != "" { + t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) } - if as[validServerAddress2].Password != "bar" { - t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + if as[validServerAddress2].Password != "" { + t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + } + if as[validServerAddress2].IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) } if as[validServerAddress2].Email != "foo@example2.com" { t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email) diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index 34a7228427..c92c97156f 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -78,17 +78,20 @@ The helpers always use the first argument in the command to identify the action. There are only three possible values for that argument: `store`, `get`, and `erase`. The `store` command takes a JSON payload from the standard input. That payload carries -the server address, to identify the credential, the user name and the password. -This is an example of that payload: +the server address, to identify the credential, the user name, and either a password +or an identity token. ```json { "ServerURL": "https://index.docker.io/v1", "Username": "david", - "Password": "passw0rd1" + "Secret": "passw0rd1" } ``` +If the secret being stored is an identity token, the Username should be set to +``. + The `store` command can write error messages to `STDOUT` that the docker engine will show if there was an issue. @@ -102,7 +105,7 @@ and password from this payload: ```json { "Username": "david", - "Password": "passw0rd1" + "Secret": "passw0rd1" } ``` diff --git a/integration-cli/fixtures/auth/docker-credential-shell-test b/integration-cli/fixtures/auth/docker-credential-shell-test index 0c94bcd216..1980bb1803 100755 --- a/integration-cli/fixtures/auth/docker-credential-shell-test +++ b/integration-cli/fixtures/auth/docker-credential-shell-test @@ -8,8 +8,8 @@ case $1 in server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}') username=$(echo "$in" | jq --raw-output ".Username") - password=$(echo "$in" | jq --raw-output ".Password") - echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server + password=$(echo "$in" | jq --raw-output ".Secret") + echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server ;; "get") in=$(