Browse code

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>

Aaron Lehmann authored on 2016/03/05 05:00:18
Showing 5 changed files
... ...
@@ -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)