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
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue