Skip to content

Commit 1fbc65c

Browse files
committed
fix: ensure docker config auth deserialized to username/password
1 parent 1ac9749 commit 1fbc65c

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

provider/docker_config.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package provider
22

3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
)
9+
310
// DockerConfigJSON represents ~/.docker/config.json file info
411
// see https://github.com/docker/docker/pull/12009
512
type DockerConfigJSON struct {
@@ -19,3 +26,82 @@ type DockerConfigEntry struct {
1926
Password string
2027
Email string
2128
}
29+
30+
// dockerConfigEntryWithAuth is used solely for deserializing the Auth field
31+
// into a dockerConfigEntry during JSON deserialization.
32+
type dockerConfigEntryWithAuth struct {
33+
// +optional
34+
Username string `json:"username,omitempty"`
35+
// +optional
36+
Password string `json:"password,omitempty"`
37+
// +optional
38+
Email string `json:"email,omitempty"`
39+
// +optional
40+
Auth string `json:"auth,omitempty"`
41+
}
42+
43+
// UnmarshalJSON implements the json.Unmarshaler interface.
44+
func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
45+
var tmp dockerConfigEntryWithAuth
46+
err := json.Unmarshal(data, &tmp)
47+
if err != nil {
48+
return err
49+
}
50+
51+
ident.Username = tmp.Username
52+
ident.Password = tmp.Password
53+
ident.Email = tmp.Email
54+
55+
if len(tmp.Auth) == 0 {
56+
return nil
57+
}
58+
59+
ident.Username, ident.Password, err = decodeDockerConfigFieldAuth(tmp.Auth)
60+
return err
61+
}
62+
63+
// MarshalJSON implements the json.Marshaler interface.
64+
func (ident DockerConfigEntry) MarshalJSON() ([]byte, error) {
65+
toEncode := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""}
66+
toEncode.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password)
67+
68+
return json.Marshal(toEncode)
69+
}
70+
71+
// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a
72+
// username and a password. The format of the auth field is base64(<username>:<password>).
73+
func decodeDockerConfigFieldAuth(field string) (username, password string, err error) {
74+
75+
var decoded []byte
76+
77+
// StdEncoding can only decode padded string
78+
// RawStdEncoding can only decode unpadded string
79+
if strings.HasSuffix(strings.TrimSpace(field), "=") {
80+
// decode padded data
81+
decoded, err = base64.StdEncoding.DecodeString(field)
82+
} else {
83+
// decode unpadded data
84+
decoded, err = base64.RawStdEncoding.DecodeString(field)
85+
}
86+
87+
if err != nil {
88+
return
89+
}
90+
91+
parts := strings.SplitN(string(decoded), ":", 2)
92+
if len(parts) != 2 {
93+
err = fmt.Errorf("unable to parse auth field, must be formatted as base64(username:password)")
94+
return
95+
}
96+
97+
username = parts[0]
98+
password = parts[1]
99+
100+
return
101+
}
102+
103+
func encodeDockerConfigFieldAuth(username, password string) string {
104+
fieldValue := username + ":" + password
105+
106+
return base64.StdEncoding.EncodeToString([]byte(fieldValue))
107+
}

