...

Source file src/github.com/google/go-containerregistry/pkg/authn/keychain_test.go

Documentation: github.com/google/go-containerregistry/pkg/authn

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    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  	// Set $HOME to a temp empty dir, to ensure $HOME/.docker/config.json
    41  	// isn't unexpectedly found.
    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  // setupConfigDir sets up an isolated configDir() for this test.
    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  	// return the config dir so we can clean up
    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  	// At first, $DOCKER_CONFIG is unset and $HOME/.docker/config.json isn't
   114  	// found, but Podman auth is configured. This should return Podman's
   115  	// auth.
   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  	// Now, configure $HOME/.docker/config.json, which should override
   133  	// Podman auth and be used.
   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  	// Then, configure DOCKER_CONFIG with a valid config file with different
   160  	// auth configured.
   161  	// This demonstrates that DOCKER_CONFIG is preferred over Podman auth
   162  	// and $HOME/.docker/config.json.
   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  			// For some reason, these tempdirs don't get cleaned up.
   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  					// success
   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  	// Create "config.json" as a directory, not a file to simulate optional
   380  	// secrets in Kubernetes.
   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  	// Increments by 1 minute each invocation.
   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