From 9f3304e2ea8ab0d089bdbc8694a24fba5ba9813b Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Mon, 4 Apr 2022 13:18:00 -0700 Subject: [PATCH 1/9] google/internal/externalaccount: create executable credenitals --- .../externalaccount/basecredentials.go | 10 + .../externalaccount/executablecredsource.go | 217 ++++ .../executablecredsource_test.go | 926 ++++++++++++++++++ 3 files changed, 1153 insertions(+) create mode 100644 google/internal/externalaccount/executablecredsource.go create mode 100644 google/internal/externalaccount/executablecredsource_test.go diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 83ce9c2..49128f1 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -171,6 +171,8 @@ type CredentialSource struct { URL string `json:"url"` Headers map[string]string `json:"headers"` + Executable ExecutableConfig `json:"executable"` + EnvironmentID string `json:"environment_id"` RegionURL string `json:"region_url"` RegionalCredVerificationURL string `json:"regional_cred_verification_url"` @@ -179,6 +181,12 @@ type CredentialSource struct { Format format `json:"format"` } +type ExecutableConfig struct { + Command string `json:"command"` + TimeoutMillis int `json:"timeout_millis"` + OutputFile string `json:"output_file"` +} + // 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" { @@ -205,6 +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 } 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 new file mode 100644 index 0000000..79fb78f --- /dev/null +++ b/google/internal/externalaccount/executablecredsource.go @@ -0,0 +1,217 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "time" +) + +var ( + EXECUTABLE_SUPPORTED_MAX_VERSION = 1 +) + +var baseEnv = os.Environ + +// runCommand is basically an alias of exec.CommandContext for testing. +var runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + cmd := exec.CommandContext(ctx, command) + cmd.Env = env + + response, err := cmd.Output() + if ctx.Err() != nil { + return nil, ctx.Err() + } + return response, err +} + +type executableCredentialSource struct { + Command string + Timeout time.Duration + OutputFile string + ctx context.Context + config *Config +} + +// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig. +// It also performs defaulting and type conversions. +func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context.Context) (result executableCredentialSource) { + result.Command = ec.Command + if ec.TimeoutMillis == 0 { + result.Timeout = 30 * time.Second + } else { + result.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond + } + result.OutputFile = ec.OutputFile + result.ctx = ctx + result.config = config + return +} + +type subjectTokenResponse 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"` +} + +func parseSubjectToken(response []byte) (string, error) { + var result subjectTokenResponse + if err := json.Unmarshal(response, &result); err != nil { + return "", errors.New("oauth2/google: Unable to parse response JSON.") + } + + if result.Version == nil { + return "", errors.New("oauth2/google: Response missing version field.") + } + + if result.Success == nil { + return "", errors.New("oauth2/google: Response missing success field.") + } + + if !*result.Success { + details := "" + if result.Code != nil { + details += fmt.Sprintf("(%v)", *result.Code) + } + if result.Message != nil { + if details != "" { + details += " " + } + details += *result.Message + } + if details == "" { + details = "Unknown Error" + } + return "", fmt.Errorf("oauth2/google: Executable returned unsuccessful response: %v.", details) + } + + if *result.Version > EXECUTABLE_SUPPORTED_MAX_VERSION { + return "", fmt.Errorf("oauth2/google: Executable returned unsupported version: %v.", *result.Version) + } + + if result.ExpirationTime == nil { + return "", errors.New("oauth2/google: Response missing expiration_time field.") + } + + if result.TokenType == nil { + return "", errors.New("oauth2/google: Response missing token_type field.") + } + + if *result.ExpirationTime < now().Unix() { + return "", errors.New("oauth2/google: The token returned by the executable is expired.") + } + + 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 *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 *result.SamlResponse, nil + } + + return "", errors.New("Executable returned unsupported token type.") +} + +func (cs executableCredentialSource) subjectToken() (string, error) { + if token, ok := cs.getTokenFromInMemoryCaching(); ok { + return token, nil + } + + if token, ok := cs.getTokenFromOutputFile(); ok { + return token, nil + } + + return cs.getTokenFromExecutableCommand() +} + +func (cs executableCredentialSource) getTokenFromInMemoryCaching() (string, bool) { + // TODO + return "", false +} + +func (cs executableCredentialSource) getTokenFromOutputFile() (string, bool) { + // TODO + return "", false +} + +func (cs executableCredentialSource) getEnvironment() []string { + result := baseEnv() + for k, v := range cs.getNewEnvironmentVariables() { + result = append(result, fmt.Sprintf("%v=%v", k, v)) + } + return result +} + +var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken") + +func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]string { + result := map[string]string{ + "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience, + "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": cs.config.SubjectTokenType, + } + + if cs.config.ServiceAccountImpersonationURL != "" { + matches := serviceAccountImpersonationCompiler.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL) + if matches != nil { + result["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = matches[1] + } + } + + if cs.isInteractive() { + result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" + } else { + result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" + } + + if cs.OutputFile != "" { + result["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = cs.OutputFile + } + + return result +} + +func (cs executableCredentialSource) isInteractive() bool { + // Currently, executableCredentialSource does not yet support interactive mode. + return false +} + +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.") + } + + ctx, cancel := context.WithDeadline(cs.ctx, now().Add(cs.Timeout)) + defer cancel() + + if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil { + if err == context.DeadlineExceeded { + return "", fmt.Errorf("oauth2/google: executable command timed out.") + } + if exitError, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("oauth2/google: executable command failed with exit code %v.", exitError.ExitCode()) + } + return "", fmt.Errorf("oauth2/google: executable command failed: %v.", err.Error()) + } else { + return parseSubjectToken(output) + } +} diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go new file mode 100644 index 0000000..b682d81 --- /dev/null +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -0,0 +1,926 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" +) + +func Bool(b bool) *bool { + return &b +} + +func Int(i int) *int { + return &i +} + +func Int64(i int64) *int64 { + return &i +} + +func String(s string) *string { + return &s +} + +var emptyEnv = func() []string { + return []string{} +} + +func TestCreateExecutableCredential(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 50000, + } + + ecs := CreateExecutableCredential(ec, nil, context.Background()) + if ecs.Command != "blarg" { + t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") + } + if ecs.Timeout != 50000*time.Millisecond { + t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, 50000*time.Millisecond) + } +} + +func TestCreateExecutableCredential_WithoutTimeout(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + } + + ecs := CreateExecutableCredential(ec, nil, context.Background()) + if ecs.Command != "blarg" { + t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") + } + if ecs.Timeout != 30000*time.Millisecond { + t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, 30000*time.Millisecond) + } +} + +func areSlicesEquivalent(a, b []string) bool { + if len(a) != len(b) { + return false + } + +OUTER: + for i, aa := range a { + for _, bb := range b { + if aa == bb { + continue OUTER + } + } + return false + } + return true +} + +func TestMinimalExectuableCredentialGetEnvironment(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{ + Command: "blarg", + }, + }, + } + + ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + + oldBaseEnv := baseEnv + defer func() { baseEnv = oldBaseEnv }() + baseEnv = func() []string { + return []string{"A=B"} + } + + expectedEnvironment := []string{ + "A=B", + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", config.Audience), + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", config.SubjectTokenType), + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", + } + + if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { + t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestExectuableCredentialGetEnvironmentMalformedImpersonationUrl(t *testing.T) { + config := Config{ + Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", + 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{ + Command: "blarg", + OutputFile: "/path/to/generated/cached/credentials", + }, + }, + } + + ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + + oldBaseEnv := baseEnv + defer func() { baseEnv = oldBaseEnv }() + baseEnv = func() []string { + return []string{"A=B"} + } + + expectedEnvironment := []string{ + "A=B", + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", config.Audience), + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", config.SubjectTokenType), + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=test@project.iam.gserviceaccount.com", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", config.CredentialSource.Executable.OutputFile), + } + + if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { + t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + } +} +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{ + Command: "blarg", + OutputFile: "/path/to/generated/cached/credentials", + }, + }, + } + + ecs := CreateExecutableCredential(config.CredentialSource.Executable, &config, context.Background()) + + oldBaseEnv := baseEnv + defer func() { baseEnv = oldBaseEnv }() + baseEnv = func() []string { + return []string{"A=B"} + } + + expectedEnvironment := []string{ + "A=B", + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", config.Audience), + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", config.SubjectTokenType), + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", + fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", config.CredentialSource.Executable.OutputFile), + } + + if got, want := ecs.getEnvironment(), expectedEnvironment; !areSlicesEquivalent(got, want) { + t.Errorf("Incorrect environment received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveExecutableSubjectTokenWithoutEnvironmentVariablesSet(t *testing.T) { + cs := CredentialSource{ + Executable: ExecutableConfig{ + Command: "blarg", + TimeoutMillis: 5000, + }, + } + + tfc := testFileConfig + tfc.CredentialSource = cs + + oldGetenv := getenv + defer func() { getenv = oldGetenv }() + getenv = setEnvironment(map[string]string{}) + + 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(), "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) + } +} + +func TestRetrieveExecutableSubjectTokenTimeoutOccurs(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, context.DeadlineExceeded + } + + 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(), "oauth2/google: executable command timed out."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenInvalidFormat(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 []byte("tokentokentoken"), nil + } + + 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(), "oauth2/google: Unable to parse response JSON."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenMissingVersion(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(subjectTokenResponse{ + Success: Bool(true), + }) + } + + 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(), "oauth2/google: Response missing version field."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenMissingSuccess(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(subjectTokenResponse{ + Version: Int(1), + }) + } + + 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(), "oauth2/google: Response missing success field."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(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(subjectTokenResponse{ + Success: Bool(false), + Version: Int(1), + Code: String("404"), + Message: String("Token Not Found"), + }) + } + + 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(), "oauth2/google: Executable returned unsuccessful response: (404) Token Not Found."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(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(subjectTokenResponse{ + Success: Bool(false), + Version: Int(1), + Code: String("404"), + }) + } + + 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(), "oauth2/google: Executable returned unsuccessful response: (404)."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(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(subjectTokenResponse{ + Success: Bool(false), + Version: Int(1), + Message: String("Token Not Found"), + }) + } + + 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(), "oauth2/google: Executable returned unsuccessful response: Token Not Found."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(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(subjectTokenResponse{ + Success: Bool(false), + Version: Int(1), + }) + } + + 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(), "oauth2/google: Executable returned unsuccessful response: Unknown Error."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenNewerVersion(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(2), + }) + } + + 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(), "oauth2/google: Executable returned unsupported version: 2."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenExpired(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() - 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(), "oauth2/google: The token returned by the executable is expired."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenJwt(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() + 3600), + TokenType: String("urn:ietf:params:oauth:token-type:jwt"), + IdToken: String("tokentokentoken"), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() + 3600), + 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(), "oauth2/google: Response missing id_token field."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + 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 TestRetrieveExecutableSubjectTokenIdToken(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() + 3600), + TokenType: String("urn:ietf:params:oauth:token-type:id_token"), + IdToken: String("tokentokentoken"), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveExecutableSubjectTokenSaml(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() + 3600), + TokenType: String("urn:ietf:params:oauth:token-type:saml2"), + SamlResponse: String("tokentokentoken"), + }) + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveExecutableSubjectTokenSamlMissingResponse(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(subjectTokenResponse{ + Success: Bool(true), + Version: Int(1), + ExpirationTime: Int64(now().Unix() + 3600), + TokenType: String("urn:ietf:params:oauth:token-type:saml2"), + }) + } + + 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(), "oauth2/google: Response missing saml_response field."; got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } + + if !deadlineSet { + t.Errorf("Command run without a deadline") + } else if deadline != now().Add(5*time.Second) { + t.Errorf("Command run with incorrect deadline") + } +} From 1c93c2e4af99c27c560dc587cb1a5bc7181e6e7b Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Tue, 19 Apr 2022 12:22:53 -0700 Subject: [PATCH 2/9] Changes requested by @lsirac --- .../externalaccount/executablecredsource.go | 24 ++-------------- .../executablecredsource_test.go | 28 +++++++++---------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 79fb78f..2576dfd 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -56,7 +56,7 @@ func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context return } -type subjectTokenResponse struct { +type executableResponse struct { Version *int `json:"version"` Success *bool `json:"success"` TokenType *string `json:"token_type"` @@ -68,7 +68,7 @@ type subjectTokenResponse struct { } func parseSubjectToken(response []byte) (string, error) { - var result subjectTokenResponse + var result executableResponse if err := json.Unmarshal(response, &result); err != nil { return "", errors.New("oauth2/google: Unable to parse response JSON.") } @@ -132,10 +132,6 @@ func parseSubjectToken(response []byte) (string, error) { } func (cs executableCredentialSource) subjectToken() (string, error) { - if token, ok := cs.getTokenFromInMemoryCaching(); ok { - return token, nil - } - if token, ok := cs.getTokenFromOutputFile(); ok { return token, nil } @@ -143,11 +139,6 @@ func (cs executableCredentialSource) subjectToken() (string, error) { return cs.getTokenFromExecutableCommand() } -func (cs executableCredentialSource) getTokenFromInMemoryCaching() (string, bool) { - // TODO - return "", false -} - func (cs executableCredentialSource) getTokenFromOutputFile() (string, bool) { // TODO return "", false @@ -176,11 +167,7 @@ func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]str } } - if cs.isInteractive() { - result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" - } else { - result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" - } + result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" if cs.OutputFile != "" { result["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = cs.OutputFile @@ -189,11 +176,6 @@ func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]str return result } -func (cs executableCredentialSource) isInteractive() bool { - // Currently, executableCredentialSource does not yet support interactive mode. - return false -} - 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" { diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index b682d81..6f9afde 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -67,7 +67,7 @@ func areSlicesEquivalent(a, b []string) bool { } OUTER: - for i, aa := range a { + for _, aa := range a { for _, bb := range b { if aa == bb { continue OUTER @@ -315,7 +315,7 @@ func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), }) } @@ -361,7 +361,7 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Version: Int(1), }) } @@ -407,7 +407,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), Code: String("404"), @@ -456,7 +456,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), Code: String("404"), @@ -504,7 +504,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), Message: String("Token Not Found"), @@ -552,7 +552,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), }) @@ -599,7 +599,7 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(2), }) @@ -646,7 +646,7 @@ func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() - 1), @@ -695,7 +695,7 @@ func TestRetrieveExecutableSubjectTokenJwt(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() + 3600), @@ -746,7 +746,7 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() + 3600), @@ -795,7 +795,7 @@ func TestRetrieveExecutableSubjectTokenIdToken(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() + 3600), @@ -846,7 +846,7 @@ func TestRetrieveExecutableSubjectTokenSaml(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() + 3600), @@ -897,7 +897,7 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { deadline, deadlineSet := now(), false runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() - return json.Marshal(subjectTokenResponse{ + return json.Marshal(executableResponse{ Success: Bool(true), Version: Int(1), ExpirationTime: Int64(now().Unix() + 3600), From 13e7453c5ba13537741e948aa785d2ea9cc4bc48 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Thu, 21 Apr 2022 08:28:25 -0700 Subject: [PATCH 3/9] Changes requested by @lsirac --- .../externalaccount/executablecredsource.go | 20 +++++-------------- .../executablecredsource_test.go | 14 ++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 2576dfd..90e8117 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -63,8 +63,8 @@ type executableResponse struct { ExpirationTime *int64 `json:"expiration_time"` IdToken *string `json:"id_token"` SamlResponse *string `json:"saml_response"` - Code *string `json:"code"` - Message *string `json:"message"` + Code string `json:"code"` + Message string `json:"message"` } func parseSubjectToken(response []byte) (string, error) { @@ -82,20 +82,10 @@ func parseSubjectToken(response []byte) (string, error) { } if !*result.Success { - details := "" - if result.Code != nil { - details += fmt.Sprintf("(%v)", *result.Code) + if result.Code == "" || result.Message == "" { + return "", errors.New("oauth2/google: Response must include `error` and `message` fields when unsuccessful.") } - if result.Message != nil { - if details != "" { - details += " " - } - details += *result.Message - } - if details == "" { - details = "Unknown Error" - } - return "", fmt.Errorf("oauth2/google: Executable returned unsuccessful response: %v.", details) + return "", fmt.Errorf("oauth2/google: Executable returned unsuccessful response: (%v) %v.", result.Code, result.Message) } if *result.Version > EXECUTABLE_SUPPORTED_MAX_VERSION { diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 6f9afde..fec3720 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -410,8 +410,8 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), - Code: String("404"), - Message: String("Token Not Found"), + Code: "404", + Message: "Token Not Found", }) } @@ -459,7 +459,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), - Code: String("404"), + Code: "404", }) } @@ -472,7 +472,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Executable returned unsuccessful response: (404)."; got != want { + 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) } @@ -507,7 +507,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin return json.Marshal(executableResponse{ Success: Bool(false), Version: Int(1), - Message: String("Token Not Found"), + Message: "Token Not Found", }) } @@ -520,7 +520,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Executable returned unsuccessful response: Token Not Found."; got != want { + 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) } @@ -567,7 +567,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Executable returned unsuccessful response: Unknown Error."; got != want { + 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) } From ec7aeb6565808c266cdc69c5da41fdcaa6f2a8fc Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Fri, 22 Apr 2022 10:00:51 -0700 Subject: [PATCH 4/9] Changes requested by @codyoss --- .../externalaccount/basecredentials.go | 8 +- .../externalaccount/executablecredsource.go | 102 ++++-- .../executablecredsource_test.go | 303 ++++++++++++++---- 3 files changed, 325 insertions(+), 88 deletions(-) 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 { From 922b64a39cc4a5ceb0a685bf10352fcd7473e13e Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Fri, 22 Apr 2022 10:32:32 -0700 Subject: [PATCH 5/9] Changes requested by @codyoss --- .../externalaccount/executablecredsource.go | 23 +++-- .../executablecredsource_test.go | 89 ------------------- 2 files changed, 15 insertions(+), 97 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 4651c31..d206eb5 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -76,7 +76,20 @@ var runCommand = func(ctx context.Context, command string, env []string) ([]byte if ctx.Err() != nil { return nil, ctx.Err() } - return response, err + + if err == nil { + return response, nil + } + + if err == context.DeadlineExceeded { + return []byte{}, timeoutError() + } + + if exitError, ok := err.(*exec.ExitError); ok { + return []byte{}, exitCodeError(exitError.ExitCode()) + } + + return []byte{}, executableError(err) } type executableCredentialSource struct { @@ -222,13 +235,7 @@ func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, er defer cancel() if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil { - if err == context.DeadlineExceeded { - return "", timeoutError() - } - if exitError, ok := err.(*exec.ExitError); ok { - return "", exitCodeError(exitError.ExitCode()) - } - return "", executableError(err) + return "", err } else { return parseSubjectToken(output) } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 9abb276..1ae6dc3 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -7,7 +7,6 @@ package externalaccount import ( "context" "encoding/json" - "errors" "fmt" "testing" "time" @@ -208,94 +207,6 @@ func TestRetrieveExecutableSubjectTokenWithoutEnvironmentVariablesSet(t *testing } } -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{ - 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, context.DeadlineExceeded - } - - 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(), timeoutError().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 TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ From 8e4e7dc9f8503025728c07c540b2edc6a17f1841 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Fri, 22 Apr 2022 10:37:47 -0700 Subject: [PATCH 6/9] Changes requested by @codyoss --- google/internal/externalaccount/basecredentials.go | 2 +- .../internal/externalaccount/executablecredsource.go | 2 +- .../externalaccount/executablecredsource_test.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index e97daa4..45cb7cb 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -214,7 +214,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } 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 != nil { - return CreateExecutableCredential(*c.CredentialSource.Executable, c, ctx), nil + return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c), 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 d206eb5..0e5c11b 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -102,7 +102,7 @@ type executableCredentialSource struct { // CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig. // It also performs defaulting and type conversions. -func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context.Context) (result executableCredentialSource) { +func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (result executableCredentialSource) { result.Command = ec.Command if ec.TimeoutMillis == 0 { result.Timeout = defaultTimeout diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 1ae6dc3..d935513 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -38,7 +38,7 @@ func TestCreateExecutableCredential(t *testing.T) { TimeoutMillis: 50000, } - ecs := CreateExecutableCredential(ec, nil, context.Background()) + ecs := CreateExecutableCredential(context.Background(), &ec, nil) if ecs.Command != "blarg" { t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") } @@ -52,7 +52,7 @@ func TestCreateExecutableCredential_WithoutTimeout(t *testing.T) { Command: "blarg", } - ecs := CreateExecutableCredential(ec, nil, context.Background()) + ecs := CreateExecutableCredential(context.Background(), &ec, nil) if ecs.Command != "blarg" { t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") } @@ -89,7 +89,7 @@ func TestMinimalExecutableCredentialGetEnvironment(t *testing.T) { }, } - ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -122,7 +122,7 @@ func TestExectuableCredentialGetEnvironmentMalformedImpersonationUrl(t *testing. }, } - ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -157,7 +157,7 @@ func TestExectuableCredentialGetEnvironment(t *testing.T) { }, } - ecs := CreateExecutableCredential(*config.CredentialSource.Executable, &config, context.Background()) + ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() From 6c2bc272c44bab4091c5fe96031ef61bf2fc5d1c Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Fri, 22 Apr 2022 10:45:22 -0700 Subject: [PATCH 7/9] Changes requested by @codyoss --- .../internal/externalaccount/executablecredsource.go | 10 +++------- .../externalaccount/executablecredsource_test.go | 4 ---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 0e5c11b..09dd7fb 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -15,6 +15,8 @@ import ( "time" ) +var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken") + const ( executableSupportedMaxVersion = 1 defaultTimeout = 30 * time.Second @@ -57,7 +59,7 @@ func exitCodeError(exitCode int) error { } func executableError(err error) error { - return fmt.Errorf("oauth2/google: executable command failed: %v", err.Error()) + return fmt.Errorf("oauth2/google: executable command failed: %v", err) } func executablesDisallowedError() error { @@ -73,10 +75,6 @@ var runCommand = func(ctx context.Context, command string, env []string) ([]byte cmd.Env = env response, err := cmd.Output() - if ctx.Err() != nil { - return nil, ctx.Err() - } - if err == nil { return response, nil } @@ -201,8 +199,6 @@ func (cs executableCredentialSource) getEnvironment() []string { return result } -var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken") - func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]string { result := map[string]string{ "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience, diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index d935513..1d6f66e 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -28,10 +28,6 @@ func String(s string) *string { return &s } -var emptyEnv = func() []string { - return []string{} -} - func TestCreateExecutableCredential(t *testing.T) { ec := ExecutableConfig{ Command: "blarg", From 2984186a39321766f55e24a10e7ee41ebc6ef4d4 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Fri, 22 Apr 2022 15:21:36 -0700 Subject: [PATCH 8/9] Changes requested by @lsirac --- .../externalaccount/basecredentials.go | 4 +- .../externalaccount/executablecredsource.go | 25 +++- .../executablecredsource_test.go | 129 ++++++++++++++---- 3 files changed, 128 insertions(+), 30 deletions(-) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 45cb7cb..4251c44 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -183,7 +183,7 @@ type CredentialSource struct { type ExecutableConfig struct { Command string `json:"command"` - TimeoutMillis int `json:"timeout_millis"` + TimeoutMillis *int `json:"timeout_millis"` OutputFile string `json:"output_file"` } @@ -214,7 +214,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } 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 != nil { - return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c), nil + return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c) } 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 09dd7fb..e43f950 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -20,6 +20,8 @@ var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredent const ( executableSupportedMaxVersion = 1 defaultTimeout = 30 * time.Second + timeoutMinimum = 5 * time.Second + timeoutMaximum = 120 * time.Second ) func missingFieldError(field string) error { @@ -63,7 +65,15 @@ func executableError(err error) error { } func executablesDisallowedError() error { - return errors.New("Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") + return errors.New("oauth2/google: Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") +} + +func timeoutRangeError() error { + return errors.New("oauth2/google: Invalid `timeout_millis` field. Executable timeout must be between 5 and 120 seconds.") +} + +func commandMissingError() error { + return errors.New("oauth2/google: Missing `command` field. Executable command must be provided.") } // baseEnv is an alias of os.Environ used for testing @@ -100,12 +110,19 @@ type executableCredentialSource struct { // CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig. // It also performs defaulting and type conversions. -func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (result executableCredentialSource) { +func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (result executableCredentialSource, err error) { + if ec.Command == "" { + err = commandMissingError() + } result.Command = ec.Command - if ec.TimeoutMillis == 0 { + if ec.TimeoutMillis == nil { result.Timeout = defaultTimeout } else { - result.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond + result.Timeout = time.Duration(*ec.TimeoutMillis) * time.Millisecond + if result.Timeout < timeoutMinimum || result.Timeout > timeoutMaximum { + err = timeoutRangeError() + return + } } result.OutputFile = ec.OutputFile result.ctx = ctx diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 1d6f66e..3b29201 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -31,10 +31,13 @@ func String(s string) *string { func TestCreateExecutableCredential(t *testing.T) { ec := ExecutableConfig{ Command: "blarg", - TimeoutMillis: 50000, + TimeoutMillis: Int(50000), } - ecs := CreateExecutableCredential(context.Background(), &ec, nil) + ecs, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err != nil { + t.Fatalf("creation failed %v", err) + } if ecs.Command != "blarg" { t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") } @@ -48,7 +51,10 @@ func TestCreateExecutableCredential_WithoutTimeout(t *testing.T) { Command: "blarg", } - ecs := CreateExecutableCredential(context.Background(), &ec, nil) + ecs, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err != nil { + t.Fatalf("creation failed %v", err) + } if ecs.Command != "blarg" { t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") } @@ -57,6 +63,72 @@ func TestCreateExecutableCredential_WithoutTimeout(t *testing.T) { } } +func TestCreateExectuableCredential_WithoutCommand(t *testing.T) { + ec := ExecutableConfig{} + + _, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), commandMissingError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestCreateExectuableCredential_TimeoutTooLow(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(4999), + } + + _, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), timeoutRangeError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestCreateExectuableCredential_TimeoutLow(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + } + + _, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err != nil { + t.Fatalf("creation failed %v", err) + } +} + +func TestCreateExectuableCredential_TimeoutHigh(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(120000), + } + + _, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err != nil { + t.Fatalf("creation failed %v", err) + } +} + +func TestCreateExectuableCredential_TimeoutTooHigh(t *testing.T) { + ec := ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(120001), + } + + _, err := CreateExecutableCredential(context.Background(), &ec, nil) + if err == nil { + t.Fatalf("Expected error but found none") + } + if got, want := err.Error(), timeoutRangeError().Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + func areSlicesEquivalent(a, b []string) bool { if len(a) != len(b) { return false @@ -85,7 +157,10 @@ func TestMinimalExecutableCredentialGetEnvironment(t *testing.T) { }, } - ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + if err != nil { + t.Fatalf("creation failed %v", err) + } oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -118,7 +193,10 @@ func TestExectuableCredentialGetEnvironmentMalformedImpersonationUrl(t *testing. }, } - ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + if err != nil { + t.Fatalf("creation failed %v", err) + } oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -153,7 +231,10 @@ func TestExectuableCredentialGetEnvironment(t *testing.T) { }, } - ecs := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + if err != nil { + t.Fatalf("creation failed %v", err) + } oldBaseEnv := baseEnv defer func() { baseEnv = oldBaseEnv }() @@ -178,7 +259,7 @@ func TestRetrieveExecutableSubjectTokenWithoutEnvironmentVariablesSet(t *testing cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -207,7 +288,7 @@ func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -251,7 +332,7 @@ func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -297,7 +378,7 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -343,7 +424,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -392,7 +473,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -440,7 +521,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -488,7 +569,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -535,7 +616,7 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -582,7 +663,7 @@ func TestRetrieveExecutableSubjectTokenMissingExpiration(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -630,7 +711,7 @@ func TestRetrieveExecutableSubjectTokenTokenTypeMissing(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -678,7 +759,7 @@ func TestRetrieveExecutableSubjectTokenInvalidTokenType(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -727,7 +808,7 @@ func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -776,7 +857,7 @@ func TestRetrieveExecutableSubjectTokenJwt(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -827,7 +908,7 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -876,7 +957,7 @@ func TestRetrieveExecutableSubjectTokenIdToken(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -927,7 +1008,7 @@ func TestRetrieveExecutableSubjectTokenSaml(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } @@ -978,7 +1059,7 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { cs := CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", - TimeoutMillis: 5000, + TimeoutMillis: Int(5000), }, } From 50e0865f50c010de878f8eda60dce832be97f528 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Mon, 25 Apr 2022 09:34:42 -0700 Subject: [PATCH 9/9] Changes requested by @codyoss --- .../externalaccount/executablecredsource.go | 59 ++++++++-------- .../executablecredsource_test.go | 68 +++++++++---------- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index e43f950..6fbb70e 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -65,15 +65,15 @@ func executableError(err error) error { } func executablesDisallowedError() error { - return errors.New("oauth2/google: Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") + return errors.New("oauth2/google: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") } func timeoutRangeError() error { - return errors.New("oauth2/google: Invalid `timeout_millis` field. Executable timeout must be between 5 and 120 seconds.") + return errors.New("oauth2/google: invalid `timeout_millis` field. Executable timeout must be between 5 and 120 seconds") } func commandMissingError() error { - return errors.New("oauth2/google: Missing `command` field. Executable command must be provided.") + return errors.New("oauth2/google: missing `command` field. Executable command must be provided") } // baseEnv is an alias of os.Environ used for testing @@ -90,14 +90,14 @@ var runCommand = func(ctx context.Context, command string, env []string) ([]byte } if err == context.DeadlineExceeded { - return []byte{}, timeoutError() + return nil, timeoutError() } if exitError, ok := err.(*exec.ExitError); ok { - return []byte{}, exitCodeError(exitError.ExitCode()) + return nil, exitCodeError(exitError.ExitCode()) } - return []byte{}, executableError(err) + return nil, executableError(err) } type executableCredentialSource struct { @@ -131,14 +131,14 @@ func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, confi } type executableResponse struct { - 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"` + 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) { @@ -147,7 +147,7 @@ func parseSubjectToken(response []byte) (string, error) { return "", jsonParsingError() } - if result.Version == nil { + if result.Version == 0 { return "", missingFieldError("version") } @@ -162,34 +162,34 @@ func parseSubjectToken(response []byte) (string, error) { return "", userDefinedError(result.Code, result.Message) } - if *result.Version > executableSupportedMaxVersion { - return "", unsupportedVersionError(*result.Version) + if result.Version > executableSupportedMaxVersion || result.Version < 0 { + return "", unsupportedVersionError(result.Version) } - if result.ExpirationTime == nil { + if result.ExpirationTime == 0 { return "", missingFieldError("expiration_time") } - if result.TokenType == nil { + if result.TokenType == "" { return "", missingFieldError("token_type") } - if *result.ExpirationTime < now().Unix() { + if result.ExpirationTime < now().Unix() { 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 { + if result.TokenType == "urn:ietf:params:oauth:token-type:jwt" || result.TokenType == "urn:ietf:params:oauth:token-type:id_token" { + if result.IdToken == "" { return "", missingFieldError("id_token") } - return *result.IdToken, nil + return result.IdToken, nil } - if *result.TokenType == "urn:ietf:params:oauth:token-type:saml2" { - if result.SamlResponse == nil { + if result.TokenType == "urn:ietf:params:oauth:token-type:saml2" { + if result.SamlResponse == "" { return "", missingFieldError("saml_response") } - return *result.SamlResponse, nil + return result.SamlResponse, nil } return "", tokenTypeError() @@ -218,8 +218,9 @@ func (cs executableCredentialSource) getEnvironment() []string { func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]string { result := map[string]string{ - "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience, - "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": cs.config.SubjectTokenType, + "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience, + "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": cs.config.SubjectTokenType, + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "0", } if cs.config.ServiceAccountImpersonationURL != "" { @@ -229,8 +230,6 @@ func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]str } } - result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" - if cs.OutputFile != "" { result["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = cs.OutputFile } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 3b29201..59f1e82 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -396,7 +396,7 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ - Version: Int(1), + Version: 1, }) } @@ -443,7 +443,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(false), - Version: Int(1), + Version: 1, Code: "404", Message: "Token Not Found", }) @@ -492,7 +492,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(false), - Version: Int(1), + Version: 1, Code: "404", }) } @@ -540,7 +540,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(false), - Version: Int(1), + Version: 1, Message: "Token Not Found", }) } @@ -588,7 +588,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(false), - Version: Int(1), + Version: 1, }) } @@ -635,7 +635,7 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(2), + Version: 2, }) } @@ -682,8 +682,8 @@ func TestRetrieveExecutableSubjectTokenMissingExpiration(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - TokenType: String("urn:ietf:params:oauth:token-type:jwt"), + Version: 1, + TokenType: "urn:ietf:params:oauth:token-type:jwt", }) } @@ -730,8 +730,8 @@ func TestRetrieveExecutableSubjectTokenTokenTypeMissing(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix()), + Version: 1, + ExpirationTime: now().Unix(), }) } @@ -778,9 +778,9 @@ func TestRetrieveExecutableSubjectTokenInvalidTokenType(t *testing.T) { 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"), + Version: 1, + ExpirationTime: now().Unix(), + TokenType: "urn:ietf:params:oauth:token-type:invalid", }) } @@ -827,9 +827,9 @@ func TestRetrieveExecutableSubjectTokenExpired(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() - 1), - TokenType: String("urn:ietf:params:oauth:token-type:jwt"), + Version: 1, + ExpirationTime: now().Unix() - 1, + TokenType: "urn:ietf:params:oauth:token-type:jwt", }) } @@ -876,10 +876,10 @@ func TestRetrieveExecutableSubjectTokenJwt(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() + 3600), - TokenType: String("urn:ietf:params:oauth:token-type:jwt"), - IdToken: String("tokentokentoken"), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", }) } @@ -927,9 +927,9 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() + 3600), - TokenType: String("urn:ietf:params:oauth:token-type:jwt"), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", }) } @@ -976,10 +976,10 @@ func TestRetrieveExecutableSubjectTokenIdToken(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() + 3600), - TokenType: String("urn:ietf:params:oauth:token-type:id_token"), - IdToken: String("tokentokentoken"), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:id_token", + IdToken: "tokentokentoken", }) } @@ -1027,10 +1027,10 @@ func TestRetrieveExecutableSubjectTokenSaml(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() + 3600), - TokenType: String("urn:ietf:params:oauth:token-type:saml2"), - SamlResponse: String("tokentokentoken"), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:saml2", + SamlResponse: "tokentokentoken", }) } @@ -1078,9 +1078,9 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { deadline, deadlineSet = ctx.Deadline() return json.Marshal(executableResponse{ Success: Bool(true), - Version: Int(1), - ExpirationTime: Int64(now().Unix() + 3600), - TokenType: String("urn:ietf:params:oauth:token-type:saml2"), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:saml2", }) }