Add support for identity tokens in client credentials store

Update unit test and documentation to handle the new case where Username
is set to <token> 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 <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2016-03-04 12:00:18 -08:00 committed by Derek McGowan
parent a6d0c66b4c
commit ba0aa5311a
5 changed files with 84 additions and 20 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

@ -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

@ -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

@ -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)