...

Source file src/cloud.google.com/go/auth/credentials/internal/externalaccount/executable_provider_test.go

Documentation: cloud.google.com/go/auth/credentials/internal/externalaccount

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    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  			// This Transformer sorts a []string.
   230  			sorter := cmp.Transformer("Sort", func(in []string) []string {
   231  				out := append([]string(nil), in...) // Copy input to avoid mutating it
   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  				// Do no more validation
   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  	// These are errors in the output file that should be reported to the user.
   630  	// Most of these will help the developers debug their code.
   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  	// These tests should ignore the error in the output file, and check the executable.
   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