cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociauth/authfile_test.go (about)

     1  package ociauth
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  	"testing"
    11  
    12  	"github.com/go-quicktest/qt"
    13  	"github.com/rogpeppe/go-internal/testscript"
    14  )
    15  
    16  func TestMain(m *testing.M) {
    17  	// We're using testscript, not for txtar tests,
    18  	// but to access the test executable functionality.
    19  	os.Exit(testscript.RunMain(m, map[string]func() int{
    20  		"docker-credential-test": helperMain,
    21  	}))
    22  }
    23  
    24  func TestLoadWithNoConfig(t *testing.T) {
    25  	qt.Patch(t, &userHomeDir, func(getenv func(string) string) string {
    26  		return getenv("HOME")
    27  	})
    28  	t.Setenv("HOME", "")
    29  	t.Setenv("DOCKER_CONFIG", "")
    30  	t.Setenv("XDG_RUNTIME_DIR", "")
    31  	c, err := Load(noRunner)
    32  	qt.Assert(t, qt.IsNil(err))
    33  	info, err := c.EntryForRegistry("some.org")
    34  	qt.Assert(t, qt.IsNil(err))
    35  	qt.Assert(t, qt.Equals(info, ConfigEntry{}))
    36  }
    37  
    38  func TestLoad(t *testing.T) {
    39  	// Write config files in all the places, so we can check
    40  	// that the precedence works OK.
    41  	d := t.TempDir()
    42  	qt.Patch(t, &userHomeDir, func(getenv func(string) string) string {
    43  		return getenv("HOME")
    44  	})
    45  	locations := []struct {
    46  		env  string
    47  		dir  string
    48  		file string
    49  	}{{
    50  		env:  "DOCKER_CONFIG",
    51  		dir:  "dockerconfig",
    52  		file: "config.json",
    53  	}, {
    54  		env:  "HOME",
    55  		dir:  "home",
    56  		file: ".docker/config.json",
    57  	}, {
    58  		env:  "XDG_RUNTIME_DIR",
    59  		dir:  "xdg",
    60  		file: "containers/auth.json",
    61  	}}
    62  	for _, loc := range locations {
    63  		epath := filepath.Join(d, loc.dir)
    64  		t.Setenv(loc.env, epath)
    65  		cfgPath := filepath.Join(epath, filepath.FromSlash(loc.file))
    66  		err := os.MkdirAll(filepath.Dir(cfgPath), 0o777)
    67  		qt.Assert(t, qt.IsNil(err))
    68  		// Write the config file with a username that
    69  		// reflects where it's stored.
    70  		err = os.WriteFile(cfgPath, []byte(`
    71  {
    72  	"auths": {
    73  		"someregistry.example.com": {
    74  			"username": `+fmt.Sprintf("%q", loc.env)+`,
    75  			"password": "somepassword"
    76  		}
    77  	}
    78  }`), 0o666)
    79  		qt.Assert(t, qt.IsNil(err))
    80  	}
    81  	for _, loc := range locations {
    82  		t.Run(loc.env, func(t *testing.T) {
    83  			c, err := Load(noRunner)
    84  			qt.Assert(t, qt.IsNil(err))
    85  			info, err := c.EntryForRegistry("someregistry.example.com")
    86  			qt.Assert(t, qt.IsNil(err))
    87  			qt.Assert(t, qt.Equals(info, ConfigEntry{
    88  				Username: loc.env,
    89  				Password: "somepassword",
    90  			}))
    91  			// Remove the directory containing the above
    92  			// config file so that the next level of precedence
    93  			// can be checked.
    94  			err = os.RemoveAll(filepath.Join(d, loc.dir))
    95  			qt.Assert(t, qt.IsNil(err))
    96  		})
    97  	}
    98  	// When there's no config file available, it should return
    99  	// an empty configuration and no error.
   100  	c, err := Load(noRunner)
   101  	qt.Assert(t, qt.IsNil(err))
   102  
   103  	info, err := c.EntryForRegistry("someregistry.example.com")
   104  	qt.Assert(t, qt.IsNil(err))
   105  	qt.Assert(t, qt.Equals(info, ConfigEntry{}))
   106  }
   107  
   108  func TestWithBase64Auth(t *testing.T) {
   109  	c, err := load(t, noRunner, `
   110  {
   111  	"auths": {
   112  		"someregistry.example.com": {
   113  			"auth": "dGVzdHVzZXI6cGFzc3dvcmQ="
   114  		}
   115  	}
   116  }`)
   117  	qt.Assert(t, qt.IsNil(err))
   118  	info, err := c.EntryForRegistry("someregistry.example.com")
   119  	qt.Assert(t, qt.IsNil(err))
   120  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   121  		Username: "testuser",
   122  		Password: "password",
   123  	}))
   124  }
   125  
   126  func TestWithMalformedBase64Auth(t *testing.T) {
   127  	_, err := load(t, noRunner, `
   128  {
   129  	"auths": {
   130  		"someregistry.example.com": {
   131  			"auth": "!!!"
   132  		}
   133  	}
   134  }`)
   135  	qt.Assert(t, qt.ErrorMatches(err, `invalid config file ".*": cannot decode auth field for "someregistry.example.com": invalid base64-encoded string`))
   136  }
   137  
   138  func TestWithAuthAndUsername(t *testing.T) {
   139  	// An auth field overrides the username/password pair.
   140  	c, err := load(t, noRunner, `
   141  {
   142  	"auths": {
   143  		"someregistry.example.com": {
   144  			"auth": "dGVzdHVzZXI6cGFzc3dvcmQ=",
   145  			"username": "foo",
   146  			"password": "bar"
   147  		}
   148  	}
   149  }`)
   150  	qt.Assert(t, qt.IsNil(err))
   151  	info, err := c.EntryForRegistry("someregistry.example.com")
   152  	qt.Assert(t, qt.IsNil(err))
   153  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   154  		Username: "testuser",
   155  		Password: "password",
   156  	}))
   157  }
   158  
   159  func TestWithURLEntry(t *testing.T) {
   160  	c, err := load(t, noRunner, `
   161  {
   162  	"auths": {
   163  		"https://someregistry.example.com/v1": {
   164  			"username": "foo",
   165  			"password": "bar"
   166  		}
   167  	}
   168  }`)
   169  	qt.Assert(t, qt.IsNil(err))
   170  	info, err := c.EntryForRegistry("someregistry.example.com")
   171  	qt.Assert(t, qt.IsNil(err))
   172  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   173  		Username: "foo",
   174  		Password: "bar",
   175  	}))
   176  }
   177  
   178  func TestWithURLEntryAndExplicitHost(t *testing.T) {
   179  	c, err := load(t, noRunner, `
   180  {
   181  	"auths": {
   182  		"https://someregistry.example.com/v1": {
   183  			"username": "foo",
   184  			"password": "bar"
   185  		},
   186  		"someregistry.example.com": {
   187  			"username": "baz",
   188  			"password": "arble"
   189  		}
   190  	}
   191  }`)
   192  	qt.Assert(t, qt.IsNil(err))
   193  	info, err := c.EntryForRegistry("someregistry.example.com")
   194  	qt.Assert(t, qt.IsNil(err))
   195  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   196  		Username: "baz",
   197  		Password: "arble",
   198  	}))
   199  	info, err = c.EntryForRegistry("https://someregistry.example.com/v1")
   200  	qt.Assert(t, qt.IsNil(err))
   201  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   202  		Username: "foo",
   203  		Password: "bar",
   204  	}))
   205  }
   206  
   207  func TestWithMultipleURLsAndSameHost(t *testing.T) {
   208  	c, err := load(t, noRunner, `
   209  {
   210  	"auths": {
   211  		"https://someregistry.example.com/v1": {
   212  			"username": "u1",
   213  			"password": "p"
   214  		},
   215  		"http://someregistry.example.com/v1": {
   216  			"username": "u2",
   217  			"password": "p"
   218  		},
   219  		"http://someregistry.example.com/v2": {
   220  			"username": "u3",
   221  			"password": "p"
   222  		}
   223  	}
   224  }`)
   225  	qt.Assert(t, qt.IsNil(err))
   226  	_, err = c.EntryForRegistry("someregistry.example.com")
   227  	qt.Assert(t, qt.ErrorMatches(err, `more than one auths entry for "someregistry.example.com" \(http://someregistry.example.com/v1, http://someregistry.example.com/v2, https://someregistry.example.com/v1\)`))
   228  }
   229  
   230  func TestWithHelperBasic(t *testing.T) {
   231  	// Note: "test" matches the executable installed using testscript in RunMain.
   232  	c, err := load(t, nil, `
   233  {
   234  	"credHelpers": {
   235  		"registry-with-basic-auth.com": "test"
   236  	}
   237  }
   238  `)
   239  	qt.Assert(t, qt.IsNil(err))
   240  	info, err := c.EntryForRegistry("registry-with-basic-auth.com")
   241  	qt.Assert(t, qt.IsNil(err))
   242  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   243  		Username: "someuser",
   244  		Password: "somesecret",
   245  	}))
   246  }
   247  
   248  func TestWithHelperToken(t *testing.T) {
   249  	// Note: "test" matches the executable installed using testscript in RunMain.
   250  	c, err := load(t, nil, `
   251  {
   252  	"credHelpers": {
   253  		"registry-with-token.com": "test"
   254  	}
   255  }
   256  `)
   257  	qt.Assert(t, qt.IsNil(err))
   258  	info, err := c.EntryForRegistry("registry-with-token.com")
   259  	qt.Assert(t, qt.IsNil(err))
   260  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   261  		RefreshToken: "sometoken",
   262  	}))
   263  }
   264  
   265  func TestWithHelperRegistryNotFound(t *testing.T) {
   266  	// Note: "test" matches the executable installed using testscript in RunMain.
   267  	c, err := load(t, nil, `
   268  {
   269  	"credHelpers": {
   270  		"other.com": "test"
   271  	}
   272  }
   273  `)
   274  	qt.Assert(t, qt.IsNil(err))
   275  	info, err := c.EntryForRegistry("other.com")
   276  	qt.Assert(t, qt.IsNil(err))
   277  	qt.Assert(t, qt.Equals(info, ConfigEntry{}))
   278  }
   279  
   280  func TestWithHelperRegistryOtherError(t *testing.T) {
   281  	// Note: "test" matches the executable installed using testscript in RunMain.
   282  	c, err := load(t, nil, `
   283  {
   284  	"credHelpers": {
   285  		"registry-with-error.com": "test"
   286  	}
   287  }
   288  `)
   289  	qt.Assert(t, qt.IsNil(err))
   290  	_, err = c.EntryForRegistry("registry-with-error.com")
   291  	qt.Assert(t, qt.ErrorMatches(err, `error getting credentials: some error`))
   292  }
   293  
   294  func TestWithDefaultHelper(t *testing.T) {
   295  	// Note: "test" matches the executable installed using testscript in RunMain.
   296  	c, err := load(t, nil, `
   297  {
   298  	"credsStore": "test"
   299  }
   300  `)
   301  	qt.Assert(t, qt.IsNil(err))
   302  	info, err := c.EntryForRegistry("registry-with-basic-auth.com")
   303  	qt.Assert(t, qt.IsNil(err))
   304  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   305  		Username: "someuser",
   306  		Password: "somesecret",
   307  	}))
   308  }
   309  
   310  func TestWithDefaultHelperNotFound(t *testing.T) {
   311  	// When there's a helper not associated with any specific
   312  	// host, it ignores the fact that the executable isn't
   313  	// found and uses the regular "auths" info.
   314  	// See https://github.com/cue-lang/cue/issues/2934.
   315  	c, err := load(t, nil, `
   316  {
   317  	"credsStore": "definitely-not-found-executable",
   318  	"auths": {
   319  		"registry-with-basic-auth.com": {
   320  			"username": "u1",
   321  			"password": "p"
   322  		}
   323  	}
   324  }
   325  `)
   326  	qt.Assert(t, qt.IsNil(err))
   327  	info, err := c.EntryForRegistry("registry-with-basic-auth.com")
   328  	qt.Assert(t, qt.IsNil(err))
   329  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   330  		Username: "u1",
   331  		Password: "p",
   332  	}))
   333  }
   334  
   335  func TestWithDefaultHelperOtherError(t *testing.T) {
   336  	// When there's a helper not associated with any specific
   337  	// host, it's still an error if it's any error other than HelperNotFound.
   338  	errHelper := func(helperName string, serverURL string) (ConfigEntry, error) {
   339  		return ConfigEntry{}, fmt.Errorf("some error")
   340  	}
   341  	c, err := load(t, errHelper, `
   342  {
   343  	"credsStore": "test",
   344  	"auths": {
   345  		"registry-with-basic-auth.com": {
   346  			"username": "u1",
   347  			"password": "p"
   348  		}
   349  	}
   350  }
   351  `)
   352  	qt.Assert(t, qt.IsNil(err))
   353  	_, err = c.EntryForRegistry("registry-with-basic-auth.com")
   354  	qt.Assert(t, qt.ErrorMatches(err, `some error`))
   355  }
   356  
   357  func TestWithSpecificHelperNotFound(t *testing.T) {
   358  	// When there's a helper specifically configured for a host,
   359  	// it _is_ an error that the helper isn't found.
   360  	c, err := load(t, nil, `
   361  {
   362  	"credHelpers": {
   363  		"registry-with-basic-auth.com": "definitely-not-found-executable"
   364  	}
   365  }
   366  `)
   367  	qt.Assert(t, qt.IsNil(err))
   368  	_, err = c.EntryForRegistry("registry-with-basic-auth.com")
   369  	qt.Assert(t, qt.ErrorMatches(err, `helper not found: exec: "docker-credential-definitely-not-found-executable": executable file not found .*`))
   370  }
   371  
   372  func TestWithHelperAndExplicitEnv(t *testing.T) {
   373  	d := t.TempDir()
   374  	// Note: "test" matches the executable installed using testscript in RunMain.
   375  	err := os.WriteFile(filepath.Join(d, "config.json"), []byte(`
   376  {
   377  	"credHelpers": {
   378  		"registry-with-env-lookup.com": "test"
   379  	}
   380  }
   381  `), 0o666)
   382  	qt.Assert(t, qt.IsNil(err))
   383  	c, err := LoadWithEnv(nil, []string{
   384  		"DOCKER_CONFIG=" + d,
   385  		"TEST_SECRET=foo",
   386  	})
   387  	qt.Assert(t, qt.IsNil(err))
   388  	info, err := c.EntryForRegistry("registry-with-env-lookup.com")
   389  	qt.Assert(t, qt.IsNil(err))
   390  	qt.Assert(t, qt.Equals(info, ConfigEntry{
   391  		Username: "someuser",
   392  		Password: "foo",
   393  	}))
   394  }
   395  
   396  func load(t *testing.T, runner HelperRunner, cfgData string) (Config, error) {
   397  	d := t.TempDir()
   398  	t.Setenv("DOCKER_CONFIG", d)
   399  	err := os.WriteFile(filepath.Join(d, "config.json"), []byte(cfgData), 0o666)
   400  	qt.Assert(t, qt.IsNil(err))
   401  	return Load(runner)
   402  }
   403  
   404  func noRunner(helperName string, serverURL string) (ConfigEntry, error) {
   405  	panic("no helpers available")
   406  }
   407  
   408  // helperMain implements a docker credential command main function.
   409  func helperMain() int {
   410  	flag.Parse()
   411  	if flag.NArg() != 1 || flag.Arg(0) != "get" {
   412  		log.Fatal("usage: docker-credential-test get")
   413  	}
   414  	input, err := io.ReadAll(os.Stdin)
   415  	if err != nil {
   416  		log.Fatal(err)
   417  	}
   418  	switch string(input) {
   419  	case "registry-with-basic-auth.com":
   420  		fmt.Printf(`
   421  {
   422  	"Username": "someuser",
   423  	"Secret": "somesecret"
   424  }`)
   425  	case "registry-with-token.com":
   426  		fmt.Printf(`
   427  {
   428  	"Username": "<token>",
   429  	"Secret": "sometoken"
   430  }
   431  `)
   432  	case "registry-with-env-lookup.com":
   433  		fmt.Printf(`
   434  {
   435  	"Username": "someuser",
   436  	"Secret": %q
   437  }`, os.Getenv("TEST_SECRET"))
   438  	case "registry-with-error.com":
   439  		fmt.Fprintf(os.Stderr, "some error\n")
   440  		return 1
   441  	default:
   442  		fmt.Printf("credentials not found in native keychain\n")
   443  		return 1
   444  	}
   445  	return 0
   446  }