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>
| ... | ... |
@@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
|
| 93 | 93 |
u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username |
| 94 | 94 |
if len(u) > 0 {
|
| 95 | 95 |
fmt.Fprintf(cli.out, "Username: %v\n", u) |
| 96 |
- fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) |
|
| 97 | 96 |
} |
| 97 |
+ fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) |
|
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 | 100 |
// Only output these warnings if the server does not support these features |
| ... | ... |
@@ -13,7 +13,10 @@ import ( |
| 13 | 13 |
"github.com/docker/engine-api/types" |
| 14 | 14 |
) |
| 15 | 15 |
|
| 16 |
-const remoteCredentialsPrefix = "docker-credential-" |
|
| 16 |
+const ( |
|
| 17 |
+ remoteCredentialsPrefix = "docker-credential-" |
|
| 18 |
+ tokenUsername = "<token>" |
|
| 19 |
+) |
|
| 17 | 20 |
|
| 18 | 21 |
// Standarize the not found error, so every helper returns |
| 19 | 22 |
// the same message and docker can handle it properly. |
| ... | ... |
@@ -29,14 +32,14 @@ type command interface {
|
| 29 | 29 |
type credentialsRequest struct {
|
| 30 | 30 |
ServerURL string |
| 31 | 31 |
Username string |
| 32 |
- Password string |
|
| 32 |
+ Secret string |
|
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 | 35 |
// credentialsGetResponse is the information serialized from a remote store |
| 36 | 36 |
// when the plugin sends requests to get the user credentials. |
| 37 | 37 |
type credentialsGetResponse struct {
|
| 38 | 38 |
Username string |
| 39 |
- Password string |
|
| 39 |
+ Secret string |
|
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 | 42 |
// nativeStore implements a credentials store |
| ... | ... |
@@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
|
| 76 | 76 |
return auth, err |
| 77 | 77 |
} |
| 78 | 78 |
auth.Username = creds.Username |
| 79 |
+ auth.IdentityToken = creds.IdentityToken |
|
| 79 | 80 |
auth.Password = creds.Password |
| 80 | 81 |
|
| 81 | 82 |
return auth, nil |
| ... | ... |
@@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
|
| 89 | 89 |
creds, _ := c.getCredentialsFromStore(s) |
| 90 | 90 |
ac.Username = creds.Username |
| 91 | 91 |
ac.Password = creds.Password |
| 92 |
+ ac.IdentityToken = creds.IdentityToken |
|
| 92 | 93 |
auths[s] = ac |
| 93 | 94 |
} |
| 94 | 95 |
|
| ... | ... |
@@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
|
| 102 | 102 |
} |
| 103 | 103 |
authConfig.Username = "" |
| 104 | 104 |
authConfig.Password = "" |
| 105 |
+ authConfig.IdentityToken = "" |
|
| 105 | 106 |
|
| 106 | 107 |
// Fallback to old credential in plain text to save only the email |
| 107 | 108 |
return c.fileStore.Store(authConfig) |
| ... | ... |
@@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
|
| 113 | 113 |
creds := &credentialsRequest{
|
| 114 | 114 |
ServerURL: config.ServerAddress, |
| 115 | 115 |
Username: config.Username, |
| 116 |
- Password: config.Password, |
|
| 116 |
+ Secret: config.Password, |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ if config.IdentityToken != "" {
|
|
| 120 |
+ creds.Username = tokenUsername |
|
| 121 |
+ creds.Secret = config.IdentityToken |
|
| 117 | 122 |
} |
| 118 | 123 |
|
| 119 | 124 |
buffer := new(bytes.Buffer) |
| ... | ... |
@@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC |
| 158 | 158 |
return ret, err |
| 159 | 159 |
} |
| 160 | 160 |
|
| 161 |
- ret.Username = resp.Username |
|
| 162 |
- ret.Password = resp.Password |
|
| 161 |
+ if resp.Username == tokenUsername {
|
|
| 162 |
+ ret.IdentityToken = resp.Secret |
|
| 163 |
+ } else {
|
|
| 164 |
+ ret.Password = resp.Secret |
|
| 165 |
+ ret.Username = resp.Username |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 163 | 168 |
ret.ServerAddress = serverAddress |
| 164 | 169 |
return ret, nil |
| 165 | 170 |
} |
| 166 | 171 |
|
| 167 |
-// eraseCredentialsFromStore executes the command to remove the server redentails from the native store. |
|
| 172 |
+// eraseCredentialsFromStore executes the command to remove the server credentails from the native store. |
|
| 168 | 173 |
func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
|
| 169 | 174 |
cmd := c.commandFn("erase")
|
| 170 | 175 |
cmd.Input(strings.NewReader(serverURL)) |
| ... | ... |
@@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) {
|
| 47 | 47 |
} |
| 48 | 48 |
case "get": |
| 49 | 49 |
switch inS {
|
| 50 |
- case validServerAddress, validServerAddress2: |
|
| 51 |
- return []byte(`{"Username": "foo", "Password": "bar"}`), nil
|
|
| 50 |
+ case validServerAddress: |
|
| 51 |
+ return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
|
| 52 |
+ case validServerAddress2: |
|
| 53 |
+ return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
|
| 52 | 54 |
case missingCredsAddress: |
| 53 | 55 |
return []byte(errCredentialsNotFound.Error()), errCommandExited |
| 54 | 56 |
case invalidServerAddress: |
| ... | ... |
@@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) {
|
| 118 | 118 |
if a.Password != "" {
|
| 119 | 119 |
t.Fatalf("expected password to be empty, got %s", a.Password)
|
| 120 | 120 |
} |
| 121 |
+ if a.IdentityToken != "" {
|
|
| 122 |
+ t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
|
|
| 123 |
+ } |
|
| 121 | 124 |
if a.Email != "foo@example.com" {
|
| 122 | 125 |
t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
|
| 123 | 126 |
} |
| ... | ... |
@@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) {
|
| 174 | 174 |
if a.Password != "bar" {
|
| 175 | 175 |
t.Fatalf("expected password `bar`, got %s", a.Password)
|
| 176 | 176 |
} |
| 177 |
+ if a.IdentityToken != "" {
|
|
| 178 |
+ t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
|
|
| 179 |
+ } |
|
| 177 | 180 |
if a.Email != "foo@example.com" {
|
| 178 | 181 |
t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
|
| 179 | 182 |
} |
| 180 | 183 |
} |
| 181 | 184 |
|
| 185 |
+func TestNativeStoreGetIdentityToken(t *testing.T) {
|
|
| 186 |
+ f := newConfigFile(map[string]types.AuthConfig{
|
|
| 187 |
+ validServerAddress2: {
|
|
| 188 |
+ Email: "foo@example2.com", |
|
| 189 |
+ }, |
|
| 190 |
+ }) |
|
| 191 |
+ f.CredentialsStore = "mock" |
|
| 192 |
+ |
|
| 193 |
+ s := &nativeStore{
|
|
| 194 |
+ commandFn: mockCommandFn, |
|
| 195 |
+ fileStore: NewFileStore(f), |
|
| 196 |
+ } |
|
| 197 |
+ a, err := s.Get(validServerAddress2) |
|
| 198 |
+ if err != nil {
|
|
| 199 |
+ t.Fatal(err) |
|
| 200 |
+ } |
|
| 201 |
+ |
|
| 202 |
+ if a.Username != "" {
|
|
| 203 |
+ t.Fatalf("expected username to be empty, got %s", a.Username)
|
|
| 204 |
+ } |
|
| 205 |
+ if a.Password != "" {
|
|
| 206 |
+ t.Fatalf("expected password to be empty, got %s", a.Password)
|
|
| 207 |
+ } |
|
| 208 |
+ if a.IdentityToken != "abcd1234" {
|
|
| 209 |
+ t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
|
|
| 210 |
+ } |
|
| 211 |
+ if a.Email != "foo@example2.com" {
|
|
| 212 |
+ t.Fatalf("expected email `foo@example2.com`, got %s", a.Email)
|
|
| 213 |
+ } |
|
| 214 |
+} |
|
| 215 |
+ |
|
| 182 | 216 |
func TestNativeStoreGetAll(t *testing.T) {
|
| 183 | 217 |
f := newConfigFile(map[string]types.AuthConfig{
|
| 184 | 218 |
validServerAddress: {
|
| ... | ... |
@@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) {
|
| 209 | 209 |
if as[validServerAddress].Password != "bar" {
|
| 210 | 210 |
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
|
| 211 | 211 |
} |
| 212 |
+ if as[validServerAddress].IdentityToken != "" {
|
|
| 213 |
+ t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
|
|
| 214 |
+ } |
|
| 212 | 215 |
if as[validServerAddress].Email != "foo@example.com" {
|
| 213 | 216 |
t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
|
| 214 | 217 |
} |
| 215 |
- if as[validServerAddress2].Username != "foo" {
|
|
| 216 |
- t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
|
|
| 218 |
+ if as[validServerAddress2].Username != "" {
|
|
| 219 |
+ t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
|
|
| 220 |
+ } |
|
| 221 |
+ if as[validServerAddress2].Password != "" {
|
|
| 222 |
+ t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
|
|
| 217 | 223 |
} |
| 218 |
- if as[validServerAddress2].Password != "bar" {
|
|
| 219 |
- t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
|
|
| 224 |
+ if as[validServerAddress2].IdentityToken != "abcd1234" {
|
|
| 225 |
+ t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
|
|
| 220 | 226 |
} |
| 221 | 227 |
if as[validServerAddress2].Email != "foo@example2.com" {
|
| 222 | 228 |
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. |
| 78 | 78 |
There are only three possible values for that argument: `store`, `get`, and `erase`. |
| 79 | 79 |
|
| 80 | 80 |
The `store` command takes a JSON payload from the standard input. That payload carries |
| 81 |
-the server address, to identify the credential, the user name and the password. |
|
| 82 |
-This is an example of that payload: |
|
| 81 |
+the server address, to identify the credential, the user name, and either a password |
|
| 82 |
+or an identity token. |
|
| 83 | 83 |
|
| 84 | 84 |
```json |
| 85 | 85 |
{
|
| 86 | 86 |
"ServerURL": "https://index.docker.io/v1", |
| 87 | 87 |
"Username": "david", |
| 88 |
- "Password": "passw0rd1" |
|
| 88 |
+ "Secret": "passw0rd1" |
|
| 89 | 89 |
} |
| 90 | 90 |
``` |
| 91 | 91 |
|
| 92 |
+If the secret being stored is an identity token, the Username should be set to |
|
| 93 |
+`<token>`. |
|
| 94 |
+ |
|
| 92 | 95 |
The `store` command can write error messages to `STDOUT` that the docker engine |
| 93 | 96 |
will show if there was an issue. |
| 94 | 97 |
|
| ... | ... |
@@ -102,7 +105,7 @@ and password from this payload: |
| 102 | 102 |
```json |
| 103 | 103 |
{
|
| 104 | 104 |
"Username": "david", |
| 105 |
- "Password": "passw0rd1" |
|
| 105 |
+ "Secret": "passw0rd1" |
|
| 106 | 106 |
} |
| 107 | 107 |
``` |
| 108 | 108 |
|
| ... | ... |
@@ -8,8 +8,8 @@ case $1 in |
| 8 | 8 |
server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}')
|
| 9 | 9 |
|
| 10 | 10 |
username=$(echo "$in" | jq --raw-output ".Username") |
| 11 |
- password=$(echo "$in" | jq --raw-output ".Password") |
|
| 12 |
- echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server
|
|
| 11 |
+ password=$(echo "$in" | jq --raw-output ".Secret") |
|
| 12 |
+ echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server
|
|
| 13 | 13 |
;; |
| 14 | 14 |
"get") |
| 15 | 15 |
in=$(</dev/stdin) |