...

Source file src/golang.org/x/oauth2/google/externalaccount/executablecredsource_test.go

Documentation: golang.org/x/oauth2/google/externalaccount

     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  }
  1058  

View as plain text