diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 49128f1..e97daa4 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -171,7 +171,7 @@ type CredentialSource struct { URL string `json:"url"` Headers map[string]string `json:"headers"` - Executable ExecutableConfig `json:"executable"` + Executable *ExecutableConfig `json:"executable"` EnvironmentID string `json:"environment_id"` RegionURL string `json:"region_url"` @@ -187,7 +187,7 @@ type ExecutableConfig struct { OutputFile string `json:"output_file"` } -// parse determines the type of CredentialSource needed +// parse determines the type of CredentialSource needed. func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { @@ -213,8 +213,8 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil - } else if c.CredentialSource.Executable.Command != "" { - return CreateExecutableCredential(c.CredentialSource.Executable, c, ctx), nil + } else if c.CredentialSource.Executable != nil { + return CreateExecutableCredential(*c.CredentialSource.Executable, c, ctx), nil } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 90e8117..4651c31 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -15,10 +15,56 @@ import ( "time" ) -var ( - EXECUTABLE_SUPPORTED_MAX_VERSION = 1 +const ( + executableSupportedMaxVersion = 1 + defaultTimeout = 30 * time.Second ) +func missingFieldError(field string) error { + return fmt.Errorf("oauth2/google: response missing `%v` field", field) +} + +func jsonParsingError() error { + return errors.New("oauth2/google: unable to parse response JSON") +} + +func malformedFailureError() error { + return errors.New("oauth2/google: response must include `error` and `message` fields when unsuccessful") +} + +func userDefinedError(code, message string) error { + return fmt.Errorf("oauth2/google: executable returned unsuccessful response: (%v) %v", code, message) +} + +func unsupportedVersionError(version int) error { + return fmt.Errorf("oauth2/google: executable returned unsupported version: %v", version) +} + +func tokenExpiredError() error { + return errors.New("oauth2/google: the token returned by the executable is expired") +} + +func tokenTypeError() error { + return errors.New("oauth2/google: executable returned unsupported token type") +} + +func timeoutError() error { + return errors.New("oauth2/google: executable command timed out") +} + +func exitCodeError(exitCode int) error { + return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode) +} + +func executableError(err error) error { + return fmt.Errorf("oauth2/google: executable command failed: %v", err.Error()) +} + +func executablesDisallowedError() error { + return errors.New("Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") +} + +// baseEnv is an alias of os.Environ used for testing var baseEnv = os.Environ // runCommand is basically an alias of exec.CommandContext for testing. @@ -46,7 +92,7 @@ type executableCredentialSource struct { func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context.Context) (result executableCredentialSource) { result.Command = ec.Command if ec.TimeoutMillis == 0 { - result.Timeout = 30 * time.Second + result.Timeout = defaultTimeout } else { result.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond } @@ -57,68 +103,68 @@ func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context } type executableResponse struct { - Version *int `json:"version"` - Success *bool `json:"success"` - TokenType *string `json:"token_type"` - ExpirationTime *int64 `json:"expiration_time"` - IdToken *string `json:"id_token"` - SamlResponse *string `json:"saml_response"` - Code string `json:"code"` - Message string `json:"message"` + Version *int `json:"version,omitempty"` + Success *bool `json:"success,omitempty"` + TokenType *string `json:"token_type,omitempty"` + ExpirationTime *int64 `json:"expiration_time,omitempty"` + IdToken *string `json:"id_token,omitempty"` + SamlResponse *string `json:"saml_response,omitempty"` + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` } func parseSubjectToken(response []byte) (string, error) { var result executableResponse if err := json.Unmarshal(response, &result); err != nil { - return "", errors.New("oauth2/google: Unable to parse response JSON.") + return "", jsonParsingError() } if result.Version == nil { - return "", errors.New("oauth2/google: Response missing version field.") + return "", missingFieldError("version") } if result.Success == nil { - return "", errors.New("oauth2/google: Response missing success field.") + return "", missingFieldError("success") } if !*result.Success { if result.Code == "" || result.Message == "" { - return "", errors.New("oauth2/google: Response must include `error` and `message` fields when unsuccessful.") + return "", malformedFailureError() } - return "", fmt.Errorf("oauth2/google: Executable returned unsuccessful response: (%v) %v.", result.Code, result.Message) + return "", userDefinedError(result.Code, result.Message) } - if *result.Version > EXECUTABLE_SUPPORTED_MAX_VERSION { - return "", fmt.Errorf("oauth2/google: Executable returned unsupported version: %v.", *result.Version) + if *result.Version > executableSupportedMaxVersion { + return "", unsupportedVersionError(*result.Version) } if result.ExpirationTime == nil { - return "", errors.New("oauth2/google: Response missing expiration_time field.") + return "", missingFieldError("expiration_time") } if result.TokenType == nil { - return "", errors.New("oauth2/google: Response missing token_type field.") + return "", missingFieldError("token_type") } if *result.ExpirationTime < now().Unix() { - return "", errors.New("oauth2/google: The token returned by the executable is expired.") + return "", tokenExpiredError() } if *result.TokenType == "urn:ietf:params:oauth:token-type:jwt" || *result.TokenType == "urn:ietf:params:oauth:token-type:id_token" { if result.IdToken == nil { - return "", errors.New("oauth2/google: Response missing id_token field.") + return "", missingFieldError("id_token") } return *result.IdToken, nil } if *result.TokenType == "urn:ietf:params:oauth:token-type:saml2" { if result.SamlResponse == nil { - return "", errors.New("oauth2/google: Response missing saml_response field.") + return "", missingFieldError("saml_response") } return *result.SamlResponse, nil } - return "", errors.New("Executable returned unsupported token type.") + return "", tokenTypeError() } func (cs executableCredentialSource) subjectToken() (string, error) { @@ -169,7 +215,7 @@ func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]str func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, error) { // For security reasons, we need our consumers to set this environment variable to allow executables to be run. if getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" { - return "", errors.New("Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run.") + return "", executablesDisallowedError() } ctx, cancel := context.WithDeadline(cs.ctx, now().Add(cs.Timeout)) @@ -177,12 +223,12 @@ func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, er if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil { if err == context.DeadlineExceeded { - return "", fmt.Errorf("oauth2/google: executable command timed out.") + return "", timeoutError() } if exitError, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("oauth2/google: executable command failed with exit code %v.", exitError.ExitCode()) + return "", exitCodeError(exitError.ExitCode()) } - return "", fmt.Errorf("oauth2/google: executable command failed: %v.", err.Error()) + return "", executableError(err) } else { return parseSubjectToken(output) } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index fec3720..9abb276 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -7,6 +7,7 @@ package externalaccount import ( "context" "encoding/json" + "errors" "fmt" "testing" "time" @@ -56,7 +57,7 @@ func TestCreateExecutableCredential_WithoutTimeout(t *testing.T) { if ecs.Command != "blarg" { t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") } - if ecs.Timeout != 30000*time.Millisecond { + if ecs.Timeout != defaultTimeout { t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, 30000*time.Millisecond) } } @@ -78,18 +79,18 @@ OUTER: return true } -func TestMinimalExectuableCredentialGetEnvironment(t *testing.T) { +func TestMinimalExecutableCredentialGetEnvironment(t *testing.T) { config := Config{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", CredentialSource: CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", }, }, } - ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -105,7 +106,7 @@ func TestMinimalExectuableCredentialGetEnvironment(t *testing.T) { } if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { - t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want) } } @@ -115,14 +116,14 @@ func TestExectuableCredentialGetEnvironmentMalformedImpersonationUrl(t *testing. ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", CredentialSource: CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", OutputFile: "/path/to/generated/cached/credentials", }, }, } - ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -140,23 +141,24 @@ func TestExectuableCredentialGetEnvironmentMalformedImpersonationUrl(t *testing. } if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { - t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want) } } + func TestExectuableCredentialGetEnvironment(t *testing.T) { config := Config{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", CredentialSource: CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", OutputFile: "/path/to/generated/cached/credentials", }, }, } - ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -173,13 +175,13 @@ func TestExectuableCredentialGetEnvironment(t *testing.T) { } if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { - t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want) } } func TestRetrieveExecutableSubjectTokenWithoutEnvironmentVariablesSet(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -201,14 +203,58 @@ func TestRetrieveExecutableSubjectTokenWithoutEnvironmentVariablesSet(t *testing if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), executablesDisallowedError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestRetrieveExecutableSubjectExecutableErrorOccurs(t *testing.T) { + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 5000, + }, + } + + tfc := testFileConfig + tfc.CredentialSource = cs + + oldGetenv, oldNow, oldRunCommand := getenv, now, runCommand + defer func() { + getenv, now, runCommand = oldGetenv, oldNow, oldRunCommand + }() + + getenv = setEnvironment(map[string]string{"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + now = setTime(defaultTime) + deadline, deadlineSet := now(), false + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + deadline, deadlineSet = ctx.Deadline() + return nil, errors.New("foo") + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), executableError(errors.New("foo")).Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") } } func TestRetrieveExecutableSubjectTokenTimeoutOccurs(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -239,8 +285,8 @@ func TestRetrieveExecutableSubjectTokenTimeoutOccurs(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: executable command timed out."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), timeoutError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -252,7 +298,7 @@ func TestRetrieveExecutableSubjectTokenTimeoutOccurs(t *testing.T) { func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -283,8 +329,8 @@ func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Unable to parse response JSON."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), jsonParsingError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -296,7 +342,7 @@ func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -329,8 +375,8 @@ func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response missing version field."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), missingFieldError("version").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -342,7 +388,7 @@ func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -375,8 +421,8 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response missing success field."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), missingFieldError("success").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -388,7 +434,7 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -424,8 +470,8 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Executable returned unsuccessful response: (404) Token Not Found."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), userDefinedError("404", "Token Not Found").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -437,7 +483,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -472,8 +518,8 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response must include `error` and `message` fields when unsuccessful."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), malformedFailureError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -485,7 +531,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -520,8 +566,8 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response must include `error` and `message` fields when unsuccessful."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), malformedFailureError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -533,7 +579,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -567,8 +613,8 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response must include `error` and `message` fields when unsuccessful."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), malformedFailureError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -580,7 +626,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -614,8 +660,153 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Executable returned unsupported version: 2."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), unsupportedVersionError(2).Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } +} + +func TestRetrieveExecutableSubjectTokenMissingExpiration(t *testing.T) { + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 5000, + }, + } + + tfc := testFileConfig + tfc.CredentialSource = cs + + oldGetenv, oldNow, oldRunCommand := getenv, now, runCommand + defer func() { + getenv, now, runCommand = oldGetenv, oldNow, oldRunCommand + }() + + getenv = setEnvironment(map[string]string{"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + now = setTime(defaultTime) + deadline, deadlineSet := now(), false + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: Int(1), + TokenType: String("urn:ietf:params:oauth:token-type:jwt"), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), missingFieldError("expiration_time").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } +} + +func TestRetrieveExecutableSubjectTokenTokenTypeMissing(t *testing.T) { + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 5000, + }, + } + + tfc := testFileConfig + tfc.CredentialSource = cs + + oldGetenv, oldNow, oldRunCommand := getenv, now, runCommand + defer func() { + getenv, now, runCommand = oldGetenv, oldNow, oldRunCommand + }() + + getenv = setEnvironment(map[string]string{"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + now = setTime(defaultTime) + deadline, deadlineSet := now(), false + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix()), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), missingFieldError("token_type").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } +} + +func TestRetrieveExecutableSubjectTokenInvalidTokenType(t *testing.T) { + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 5000, + }, + } + + tfc := testFileConfig + tfc.CredentialSource = cs + + oldGetenv, oldNow, oldRunCommand := getenv, now, runCommand + defer func() { + getenv, now, runCommand = oldGetenv, oldNow, oldRunCommand + }() + + getenv = setEnvironment(map[string]string{"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + now = setTime(defaultTime) + deadline, deadlineSet := now(), false + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix()), + TokenType: String("urn:ietf:params:oauth:token-type:invalid"), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), tokenTypeError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -627,7 +818,7 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -663,8 +854,8 @@ func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: The token returned by the executable is expired."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), tokenExpiredError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -676,7 +867,7 @@ func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { func TestRetrieveExecutableSubjectTokenJwt(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -721,13 +912,13 @@ func TestRetrieveExecutableSubjectTokenJwt(t *testing.T) { } if got, want := out, "tokentokentoken"; got != want { - t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want) } } func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -763,8 +954,8 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response missing id_token field."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), missingFieldError("id_token").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet { @@ -776,7 +967,7 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { func TestRetrieveExecutableSubjectTokenIdToken(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -821,13 +1012,13 @@ func TestRetrieveExecutableSubjectTokenIdToken(t *testing.T) { } if got, want := out, "tokentokentoken"; got != want { - t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want) } } func TestRetrieveExecutableSubjectTokenSaml(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -872,13 +1063,13 @@ func TestRetrieveExecutableSubjectTokenSaml(t *testing.T) { } if got, want := out, "tokentokentoken"; got != want { - t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want) } } func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { cs := CredentialSource{ - Executable: ExecutableConfig{ + Executable: &ExecutableConfig{ Command: "blarg", TimeoutMillis: 5000, }, @@ -914,8 +1105,8 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Response missing saml_response field."; got != want { - t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + if got, want := err.Error(), missingFieldError("saml_response").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } if !deadlineSet {