provider/docker_config_test.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package provider
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"reflect"
7+
"testing"
8+
)
9+
10+
func TestDockerConfigJsonJSONDecode(t *testing.T) {
11+
// Fake values for testing.
12+
input := []byte(`{"auths": {"http://foo.example.com":{"username": "foo", "password": "bar", "email": "[email protected]"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "[email protected]"}}}`)
13+
14+
expect := DockerConfigJSON{
15+
Auths: DockerConfig(map[string]DockerConfigEntry{
16+
"http://foo.example.com": {
17+
Username: "foo",
18+
Password: "bar",
19+
20+
},
21+
"http://bar.example.com": {
22+
Username: "bar",
23+
Password: "baz",
24+
25+
},
26+
}),
27+
}
28+
29+
var output DockerConfigJSON
30+
err := json.Unmarshal(input, &output)
31+
if err != nil {
32+
t.Errorf("Received unexpected error: %v", err)
33+
}
34+
35+
if !reflect.DeepEqual(expect, output) {
36+
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
37+
}
38+
}
39+
40+
func TestDockerConfigJSONDecode(t *testing.T) {
41+
// Fake values for testing.
42+
input := []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "[email protected]"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "[email protected]"}}`)
43+
44+
expect := DockerConfig(map[string]DockerConfigEntry{
45+
"http://foo.example.com": {
46+
Username: "foo",
47+
Password: "bar",
48+
49+
},
50+
"http://bar.example.com": {
51+
Username: "bar",
52+
Password: "baz",
53+
54+
},
55+
})
56+
57+
var output DockerConfig
58+
err := json.Unmarshal(input, &output)
59+
if err != nil {
60+
t.Errorf("Received unexpected error: %v", err)
61+
}
62+
63+
if !reflect.DeepEqual(expect, output) {
64+
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
65+
}
66+
}
67+
68+
func TestDockerConfigEntryJSONDecode(t *testing.T) {
69+
tests := []struct {
70+
input []byte
71+
expect DockerConfigEntry
72+
fail bool
73+
}{
74+
// simple case, just decode the fields
75+
{
76+
// Fake values for testing.
77+
input: []byte(`{"username": "foo", "password": "bar", "email": "[email protected]"}`),
78+
expect: DockerConfigEntry{
79+
Username: "foo",
80+
Password: "bar",
81+
82+
},
83+
fail: false,
84+
},
85+
86+
// auth field decodes to username & password
87+
{
88+
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "[email protected]"}`),
89+
expect: DockerConfigEntry{
90+
Username: "foo",
91+
Password: "bar",
92+
93+
},
94+
fail: false,
95+
},
96+
97+
// auth field overrides username & password
98+
{
99+
// Fake values for testing.
100+
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "[email protected]"}`),
101+
expect: DockerConfigEntry{
102+
Username: "ping",
103+
Password: "pong",
104+
105+
},
106+
fail: false,
107+
},
108+
109+
// poorly-formatted auth causes failure
110+
{
111+
input: []byte(`{"auth": "pants", "email": "[email protected]"}`),
112+
expect: DockerConfigEntry{
113+
Username: "",
114+
Password: "",
115+
116+
},
117+
fail: true,
118+
},
119+
120+
// invalid JSON causes failure
121+
{
122+
input: []byte(`{"email": false}`),
123+
expect: DockerConfigEntry{
124+
Username: "",
125+
Password: "",
126+
Email: "",
127+
},
128+
fail: true,
129+
},
130+
}
131+
132+
for i, tt := range tests {
133+
var output DockerConfigEntry
134+
err := json.Unmarshal(tt.input, &output)
135+
if (err != nil) != tt.fail {
136+
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
137+
}
138+
139+
if !reflect.DeepEqual(tt.expect, output) {
140+
t.Errorf("case %d: expected output %#v, got %#v", i, tt.expect, output)
141+
}
142+
}
143+
}
144+
145+
func TestDecodeDockerConfigFieldAuth(t *testing.T) {
146+
tests := []struct {
147+
input string
148+
username string
149+
password string
150+
fail bool
151+
}{
152+
// auth field decodes to username & password
153+
{
154+
input: "Zm9vOmJhcg==",
155+
username: "foo",
156+
password: "bar",
157+
},
158+
159+
// some test as before but with field not well padded
160+
{
161+
input: "Zm9vOmJhcg",
162+
username: "foo",
163+
password: "bar",
164+
},
165+
166+
// some test as before but with new line characters
167+
{
168+
input: "Zm9vOm\nJhcg==\n",
169+
username: "foo",
170+
password: "bar",
171+
},
172+
173+
// standard encoding (with padding)
174+
{
175+
input: base64.StdEncoding.EncodeToString([]byte("foo:bar")),
176+
username: "foo",
177+
password: "bar",
178+
},
179+
180+
// raw encoding (without padding)
181+
{
182+
input: base64.RawStdEncoding.EncodeToString([]byte("foo:bar")),
183+
username: "foo",
184+
password: "bar",
185+
},
186+
187+
// the input is encoded with encodeDockerConfigFieldAuth (standard encoding)
188+
{
189+
input: encodeDockerConfigFieldAuth("foo", "bar"),
190+
username: "foo",
191+
password: "bar",
192+
},
193+
194+
// good base64 data, but no colon separating username & password
195+
{
196+
input: "cGFudHM=",
197+
fail: true,
198+
},
199+
200+
// only new line characters are ignored
201+
{
202+
input: "Zm9vOmJhcg== ",
203+
fail: true,
204+
},
205+
206+
// bad base64 data
207+
{
208+
input: "pants",
209+
fail: true,
210+
},
211+
}
212+
213+
for i, tt := range tests {
214+
username, password, err := decodeDockerConfigFieldAuth(tt.input)
215+
if (err != nil) != tt.fail {
216+
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
217+
}
218+
219+
if tt.username != username {
220+
t.Errorf("case %d: expected username %q, got %q", i, tt.username, username)
221+
}
222+
223+
if tt.password != password {
224+
t.Errorf("case %d: expected password %q, got %q", i, tt.password, password)
225+
}
226+
}
227+
}
228+
229+
func TestDockerConfigEntryJSONCompatibleEncode(t *testing.T) {
230+
tests := []struct {
231+
input DockerConfigEntry
232+
expect []byte
233+
}{
234+
// simple case, just decode the fields
235+
{
236+
// Fake values for testing.
237+
expect: []byte(`{"username":"foo","password":"bar","email":"[email protected]","auth":"Zm9vOmJhcg=="}`),
238+
input: DockerConfigEntry{
239+
Username: "foo",
240+
Password: "bar",
241+
242+
},
243+
},
244+
}
245+
246+
for i, tt := range tests {
247+
actual, err := json.Marshal(tt.input)
248+
if err != nil {
249+
t.Errorf("case %d: unexpected error: %v", i, err)
250+
}
251+
252+
if string(tt.expect) != string(actual) {
253+
t.Errorf("case %d: expected %v, got %v", i, string(tt.expect), string(actual))
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)