1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package authn
16
17 import (
18 "encoding/base64"
19 "errors"
20 "fmt"
21 "log"
22 "os"
23 "path"
24 "path/filepath"
25 "reflect"
26 "testing"
27 "time"
28
29 "github.com/google/go-containerregistry/pkg/name"
30 )
31
32 var (
33 fresh = 0
34 testRegistry, _ = name.NewRegistry("test.io", name.WeakValidation)
35 testRepo, _ = name.NewRepository("test.io/my-repo", name.WeakValidation)
36 defaultRegistry, _ = name.NewRegistry(name.DefaultRegistry, name.WeakValidation)
37 )
38
39 func TestMain(m *testing.M) {
40
41
42 tmp, err := os.MkdirTemp("", "keychain_test_home")
43 if err != nil {
44 log.Fatal(err)
45 }
46 os.Setenv("HOME", tmp)
47 os.Exit(func() int {
48 defer os.RemoveAll(tmp)
49 return m.Run()
50 }())
51 }
52
53
54 func setupConfigDir(t *testing.T) string {
55 tmpdir := os.Getenv("TEST_TMPDIR")
56 if tmpdir == "" {
57 tmpdir = t.TempDir()
58 }
59
60 fresh++
61 p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
62 t.Logf("DOCKER_CONFIG=%s", p)
63 t.Setenv("DOCKER_CONFIG", p)
64 if err := os.Mkdir(p, 0777); err != nil {
65 t.Fatalf("mkdir %q: %v", p, err)
66 }
67 return p
68 }
69
70 func setupConfigFile(t *testing.T, content string) string {
71 cd := setupConfigDir(t)
72 p := filepath.Join(cd, "config.json")
73 if err := os.WriteFile(p, []byte(content), 0600); err != nil {
74 t.Fatalf("write %q: %v", p, err)
75 }
76
77
78 return cd
79 }
80
81 func TestNoConfig(t *testing.T) {
82 cd := setupConfigDir(t)
83 defer os.RemoveAll(filepath.Dir(cd))
84
85 auth, err := DefaultKeychain.Resolve(testRegistry)
86 if err != nil {
87 t.Fatalf("Resolve() = %v", err)
88 }
89
90 if auth != Anonymous {
91 t.Errorf("expected Anonymous, got %v", auth)
92 }
93 }
94
95 func TestPodmanConfig(t *testing.T) {
96 tmpdir := os.Getenv("TEST_TMPDIR")
97 if tmpdir == "" {
98 tmpdir = t.TempDir()
99 }
100 fresh++
101 p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
102 t.Setenv("XDG_RUNTIME_DIR", p)
103 os.Unsetenv("DOCKER_CONFIG")
104 if err := os.MkdirAll(filepath.Join(p, "containers"), 0777); err != nil {
105 t.Fatalf("mkdir %s/containers: %v", p, err)
106 }
107 cfg := filepath.Join(p, "containers/auth.json")
108 content := fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar"))
109 if err := os.WriteFile(cfg, []byte(content), 0600); err != nil {
110 t.Fatalf("write %q: %v", cfg, err)
111 }
112
113
114
115
116 auth, err := DefaultKeychain.Resolve(testRegistry)
117 if err != nil {
118 t.Fatalf("Resolve() = %v", err)
119 }
120 got, err := auth.Authorization()
121 if err != nil {
122 t.Fatal(err)
123 }
124 want := &AuthConfig{
125 Username: "foo",
126 Password: "bar",
127 }
128 if !reflect.DeepEqual(got, want) {
129 t.Errorf("got %+v, want %+v", got, want)
130 }
131
132
133
134 if err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".docker"), 0777); err != nil {
135 t.Fatalf("mkdir $HOME/.docker: %v", err)
136 }
137 cfg = filepath.Join(os.Getenv("HOME"), ".docker/config.json")
138 content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("home-foo", "home-bar"))
139 if err := os.WriteFile(cfg, []byte(content), 0600); err != nil {
140 t.Fatalf("write %q: %v", cfg, err)
141 }
142 defer func() { os.Remove(cfg) }()
143 auth, err = DefaultKeychain.Resolve(testRegistry)
144 if err != nil {
145 t.Fatalf("Resolve() = %v", err)
146 }
147 got, err = auth.Authorization()
148 if err != nil {
149 t.Fatal(err)
150 }
151 want = &AuthConfig{
152 Username: "home-foo",
153 Password: "home-bar",
154 }
155 if !reflect.DeepEqual(got, want) {
156 t.Errorf("got %+v, want %+v", got, want)
157 }
158
159
160
161
162
163 content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("another-foo", "another-bar"))
164 cd := setupConfigFile(t, content)
165 defer os.RemoveAll(filepath.Dir(cd))
166
167 auth, err = DefaultKeychain.Resolve(testRegistry)
168 if err != nil {
169 t.Fatalf("Resolve() = %v", err)
170 }
171 got, err = auth.Authorization()
172 if err != nil {
173 t.Fatal(err)
174 }
175 want = &AuthConfig{
176 Username: "another-foo",
177 Password: "another-bar",
178 }
179 if !reflect.DeepEqual(got, want) {
180 t.Errorf("got %+v, want %+v", got, want)
181 }
182 }
183
184 func encode(user, pass string) string {
185 delimited := fmt.Sprintf("%s:%s", user, pass)
186 return base64.StdEncoding.EncodeToString([]byte(delimited))
187 }
188
189 func TestVariousPaths(t *testing.T) {
190 tests := []struct {
191 desc string
192 content string
193 wantErr bool
194 target Resource
195 cfg *AuthConfig
196 anonymous bool
197 }{{
198 desc: "invalid config file",
199 target: testRegistry,
200 content: `}{`,
201 wantErr: true,
202 }, {
203 desc: "creds store does not exist",
204 target: testRegistry,
205 content: `{"credsStore":"#definitely-does-not-exist"}`,
206 wantErr: true,
207 }, {
208 desc: "valid config file",
209 target: testRegistry,
210 content: fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")),
211 cfg: &AuthConfig{
212 Username: "foo",
213 Password: "bar",
214 },
215 }, {
216 desc: "valid config file; default registry",
217 target: defaultRegistry,
218 content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, DefaultAuthKey, encode("foo", "bar")),
219 cfg: &AuthConfig{
220 Username: "foo",
221 Password: "bar",
222 },
223 }, {
224 desc: "valid config file; matches registry w/ v1",
225 target: testRegistry,
226 content: fmt.Sprintf(`{
227 "auths": {
228 "http://test.io/v1/": {"auth": %q}
229 }
230 }`, encode("baz", "quux")),
231 cfg: &AuthConfig{
232 Username: "baz",
233 Password: "quux",
234 },
235 }, {
236 desc: "valid config file; matches registry w/ v2",
237 target: testRegistry,
238 content: fmt.Sprintf(`{
239 "auths": {
240 "http://test.io/v2/": {"auth": %q}
241 }
242 }`, encode("baz", "quux")),
243 cfg: &AuthConfig{
244 Username: "baz",
245 Password: "quux",
246 },
247 }, {
248 desc: "valid config file; matches repo",
249 target: testRepo,
250 content: fmt.Sprintf(`{
251 "auths": {
252 "test.io/my-repo": {"auth": %q},
253 "test.io/another-repo": {"auth": %q},
254 "test.io": {"auth": %q}
255 }
256 }`, encode("foo", "bar"), encode("bar", "baz"), encode("baz", "quux")),
257 cfg: &AuthConfig{
258 Username: "foo",
259 Password: "bar",
260 },
261 }, {
262 desc: "ignore unrelated repo",
263 target: testRepo,
264 content: fmt.Sprintf(`{
265 "auths": {
266 "test.io/another-repo": {"auth": %q},
267 "test.io": {}
268 }
269 }`, encode("bar", "baz")),
270 cfg: &AuthConfig{},
271 anonymous: true,
272 }}
273
274 for _, test := range tests {
275 t.Run(test.desc, func(t *testing.T) {
276 cd := setupConfigFile(t, test.content)
277
278 defer os.RemoveAll(filepath.Dir(cd))
279
280 auth, err := DefaultKeychain.Resolve(test.target)
281 if test.wantErr {
282 if err == nil {
283 t.Fatal("wanted err, got nil")
284 } else if err != nil {
285
286 return
287 }
288 }
289 if err != nil {
290 t.Fatalf("wanted nil, got err: %v", err)
291 }
292 cfg, err := auth.Authorization()
293 if err != nil {
294 t.Fatal(err)
295 }
296
297 if !reflect.DeepEqual(cfg, test.cfg) {
298 t.Errorf("got %+v, want %+v", cfg, test.cfg)
299 }
300
301 if test.anonymous != (auth == Anonymous) {
302 t.Fatalf("unexpected anonymous authenticator")
303 }
304 })
305 }
306 }
307
308 type helper struct {
309 u, p string
310 err error
311 }
312
313 func (h helper) Get(serverURL string) (string, string, error) {
314 if serverURL != "example.com" {
315 return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL)
316 }
317 return h.u, h.p, h.err
318 }
319
320 func TestNewKeychainFromHelper(t *testing.T) {
321 var repo = name.MustParseReference("example.com/my/repo").Context()
322
323 t.Run("success", func(t *testing.T) {
324 kc := NewKeychainFromHelper(helper{"username", "password", nil})
325 auth, err := kc.Resolve(repo)
326 if err != nil {
327 t.Fatalf("Resolve(%q): %v", repo, err)
328 }
329 cfg, err := auth.Authorization()
330 if err != nil {
331 t.Fatalf("Authorization: %v", err)
332 }
333 if got, want := cfg.Username, "username"; got != want {
334 t.Errorf("Username: got %q, want %q", got, want)
335 }
336 if got, want := cfg.IdentityToken, ""; got != want {
337 t.Errorf("IdentityToken: got %q, want %q", got, want)
338 }
339 if got, want := cfg.Password, "password"; got != want {
340 t.Errorf("Password: got %q, want %q", got, want)
341 }
342 })
343
344 t.Run("success; identity token", func(t *testing.T) {
345 kc := NewKeychainFromHelper(helper{"<token>", "idtoken", nil})
346 auth, err := kc.Resolve(repo)
347 if err != nil {
348 t.Fatalf("Resolve(%q): %v", repo, err)
349 }
350 cfg, err := auth.Authorization()
351 if err != nil {
352 t.Fatalf("Authorization: %v", err)
353 }
354 if got, want := cfg.Username, "<token>"; got != want {
355 t.Errorf("Username: got %q, want %q", got, want)
356 }
357 if got, want := cfg.IdentityToken, "idtoken"; got != want {
358 t.Errorf("IdentityToken: got %q, want %q", got, want)
359 }
360 if got, want := cfg.Password, ""; got != want {
361 t.Errorf("Password: got %q, want %q", got, want)
362 }
363 })
364
365 t.Run("failure", func(t *testing.T) {
366 kc := NewKeychainFromHelper(helper{"", "", errors.New("oh no bad")})
367 auth, err := kc.Resolve(repo)
368 if err != nil {
369 t.Fatalf("Resolve(%q): %v", repo, err)
370 }
371 if auth != Anonymous {
372 t.Errorf("Resolve: got %v, want %v", auth, Anonymous)
373 }
374 })
375 }
376
377 func TestConfigFileIsADir(t *testing.T) {
378 tmpdir := setupConfigDir(t)
379
380
381 err := os.Mkdir(path.Join(tmpdir, "config.json"), 0777)
382 if err != nil {
383 t.Fatal(err)
384 }
385
386 auth, err := DefaultKeychain.Resolve(testRegistry)
387 if err != nil {
388 t.Fatalf("Resolve() = %v", err)
389 }
390 if auth != Anonymous {
391 t.Errorf("expected Anonymous, got %v", auth)
392 }
393 }
394
395 type fakeKeychain struct {
396 auth Authenticator
397 err error
398
399 count int
400 }
401
402 func (k *fakeKeychain) Resolve(_ Resource) (Authenticator, error) {
403 k.count++
404 return k.auth, k.err
405 }
406
407 func TestRefreshingAuth(t *testing.T) {
408 repo := name.MustParseReference("example.com/my/repo").Context()
409 last := time.Now()
410
411
412 clock := func() time.Time {
413 last = last.Add(1 * time.Minute)
414 return last
415 }
416
417 want := AuthConfig{
418 Username: "foo",
419 Password: "secret",
420 }
421
422 keychain := &fakeKeychain{FromConfig(want), nil, 0}
423 rk := RefreshingKeychain(keychain, 5*time.Minute)
424 rk.(*refreshingKeychain).clock = clock
425
426 auth, err := rk.Resolve(repo)
427 if err != nil {
428 t.Fatal(err)
429 }
430
431 for i := 0; i < 10; i++ {
432 got, err := auth.Authorization()
433 if err != nil {
434 t.Fatal(err)
435 }
436
437 if *got != want {
438 t.Errorf("got %+v, want %+v", got, want)
439 }
440 }
441
442 if got, want := keychain.count, 2; got != want {
443 t.Errorf("refreshed %d times, wanted %d", got, want)
444 }
445 }
446
View as plain text