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 }