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:
parent
a6d0c66b4c
commit
ba0aa5311a
5 changed files with 84 additions and 20 deletions
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue