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

View File

@ -13,7 +13,10 @@ import (
"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
// 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))

View File

@ -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": "<token>", "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)

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`.
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
`<token>`.
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"
}
```

View File

@ -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=$(</dev/stdin)