github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/cliconfig/credentials_test.go (about) 1 package cliconfig 2 3 import ( 4 "net/http" 5 "os" 6 "path/filepath" 7 "testing" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/zclconf/go-cty/cty" 11 12 svchost "github.com/hashicorp/terraform-svchost" 13 svcauth "github.com/hashicorp/terraform-svchost/auth" 14 ) 15 16 func TestCredentialsForHost(t *testing.T) { 17 credSrc := &CredentialsSource{ 18 configured: map[svchost.Hostname]cty.Value{ 19 "configured.example.com": cty.ObjectVal(map[string]cty.Value{ 20 "token": cty.StringVal("configured"), 21 }), 22 "unused.example.com": cty.ObjectVal(map[string]cty.Value{ 23 "token": cty.StringVal("incorrectly-configured"), 24 }), 25 }, 26 27 // We'll use a static source to stand in for what would normally be 28 // a credentials helper program, since we're only testing the logic 29 // for choosing when to delegate to the helper here. The logic for 30 // interacting with a helper program is tested in the svcauth package. 31 helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 32 "from-helper.example.com": { 33 "token": "from-helper", 34 }, 35 36 // This should be shadowed by the "configured" entry with the same 37 // hostname above. 38 "configured.example.com": { 39 "token": "incorrectly-from-helper", 40 }, 41 }), 42 helperType: "fake", 43 } 44 45 testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string { 46 t.Helper() 47 48 if creds == nil { 49 return "" 50 } 51 52 req, err := http.NewRequest("GET", "http://example.com/", nil) 53 if err != nil { 54 t.Fatalf("cannot construct HTTP request: %s", err) 55 } 56 creds.PrepareRequest(req) 57 return req.Header.Get("Authorization") 58 } 59 60 t.Run("configured", func(t *testing.T) { 61 creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com")) 62 if err != nil { 63 t.Fatalf("unexpected error: %s", err) 64 } 65 if got, want := testReqAuthHeader(t, creds), "Bearer configured"; got != want { 66 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 67 } 68 }) 69 t.Run("from helper", func(t *testing.T) { 70 creds, err := credSrc.ForHost(svchost.Hostname("from-helper.example.com")) 71 if err != nil { 72 t.Fatalf("unexpected error: %s", err) 73 } 74 if got, want := testReqAuthHeader(t, creds), "Bearer from-helper"; got != want { 75 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 76 } 77 }) 78 t.Run("not available", func(t *testing.T) { 79 creds, err := credSrc.ForHost(svchost.Hostname("unavailable.example.com")) 80 if err != nil { 81 t.Fatalf("unexpected error: %s", err) 82 } 83 if got, want := testReqAuthHeader(t, creds), ""; got != want { 84 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 85 } 86 }) 87 t.Run("set in environment", func(t *testing.T) { 88 envName := "TF_TOKEN_configured_example_com" 89 t.Cleanup(func() { 90 os.Unsetenv(envName) 91 }) 92 93 expectedToken := "configured-by-env" 94 os.Setenv(envName, expectedToken) 95 96 creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com")) 97 if err != nil { 98 t.Fatalf("unexpected error: %s", err) 99 } 100 101 if creds == nil { 102 t.Fatal("no credentials found") 103 } 104 105 if got := creds.Token(); got != expectedToken { 106 t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) 107 } 108 }) 109 110 t.Run("punycode name set in environment", func(t *testing.T) { 111 envName := "TF_TOKEN_env_xn--eckwd4c7cu47r2wf_com" 112 t.Cleanup(func() { 113 os.Unsetenv(envName) 114 }) 115 116 expectedToken := "configured-by-env" 117 os.Setenv(envName, expectedToken) 118 119 hostname, _ := svchost.ForComparison("env.ドメイン名例.com") 120 creds, err := credSrc.ForHost(hostname) 121 122 if err != nil { 123 t.Fatalf("unexpected error: %s", err) 124 } 125 126 if creds == nil { 127 t.Fatal("no credentials found") 128 } 129 130 if got := creds.Token(); got != expectedToken { 131 t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) 132 } 133 }) 134 135 t.Run("hyphens can be encoded as double underscores", func(t *testing.T) { 136 envName := "TF_TOKEN_env_xn____caf__dma_fr" 137 expectedToken := "configured-by-fallback" 138 t.Cleanup(func() { 139 os.Unsetenv(envName) 140 }) 141 142 os.Setenv(envName, expectedToken) 143 144 hostname, _ := svchost.ForComparison("env.café.fr") 145 creds, err := credSrc.ForHost(hostname) 146 147 if err != nil { 148 t.Fatalf("unexpected error: %s", err) 149 } 150 151 if creds == nil { 152 t.Fatal("no credentials found") 153 } 154 155 if got := creds.Token(); got != expectedToken { 156 t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) 157 } 158 }) 159 160 t.Run("periods are ok", func(t *testing.T) { 161 envName := "TF_TOKEN_configured.example.com" 162 expectedToken := "configured-by-env" 163 t.Cleanup(func() { 164 os.Unsetenv(envName) 165 }) 166 167 os.Setenv(envName, expectedToken) 168 169 hostname, _ := svchost.ForComparison("configured.example.com") 170 creds, err := credSrc.ForHost(hostname) 171 172 if err != nil { 173 t.Fatalf("unexpected error: %s", err) 174 } 175 176 if creds == nil { 177 t.Fatal("no credentials found") 178 } 179 180 if got := creds.Token(); got != expectedToken { 181 t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) 182 } 183 }) 184 185 t.Run("casing is insensitive", func(t *testing.T) { 186 envName := "TF_TOKEN_CONFIGUREDUPPERCASE_EXAMPLE_COM" 187 expectedToken := "configured-by-env" 188 189 os.Setenv(envName, expectedToken) 190 t.Cleanup(func() { 191 os.Unsetenv(envName) 192 }) 193 194 hostname, _ := svchost.ForComparison("configureduppercase.example.com") 195 creds, err := credSrc.ForHost(hostname) 196 197 if err != nil { 198 t.Fatalf("unexpected error: %s", err) 199 } 200 201 if creds == nil { 202 t.Fatal("no credentials found") 203 } 204 205 if got := creds.Token(); got != expectedToken { 206 t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken) 207 } 208 }) 209 } 210 211 func TestCredentialsStoreForget(t *testing.T) { 212 d := t.TempDir() 213 214 mockCredsFilename := filepath.Join(d, "credentials.tfrc.json") 215 216 cfg := &Config{ 217 // This simulates there being a credentials block manually configured 218 // in some file _other than_ credentials.tfrc.json. 219 Credentials: map[string]map[string]interface{}{ 220 "manually-configured.example.com": { 221 "token": "manually-configured", 222 }, 223 }, 224 } 225 226 // We'll initially use a credentials source with no credentials helper at 227 // all, and thus with credentials stored in the credentials file. 228 credSrc := cfg.credentialsSource( 229 "", nil, 230 mockCredsFilename, 231 ) 232 233 testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string { 234 t.Helper() 235 236 if creds == nil { 237 return "" 238 } 239 240 req, err := http.NewRequest("GET", "http://example.com/", nil) 241 if err != nil { 242 t.Fatalf("cannot construct HTTP request: %s", err) 243 } 244 creds.PrepareRequest(req) 245 return req.Header.Get("Authorization") 246 } 247 248 // Because these store/forget calls have side-effects, we'll bail out with 249 // t.Fatal (or equivalent) as soon as anything unexpected happens. 250 // Otherwise downstream tests might fail in confusing ways. 251 { 252 err := credSrc.StoreForHost( 253 svchost.Hostname("manually-configured.example.com"), 254 svcauth.HostCredentialsToken("not-manually-configured"), 255 ) 256 if err == nil { 257 t.Fatalf("successfully stored for manually-configured; want error") 258 } 259 if _, ok := err.(ErrUnwritableHostCredentials); !ok { 260 t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err) 261 } 262 } 263 { 264 err := credSrc.ForgetForHost( 265 svchost.Hostname("manually-configured.example.com"), 266 ) 267 if err == nil { 268 t.Fatalf("successfully forgot for manually-configured; want error") 269 } 270 if _, ok := err.(ErrUnwritableHostCredentials); !ok { 271 t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err) 272 } 273 } 274 { 275 // We don't have a credentials file at all yet, so this first call 276 // must create it. 277 err := credSrc.StoreForHost( 278 svchost.Hostname("stored-locally.example.com"), 279 svcauth.HostCredentialsToken("stored-locally"), 280 ) 281 if err != nil { 282 t.Fatalf("unexpected error storing locally: %s", err) 283 } 284 285 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 286 if err != nil { 287 t.Fatalf("failed to read back stored-locally credentials: %s", err) 288 } 289 290 if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally"; got != want { 291 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 292 } 293 294 got := readHostsInCredentialsFile(mockCredsFilename) 295 want := map[svchost.Hostname]struct{}{ 296 svchost.Hostname("stored-locally.example.com"): struct{}{}, 297 } 298 if diff := cmp.Diff(want, got); diff != "" { 299 t.Fatalf("wrong credentials file content\n%s", diff) 300 } 301 } 302 303 // Now we'll switch to having a credential helper active. 304 // If we were loading the real CLI config from disk here then this 305 // entry would already be in cfg.Credentials, but we need to fake that 306 // in the test because we're constructing this *Config value directly. 307 cfg.Credentials["stored-locally.example.com"] = map[string]interface{}{ 308 "token": "stored-locally", 309 } 310 mockHelper := &mockCredentialsHelper{current: make(map[svchost.Hostname]cty.Value)} 311 credSrc = cfg.credentialsSource( 312 "mock", mockHelper, 313 mockCredsFilename, 314 ) 315 { 316 err := credSrc.StoreForHost( 317 svchost.Hostname("manually-configured.example.com"), 318 svcauth.HostCredentialsToken("not-manually-configured"), 319 ) 320 if err == nil { 321 t.Fatalf("successfully stored for manually-configured with helper active; want error") 322 } 323 } 324 { 325 err := credSrc.StoreForHost( 326 svchost.Hostname("stored-in-helper.example.com"), 327 svcauth.HostCredentialsToken("stored-in-helper"), 328 ) 329 if err != nil { 330 t.Fatalf("unexpected error storing in helper: %s", err) 331 } 332 333 creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) 334 if err != nil { 335 t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 336 } 337 338 if got, want := testReqAuthHeader(t, creds), "Bearer stored-in-helper"; got != want { 339 t.Fatalf("wrong header value for stored-in-helper\ngot: %s\nwant: %s", got, want) 340 } 341 342 // Nothing should have changed in the saved credentials file 343 got := readHostsInCredentialsFile(mockCredsFilename) 344 want := map[svchost.Hostname]struct{}{ 345 svchost.Hostname("stored-locally.example.com"): struct{}{}, 346 } 347 if diff := cmp.Diff(want, got); diff != "" { 348 t.Fatalf("wrong credentials file content\n%s", diff) 349 } 350 } 351 { 352 // Because stored-locally is already in the credentials file, a new 353 // store should be sent there rather than to the credentials helper. 354 err := credSrc.StoreForHost( 355 svchost.Hostname("stored-locally.example.com"), 356 svcauth.HostCredentialsToken("stored-locally-again"), 357 ) 358 if err != nil { 359 t.Fatalf("unexpected error storing locally again: %s", err) 360 } 361 362 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 363 if err != nil { 364 t.Fatalf("failed to read back stored-locally credentials: %s", err) 365 } 366 367 if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally-again"; got != want { 368 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 369 } 370 } 371 { 372 // Forgetting a host already in the credentials file should remove it 373 // from the credentials file, not from the helper. 374 err := credSrc.ForgetForHost( 375 svchost.Hostname("stored-locally.example.com"), 376 ) 377 if err != nil { 378 t.Fatalf("unexpected error forgetting locally: %s", err) 379 } 380 381 creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com")) 382 if err != nil { 383 t.Fatalf("failed to read back stored-locally credentials: %s", err) 384 } 385 386 if got, want := testReqAuthHeader(t, creds), ""; got != want { 387 t.Fatalf("wrong header value for stored-locally\ngot: %s\nwant: %s", got, want) 388 } 389 390 // Should not be present in the credentials file anymore 391 got := readHostsInCredentialsFile(mockCredsFilename) 392 want := map[svchost.Hostname]struct{}{} 393 if diff := cmp.Diff(want, got); diff != "" { 394 t.Fatalf("wrong credentials file content\n%s", diff) 395 } 396 } 397 { 398 err := credSrc.ForgetForHost( 399 svchost.Hostname("stored-in-helper.example.com"), 400 ) 401 if err != nil { 402 t.Fatalf("unexpected error forgetting in helper: %s", err) 403 } 404 405 creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com")) 406 if err != nil { 407 t.Fatalf("failed to read back stored-in-helper credentials: %s", err) 408 } 409 410 if got, want := testReqAuthHeader(t, creds), ""; got != want { 411 t.Fatalf("wrong header value for stored-in-helper\ngot: %s\nwant: %s", got, want) 412 } 413 } 414 415 { 416 // Finally, the log in our mock helper should show that it was only 417 // asked to deal with stored-in-helper, not stored-locally. 418 got := mockHelper.log 419 want := []mockCredentialsHelperChange{ 420 { 421 Host: svchost.Hostname("stored-in-helper.example.com"), 422 Action: "store", 423 }, 424 { 425 Host: svchost.Hostname("stored-in-helper.example.com"), 426 Action: "forget", 427 }, 428 } 429 if diff := cmp.Diff(want, got); diff != "" { 430 t.Errorf("unexpected credentials helper operation log\n%s", diff) 431 } 432 } 433 } 434 435 type mockCredentialsHelperChange struct { 436 Host svchost.Hostname 437 Action string 438 } 439 440 type mockCredentialsHelper struct { 441 current map[svchost.Hostname]cty.Value 442 log []mockCredentialsHelperChange 443 } 444 445 // Assertion that mockCredentialsHelper implements svcauth.CredentialsSource 446 var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil) 447 448 func (s *mockCredentialsHelper) ForHost(hostname svchost.Hostname) (svcauth.HostCredentials, error) { 449 v, ok := s.current[hostname] 450 if !ok { 451 return nil, nil 452 } 453 return svcauth.HostCredentialsFromObject(v), nil 454 } 455 456 func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svcauth.HostCredentialsWritable) error { 457 s.log = append(s.log, mockCredentialsHelperChange{ 458 Host: hostname, 459 Action: "store", 460 }) 461 s.current[hostname] = new.ToStore() 462 return nil 463 } 464 465 func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error { 466 s.log = append(s.log, mockCredentialsHelperChange{ 467 Host: hostname, 468 Action: "forget", 469 }) 470 delete(s.current, hostname) 471 return nil 472 }