golang.org/x/oauth2@v0.18.0/google/externalaccount/executablecredsource_test.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package externalaccount 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "sort" 14 "testing" 15 "time" 16 17 "github.com/google/go-cmp/cmp" 18 ) 19 20 type testEnvironment struct { 21 envVars map[string]string 22 deadline time.Time 23 deadlineSet bool 24 byteResponse []byte 25 jsonResponse *executableResponse 26 } 27 28 var executablesAllowed = map[string]string{ 29 "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", 30 } 31 32 func (t *testEnvironment) existingEnv() []string { 33 result := []string{} 34 for k, v := range t.envVars { 35 result = append(result, fmt.Sprintf("%v=%v", k, v)) 36 } 37 return result 38 } 39 40 func (t *testEnvironment) getenv(key string) string { 41 return t.envVars[key] 42 } 43 44 func (t *testEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) { 45 t.deadline, t.deadlineSet = ctx.Deadline() 46 if t.jsonResponse != nil { 47 return json.Marshal(t.jsonResponse) 48 } 49 return t.byteResponse, nil 50 } 51 52 func (t *testEnvironment) getDeadline() (time.Time, bool) { 53 return t.deadline, t.deadlineSet 54 } 55 56 func (t *testEnvironment) now() time.Time { 57 return defaultTime 58 } 59 60 func Bool(b bool) *bool { 61 return &b 62 } 63 64 func Int(i int) *int { 65 return &i 66 } 67 68 var creationTests = []struct { 69 name string 70 executableConfig ExecutableConfig 71 expectedErr error 72 expectedTimeout time.Duration 73 }{ 74 { 75 name: "Basic Creation", 76 executableConfig: ExecutableConfig{ 77 Command: "blarg", 78 TimeoutMillis: Int(50000), 79 }, 80 expectedTimeout: 50000 * time.Millisecond, 81 }, 82 { 83 name: "Without Timeout", 84 executableConfig: ExecutableConfig{ 85 Command: "blarg", 86 }, 87 expectedTimeout: 30000 * time.Millisecond, 88 }, 89 { 90 name: "Without Command", 91 executableConfig: ExecutableConfig{}, 92 expectedErr: commandMissingError(), 93 }, 94 { 95 name: "Timeout Too Low", 96 executableConfig: ExecutableConfig{ 97 Command: "blarg", 98 TimeoutMillis: Int(4999), 99 }, 100 expectedErr: timeoutRangeError(), 101 }, 102 { 103 name: "Timeout Lower Bound", 104 executableConfig: ExecutableConfig{ 105 Command: "blarg", 106 TimeoutMillis: Int(5000), 107 }, 108 expectedTimeout: 5000 * time.Millisecond, 109 }, 110 { 111 name: "Timeout Upper Bound", 112 executableConfig: ExecutableConfig{ 113 Command: "blarg", 114 TimeoutMillis: Int(120000), 115 }, 116 expectedTimeout: 120000 * time.Millisecond, 117 }, 118 { 119 name: "Timeout Too High", 120 executableConfig: ExecutableConfig{ 121 Command: "blarg", 122 TimeoutMillis: Int(120001), 123 }, 124 expectedErr: timeoutRangeError(), 125 }, 126 } 127 128 func TestCreateExecutableCredential(t *testing.T) { 129 for _, tt := range creationTests { 130 t.Run(tt.name, func(t *testing.T) { 131 ecs, err := createExecutableCredential(context.Background(), &tt.executableConfig, nil) 132 if tt.expectedErr != nil { 133 if err == nil { 134 t.Fatalf("Expected error but found none") 135 } 136 if got, want := err.Error(), tt.expectedErr.Error(); got != want { 137 t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) 138 } 139 } else if err != nil { 140 ecJson := "{???}" 141 if ecBytes, err2 := json.Marshal(tt.executableConfig); err2 != nil { 142 ecJson = string(ecBytes) 143 } 144 145 t.Fatalf("CreateExecutableCredential with %v returned error: %v", ecJson, err) 146 } else { 147 if ecs.Command != "blarg" { 148 t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") 149 } 150 if ecs.Timeout != tt.expectedTimeout { 151 t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout) 152 } 153 if ecs.credentialSourceType() != "executable" { 154 t.Errorf("ecs.CredentialSourceType() got %s but want executable", ecs.credentialSourceType()) 155 } 156 } 157 }) 158 } 159 } 160 161 var getEnvironmentTests = []struct { 162 name string 163 config Config 164 environment testEnvironment 165 expectedEnvironment []string 166 }{ 167 { 168 name: "Minimal Executable Config", 169 config: Config{ 170 Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 171 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 172 CredentialSource: &CredentialSource{ 173 Executable: &ExecutableConfig{ 174 Command: "blarg", 175 }, 176 }, 177 }, 178 environment: testEnvironment{ 179 envVars: map[string]string{ 180 "A": "B", 181 }, 182 }, 183 expectedEnvironment: []string{ 184 "A=B", 185 "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 186 "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", 187 "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", 188 }, 189 }, 190 { 191 name: "Full Impersonation URL", 192 config: Config{ 193 Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 194 ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", 195 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 196 CredentialSource: &CredentialSource{ 197 Executable: &ExecutableConfig{ 198 Command: "blarg", 199 OutputFile: "/path/to/generated/cached/credentials", 200 }, 201 }, 202 }, 203 environment: testEnvironment{ 204 envVars: map[string]string{ 205 "A": "B", 206 }, 207 }, 208 expectedEnvironment: []string{ 209 "A=B", 210 "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 211 "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", 212 "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=test@project.iam.gserviceaccount.com", 213 "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", 214 "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials", 215 }, 216 }, 217 { 218 name: "Impersonation Email", 219 config: Config{ 220 Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 221 ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", 222 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 223 CredentialSource: &CredentialSource{ 224 Executable: &ExecutableConfig{ 225 Command: "blarg", 226 OutputFile: "/path/to/generated/cached/credentials", 227 }, 228 }, 229 }, 230 environment: testEnvironment{ 231 envVars: map[string]string{ 232 "A": "B", 233 }, 234 }, 235 expectedEnvironment: []string{ 236 "A=B", 237 "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", 238 "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", 239 "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", 240 "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials", 241 }, 242 }, 243 } 244 245 func TestExecutableCredentialGetEnvironment(t *testing.T) { 246 for _, tt := range getEnvironmentTests { 247 t.Run(tt.name, func(t *testing.T) { 248 config := tt.config 249 250 ecs, err := createExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) 251 if err != nil { 252 t.Fatalf("creation failed %v", err) 253 } 254 255 ecs.env = &tt.environment 256 257 // This Transformer sorts a []string. 258 sorter := cmp.Transformer("Sort", func(in []string) []string { 259 out := append([]string(nil), in...) // Copy input to avoid mutating it 260 sort.Strings(out) 261 return out 262 }) 263 264 if got, want := ecs.executableEnvironment(), tt.expectedEnvironment; !cmp.Equal(got, want, sorter) { 265 t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want) 266 } 267 }) 268 } 269 } 270 271 var failureTests = []struct { 272 name string 273 testEnvironment testEnvironment 274 noExecution bool 275 expectedErr error 276 }{ 277 { 278 name: "Environment Variable Not Set", 279 testEnvironment: testEnvironment{ 280 byteResponse: []byte{}, 281 }, 282 noExecution: true, 283 expectedErr: executablesDisallowedError(), 284 }, 285 286 { 287 name: "Invalid Token", 288 testEnvironment: testEnvironment{ 289 envVars: executablesAllowed, 290 byteResponse: []byte("tokentokentoken"), 291 }, 292 expectedErr: jsonParsingError(executableSource, "tokentokentoken"), 293 }, 294 295 { 296 name: "Version Field Missing", 297 testEnvironment: testEnvironment{ 298 envVars: executablesAllowed, 299 jsonResponse: &executableResponse{ 300 Success: Bool(true), 301 }, 302 }, 303 expectedErr: missingFieldError(executableSource, "version"), 304 }, 305 306 { 307 name: "Success Field Missing", 308 testEnvironment: testEnvironment{ 309 envVars: executablesAllowed, 310 jsonResponse: &executableResponse{ 311 Version: 1, 312 }, 313 }, 314 expectedErr: missingFieldError(executableSource, "success"), 315 }, 316 317 { 318 name: "User defined error", 319 testEnvironment: testEnvironment{ 320 envVars: executablesAllowed, 321 jsonResponse: &executableResponse{ 322 Success: Bool(false), 323 Version: 1, 324 Code: "404", 325 Message: "Token Not Found", 326 }, 327 }, 328 expectedErr: userDefinedError("404", "Token Not Found"), 329 }, 330 331 { 332 name: "User defined error without code", 333 testEnvironment: testEnvironment{ 334 envVars: executablesAllowed, 335 jsonResponse: &executableResponse{ 336 Success: Bool(false), 337 Version: 1, 338 Message: "Token Not Found", 339 }, 340 }, 341 expectedErr: malformedFailureError(), 342 }, 343 344 { 345 name: "User defined error without message", 346 testEnvironment: testEnvironment{ 347 envVars: executablesAllowed, 348 jsonResponse: &executableResponse{ 349 Success: Bool(false), 350 Version: 1, 351 Code: "404", 352 }, 353 }, 354 expectedErr: malformedFailureError(), 355 }, 356 357 { 358 name: "User defined error without fields", 359 testEnvironment: testEnvironment{ 360 envVars: executablesAllowed, 361 jsonResponse: &executableResponse{ 362 Success: Bool(false), 363 Version: 1, 364 }, 365 }, 366 expectedErr: malformedFailureError(), 367 }, 368 369 { 370 name: "Newer Version", 371 testEnvironment: testEnvironment{ 372 envVars: executablesAllowed, 373 jsonResponse: &executableResponse{ 374 Success: Bool(true), 375 Version: 2, 376 }, 377 }, 378 expectedErr: unsupportedVersionError(executableSource, 2), 379 }, 380 381 { 382 name: "Missing Token Type", 383 testEnvironment: testEnvironment{ 384 envVars: executablesAllowed, 385 jsonResponse: &executableResponse{ 386 Success: Bool(true), 387 Version: 1, 388 ExpirationTime: defaultTime.Unix(), 389 }, 390 }, 391 expectedErr: missingFieldError(executableSource, "token_type"), 392 }, 393 394 { 395 name: "Token Expired", 396 testEnvironment: testEnvironment{ 397 envVars: executablesAllowed, 398 jsonResponse: &executableResponse{ 399 Success: Bool(true), 400 Version: 1, 401 ExpirationTime: defaultTime.Unix() - 1, 402 TokenType: "urn:ietf:params:oauth:token-type:jwt", 403 }, 404 }, 405 expectedErr: tokenExpiredError(), 406 }, 407 408 { 409 name: "Invalid Token Type", 410 testEnvironment: testEnvironment{ 411 envVars: executablesAllowed, 412 jsonResponse: &executableResponse{ 413 Success: Bool(true), 414 Version: 1, 415 ExpirationTime: defaultTime.Unix(), 416 TokenType: "urn:ietf:params:oauth:token-type:invalid", 417 }, 418 }, 419 expectedErr: tokenTypeError(executableSource), 420 }, 421 422 { 423 name: "Missing JWT", 424 testEnvironment: testEnvironment{ 425 envVars: executablesAllowed, 426 jsonResponse: &executableResponse{ 427 Success: Bool(true), 428 Version: 1, 429 ExpirationTime: defaultTime.Unix(), 430 TokenType: "urn:ietf:params:oauth:token-type:jwt", 431 }, 432 }, 433 expectedErr: missingFieldError(executableSource, "id_token"), 434 }, 435 436 { 437 name: "Missing ID Token", 438 testEnvironment: testEnvironment{ 439 envVars: executablesAllowed, 440 jsonResponse: &executableResponse{ 441 Success: Bool(true), 442 Version: 1, 443 ExpirationTime: defaultTime.Unix(), 444 TokenType: "urn:ietf:params:oauth:token-type:id_token", 445 }, 446 }, 447 expectedErr: missingFieldError(executableSource, "id_token"), 448 }, 449 450 { 451 name: "Missing SAML Token", 452 testEnvironment: testEnvironment{ 453 envVars: executablesAllowed, 454 jsonResponse: &executableResponse{ 455 Success: Bool(true), 456 Version: 1, 457 ExpirationTime: defaultTime.Unix(), 458 TokenType: "urn:ietf:params:oauth:token-type:saml2", 459 }, 460 }, 461 expectedErr: missingFieldError(executableSource, "saml_response"), 462 }, 463 } 464 465 func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) { 466 cs := CredentialSource{ 467 Executable: &ExecutableConfig{ 468 Command: "blarg", 469 TimeoutMillis: Int(5000), 470 }, 471 } 472 473 tfc := testFileConfig 474 tfc.CredentialSource = &cs 475 476 base, err := tfc.parse(context.Background()) 477 if err != nil { 478 t.Fatalf("parse() failed %v", err) 479 } 480 481 ecs, ok := base.(executableCredentialSource) 482 if !ok { 483 t.Fatalf("Wrong credential type created.") 484 } 485 486 for _, tt := range failureTests { 487 t.Run(tt.name, func(t *testing.T) { 488 ecs.env = &tt.testEnvironment 489 490 if _, err = ecs.subjectToken(); err == nil { 491 t.Fatalf("Expected error but found none") 492 } else if got, want := err.Error(), tt.expectedErr.Error(); got != want { 493 t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) 494 } 495 496 deadline, deadlineSet := tt.testEnvironment.getDeadline() 497 if tt.noExecution { 498 if deadlineSet { 499 t.Errorf("Executable called when it should not have been") 500 } 501 } else { 502 if !deadlineSet { 503 t.Errorf("Command run without a deadline") 504 } else if deadline != defaultTime.Add(5*time.Second) { 505 t.Errorf("Command run with incorrect deadline") 506 } 507 } 508 }) 509 } 510 } 511 512 var successTests = []struct { 513 name string 514 testEnvironment testEnvironment 515 }{ 516 { 517 name: "JWT", 518 testEnvironment: testEnvironment{ 519 envVars: executablesAllowed, 520 jsonResponse: &executableResponse{ 521 Success: Bool(true), 522 Version: 1, 523 ExpirationTime: defaultTime.Unix() + 3600, 524 TokenType: "urn:ietf:params:oauth:token-type:jwt", 525 IdToken: "tokentokentoken", 526 }, 527 }, 528 }, 529 530 { 531 name: "ID Token", 532 testEnvironment: testEnvironment{ 533 envVars: executablesAllowed, 534 jsonResponse: &executableResponse{ 535 Success: Bool(true), 536 Version: 1, 537 ExpirationTime: defaultTime.Unix() + 3600, 538 TokenType: "urn:ietf:params:oauth:token-type:id_token", 539 IdToken: "tokentokentoken", 540 }, 541 }, 542 }, 543 544 { 545 name: "SAML", 546 testEnvironment: testEnvironment{ 547 envVars: executablesAllowed, 548 jsonResponse: &executableResponse{ 549 Success: Bool(true), 550 Version: 1, 551 ExpirationTime: defaultTime.Unix() + 3600, 552 TokenType: "urn:ietf:params:oauth:token-type:saml2", 553 SamlResponse: "tokentokentoken", 554 }, 555 }, 556 }, 557 558 { 559 name: "Missing Expiration", 560 testEnvironment: testEnvironment{ 561 envVars: executablesAllowed, 562 jsonResponse: &executableResponse{ 563 Success: Bool(true), 564 Version: 1, 565 TokenType: "urn:ietf:params:oauth:token-type:jwt", 566 IdToken: "tokentokentoken", 567 }, 568 }, 569 }, 570 } 571 572 func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) { 573 cs := CredentialSource{ 574 Executable: &ExecutableConfig{ 575 Command: "blarg", 576 TimeoutMillis: Int(5000), 577 }, 578 } 579 580 tfc := testFileConfig 581 tfc.CredentialSource = &cs 582 583 base, err := tfc.parse(context.Background()) 584 if err != nil { 585 t.Fatalf("parse() failed %v", err) 586 } 587 588 ecs, ok := base.(executableCredentialSource) 589 if !ok { 590 t.Fatalf("Wrong credential type created.") 591 } 592 593 for _, tt := range successTests { 594 t.Run(tt.name, func(t *testing.T) { 595 ecs.env = &tt.testEnvironment 596 597 out, err := ecs.subjectToken() 598 if err != nil { 599 t.Fatalf("retrieveSubjectToken() failed: %v", err) 600 } 601 602 deadline, deadlineSet := tt.testEnvironment.getDeadline() 603 if !deadlineSet { 604 t.Errorf("Command run without a deadline") 605 } else if deadline != defaultTime.Add(5*time.Second) { 606 t.Errorf("Command run with incorrect deadline") 607 } 608 609 if got, want := out, "tokentokentoken"; got != want { 610 t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want) 611 } 612 }) 613 } 614 } 615 616 func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) { 617 outputFile, err := ioutil.TempFile("testdata", "result.*.json") 618 if err != nil { 619 t.Fatalf("Tempfile failed: %v", err) 620 } 621 defer os.Remove(outputFile.Name()) 622 623 cs := CredentialSource{ 624 Executable: &ExecutableConfig{ 625 Command: "blarg", 626 TimeoutMillis: Int(5000), 627 OutputFile: outputFile.Name(), 628 }, 629 } 630 631 tfc := testFileConfig 632 tfc.CredentialSource = &cs 633 634 base, err := tfc.parse(context.Background()) 635 if err != nil { 636 t.Fatalf("parse() failed %v", err) 637 } 638 639 ecs, ok := base.(executableCredentialSource) 640 if !ok { 641 t.Fatalf("Wrong credential type created.") 642 } 643 644 if _, err = outputFile.Write([]byte("tokentokentoken")); err != nil { 645 t.Fatalf("error writing to file: %v", err) 646 } 647 648 te := testEnvironment{ 649 envVars: executablesAllowed, 650 byteResponse: []byte{}, 651 } 652 ecs.env = &te 653 654 if _, err = base.subjectToken(); err == nil { 655 t.Fatalf("Expected error but found none") 656 } else if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want { 657 t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) 658 } 659 660 _, deadlineSet := te.getDeadline() 661 if deadlineSet { 662 t.Errorf("Executable called when it should not have been") 663 } 664 } 665 666 // These are errors in the output file that should be reported to the user. 667 // Most of these will help the developers debug their code. 668 var cacheFailureTests = []struct { 669 name string 670 outputFileContents executableResponse 671 expectedErr error 672 }{ 673 { 674 name: "Missing Version", 675 outputFileContents: executableResponse{ 676 Success: Bool(true), 677 }, 678 expectedErr: missingFieldError(outputFileSource, "version"), 679 }, 680 681 { 682 name: "Missing Success", 683 outputFileContents: executableResponse{ 684 Version: 1, 685 }, 686 expectedErr: missingFieldError(outputFileSource, "success"), 687 }, 688 689 { 690 name: "Newer Version", 691 outputFileContents: executableResponse{ 692 Success: Bool(true), 693 Version: 2, 694 }, 695 expectedErr: unsupportedVersionError(outputFileSource, 2), 696 }, 697 698 { 699 name: "Missing Token Type", 700 outputFileContents: executableResponse{ 701 Success: Bool(true), 702 Version: 1, 703 ExpirationTime: defaultTime.Unix(), 704 }, 705 expectedErr: missingFieldError(outputFileSource, "token_type"), 706 }, 707 708 { 709 name: "Missing Expiration", 710 outputFileContents: executableResponse{ 711 Success: Bool(true), 712 Version: 1, 713 TokenType: "urn:ietf:params:oauth:token-type:jwt", 714 }, 715 expectedErr: missingFieldError(outputFileSource, "expiration_time"), 716 }, 717 718 { 719 name: "Invalid Token Type", 720 outputFileContents: executableResponse{ 721 Success: Bool(true), 722 Version: 1, 723 ExpirationTime: defaultTime.Unix(), 724 TokenType: "urn:ietf:params:oauth:token-type:invalid", 725 }, 726 expectedErr: tokenTypeError(outputFileSource), 727 }, 728 729 { 730 name: "Missing JWT", 731 outputFileContents: executableResponse{ 732 Success: Bool(true), 733 Version: 1, 734 ExpirationTime: defaultTime.Unix() + 3600, 735 TokenType: "urn:ietf:params:oauth:token-type:jwt", 736 }, 737 expectedErr: missingFieldError(outputFileSource, "id_token"), 738 }, 739 740 { 741 name: "Missing ID Token", 742 outputFileContents: executableResponse{ 743 Success: Bool(true), 744 Version: 1, 745 ExpirationTime: defaultTime.Unix() + 3600, 746 TokenType: "urn:ietf:params:oauth:token-type:id_token", 747 }, 748 expectedErr: missingFieldError(outputFileSource, "id_token"), 749 }, 750 751 { 752 name: "Missing SAML", 753 outputFileContents: executableResponse{ 754 Success: Bool(true), 755 Version: 1, 756 ExpirationTime: defaultTime.Unix() + 3600, 757 TokenType: "urn:ietf:params:oauth:token-type:jwt", 758 }, 759 expectedErr: missingFieldError(outputFileSource, "id_token"), 760 }, 761 } 762 763 func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) { 764 for _, tt := range cacheFailureTests { 765 t.Run(tt.name, func(t *testing.T) { 766 outputFile, err := ioutil.TempFile("testdata", "result.*.json") 767 if err != nil { 768 t.Fatalf("Tempfile failed: %v", err) 769 } 770 defer os.Remove(outputFile.Name()) 771 772 cs := CredentialSource{ 773 Executable: &ExecutableConfig{ 774 Command: "blarg", 775 TimeoutMillis: Int(5000), 776 OutputFile: outputFile.Name(), 777 }, 778 } 779 780 tfc := testFileConfig 781 tfc.CredentialSource = &cs 782 783 base, err := tfc.parse(context.Background()) 784 if err != nil { 785 t.Fatalf("parse() failed %v", err) 786 } 787 788 ecs, ok := base.(executableCredentialSource) 789 if !ok { 790 t.Fatalf("Wrong credential type created.") 791 } 792 te := testEnvironment{ 793 envVars: executablesAllowed, 794 byteResponse: []byte{}, 795 } 796 ecs.env = &te 797 if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { 798 t.Errorf("Error encoding to file: %v", err) 799 return 800 } 801 if _, err = ecs.subjectToken(); err == nil { 802 t.Errorf("Expected error but found none") 803 } else if got, want := err.Error(), tt.expectedErr.Error(); got != want { 804 t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) 805 } 806 807 if _, deadlineSet := te.getDeadline(); deadlineSet { 808 t.Errorf("Executable called when it should not have been") 809 } 810 }) 811 } 812 } 813 814 // These tests should ignore the error in the output file, and check the executable. 815 var invalidCacheTests = []struct { 816 name string 817 outputFileContents executableResponse 818 }{ 819 { 820 name: "User Defined Error", 821 outputFileContents: executableResponse{ 822 Success: Bool(false), 823 Version: 1, 824 Code: "404", 825 Message: "Token Not Found", 826 }, 827 }, 828 829 { 830 name: "User Defined Error without Code", 831 outputFileContents: executableResponse{ 832 Success: Bool(false), 833 Version: 1, 834 Message: "Token Not Found", 835 }, 836 }, 837 838 { 839 name: "User Defined Error without Message", 840 outputFileContents: executableResponse{ 841 Success: Bool(false), 842 Version: 1, 843 Code: "404", 844 }, 845 }, 846 847 { 848 name: "User Defined Error without Fields", 849 outputFileContents: executableResponse{ 850 Success: Bool(false), 851 Version: 1, 852 }, 853 }, 854 855 { 856 name: "Expired Token", 857 outputFileContents: executableResponse{ 858 Success: Bool(true), 859 Version: 1, 860 ExpirationTime: defaultTime.Unix() - 1, 861 TokenType: "urn:ietf:params:oauth:token-type:jwt", 862 }, 863 }, 864 } 865 866 func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) { 867 for _, tt := range invalidCacheTests { 868 t.Run(tt.name, func(t *testing.T) { 869 outputFile, err := ioutil.TempFile("testdata", "result.*.json") 870 if err != nil { 871 t.Fatalf("Tempfile failed: %v", err) 872 } 873 defer os.Remove(outputFile.Name()) 874 875 cs := CredentialSource{ 876 Executable: &ExecutableConfig{ 877 Command: "blarg", 878 TimeoutMillis: Int(5000), 879 OutputFile: outputFile.Name(), 880 }, 881 } 882 883 tfc := testFileConfig 884 tfc.CredentialSource = &cs 885 886 base, err := tfc.parse(context.Background()) 887 if err != nil { 888 t.Fatalf("parse() failed %v", err) 889 } 890 891 te := testEnvironment{ 892 envVars: executablesAllowed, 893 jsonResponse: &executableResponse{ 894 Success: Bool(true), 895 Version: 1, 896 ExpirationTime: defaultTime.Unix() + 3600, 897 TokenType: "urn:ietf:params:oauth:token-type:jwt", 898 IdToken: "tokentokentoken", 899 }, 900 } 901 902 ecs, ok := base.(executableCredentialSource) 903 if !ok { 904 t.Fatalf("Wrong credential type created.") 905 } 906 ecs.env = &te 907 908 if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { 909 t.Errorf("Error encoding to file: %v", err) 910 return 911 } 912 913 out, err := ecs.subjectToken() 914 if err != nil { 915 t.Errorf("retrieveSubjectToken() failed: %v", err) 916 return 917 } 918 919 if deadline, deadlineSet := te.getDeadline(); !deadlineSet { 920 t.Errorf("Command run without a deadline") 921 } else if deadline != defaultTime.Add(5*time.Second) { 922 t.Errorf("Command run with incorrect deadline") 923 } 924 925 if got, want := out, "tokentokentoken"; got != want { 926 t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) 927 } 928 }) 929 } 930 } 931 932 var cacheSuccessTests = []struct { 933 name string 934 outputFileContents executableResponse 935 }{ 936 { 937 name: "JWT", 938 outputFileContents: executableResponse{ 939 Success: Bool(true), 940 Version: 1, 941 ExpirationTime: defaultTime.Unix() + 3600, 942 TokenType: "urn:ietf:params:oauth:token-type:jwt", 943 IdToken: "tokentokentoken", 944 }, 945 }, 946 947 { 948 name: "Id Token", 949 outputFileContents: executableResponse{ 950 Success: Bool(true), 951 Version: 1, 952 ExpirationTime: defaultTime.Unix() + 3600, 953 TokenType: "urn:ietf:params:oauth:token-type:id_token", 954 IdToken: "tokentokentoken", 955 }, 956 }, 957 958 { 959 name: "SAML", 960 outputFileContents: executableResponse{ 961 Success: Bool(true), 962 Version: 1, 963 ExpirationTime: defaultTime.Unix() + 3600, 964 TokenType: "urn:ietf:params:oauth:token-type:saml2", 965 SamlResponse: "tokentokentoken", 966 }, 967 }, 968 } 969 970 func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { 971 for _, tt := range cacheSuccessTests { 972 t.Run(tt.name, func(t *testing.T) { 973 974 outputFile, err := ioutil.TempFile("testdata", "result.*.json") 975 if err != nil { 976 t.Fatalf("Tempfile failed: %v", err) 977 } 978 defer os.Remove(outputFile.Name()) 979 980 cs := CredentialSource{ 981 Executable: &ExecutableConfig{ 982 Command: "blarg", 983 TimeoutMillis: Int(5000), 984 OutputFile: outputFile.Name(), 985 }, 986 } 987 988 tfc := testFileConfig 989 tfc.CredentialSource = &cs 990 991 base, err := tfc.parse(context.Background()) 992 if err != nil { 993 t.Fatalf("parse() failed %v", err) 994 } 995 996 te := testEnvironment{ 997 envVars: executablesAllowed, 998 byteResponse: []byte{}, 999 } 1000 1001 ecs, ok := base.(executableCredentialSource) 1002 if !ok { 1003 t.Fatalf("Wrong credential type created.") 1004 } 1005 ecs.env = &te 1006 1007 if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { 1008 t.Errorf("Error encoding to file: %v", err) 1009 return 1010 } 1011 1012 if out, err := ecs.subjectToken(); err != nil { 1013 t.Errorf("retrieveSubjectToken() failed: %v", err) 1014 } else if got, want := out, "tokentokentoken"; got != want { 1015 t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) 1016 } 1017 1018 if _, deadlineSet := te.getDeadline(); deadlineSet { 1019 t.Errorf("Executable called when it should not have been") 1020 } 1021 }) 1022 } 1023 } 1024 1025 func TestServiceAccountImpersonationRE(t *testing.T) { 1026 tests := []struct { 1027 name string 1028 serviceAccountImpersonationURL string 1029 want string 1030 }{ 1031 { 1032 name: "universe domain Google Default Universe (GDU) googleapis.com", 1033 serviceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", 1034 want: "test@project.iam.gserviceaccount.com", 1035 }, 1036 { 1037 name: "email does not match", 1038 serviceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", 1039 want: "", 1040 }, 1041 { 1042 name: "universe domain non-GDU", 1043 serviceAccountImpersonationURL: "https://iamcredentials.apis-tpclp.goog/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", 1044 want: "test@project.iam.gserviceaccount.com", 1045 }, 1046 } 1047 for _, tt := range tests { 1048 matches := serviceAccountImpersonationRE.FindStringSubmatch(tt.serviceAccountImpersonationURL) 1049 if matches == nil { 1050 if tt.want != "" { 1051 t.Errorf("%q: got nil, want %q", tt.name, tt.want) 1052 } 1053 } else if matches[1] != tt.want { 1054 t.Errorf("%q: got %q, want %q", tt.name, matches[1], tt.want) 1055 } 1056 } 1057 }