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