1
2
3
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
258 sorter := cmp.Transformer("Sort", func(in []string) []string {
259 out := append([]string(nil), in...)
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
667
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
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 }
1058
View as plain text