From f83d1aae67d70688977be83b8b44a3efa15f3ce9 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Thu, 21 Apr 2022 08:05:51 -0700 Subject: [PATCH 1/4] Add Output File --- .../externalaccount/executablecredsource.go | 102 +- .../executablecredsource_test.go | 930 +++++++++++++++++- 2 files changed, 988 insertions(+), 44 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 6fbb70e..9d35f6d 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -9,6 +9,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "io/ioutil" "os" "os/exec" "regexp" @@ -22,34 +24,43 @@ const ( defaultTimeout = 30 * time.Second timeoutMinimum = 5 * time.Second timeoutMaximum = 120 * time.Second + executableSource = "response" + outputFileSource = "output file" ) -func missingFieldError(field string) error { - return fmt.Errorf("oauth2/google: response missing `%v` field", field) +func missingFieldError(source, field string) error { + return fmt.Errorf("oauth2/google: %v missing `%v` field", source, field) } -func jsonParsingError() error { - return errors.New("oauth2/google: unable to parse response JSON") +func jsonParsingError(source string) error { + return fmt.Errorf("oauth2/google: unable to parse %v JSON", source) } -func malformedFailureError() error { - return errors.New("oauth2/google: response must include `error` and `message` fields when unsuccessful") +func malformedFailureError(source string) error { + return fmt.Errorf("oauth2/google: %v must include `error` and `message` fields when unsuccessful", source) } -func userDefinedError(code, message string) error { - return fmt.Errorf("oauth2/google: executable returned unsuccessful response: (%v) %v", code, message) +func userDefinedError(source, code, message string) error { + return fmt.Errorf("oauth2/google: %v contains unsuccessful response: (%v) %v", source, code, message) } -func unsupportedVersionError(version int) error { - return fmt.Errorf("oauth2/google: executable returned unsupported version: %v", version) +func unsupportedVersionError(source string, version int) error { + return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version) +} + +type timeoutException struct { +} + +func (t timeoutException) Error() string { + return "oauth2/google: the token returned by the executable is expired" } func tokenExpiredError() error { - return errors.New("oauth2/google: the token returned by the executable is expired") + return timeoutException{} } -func tokenTypeError() error { - return errors.New("oauth2/google: executable returned unsupported token type") +func tokenTypeError(source string) error { + return fmt.Errorf("oauth2/google: %v contains unsupported token type", source) } func timeoutError() error { @@ -141,37 +152,37 @@ type executableResponse struct { Message string `json:"message,omitempty"` } -func parseSubjectToken(response []byte) (string, error) { +func parseSubjectTokenFromSource(response []byte, source string) (string, error) { var result executableResponse if err := json.Unmarshal(response, &result); err != nil { - return "", jsonParsingError() + return "", jsonParsingError(source) } if result.Version == 0 { - return "", missingFieldError("version") + return "", missingFieldError(source, "version") } if result.Success == nil { - return "", missingFieldError("success") + return "", missingFieldError(source, "success") } if !*result.Success { if result.Code == "" || result.Message == "" { - return "", malformedFailureError() + return "", malformedFailureError(source) } - return "", userDefinedError(result.Code, result.Message) + return "", userDefinedError(source, result.Code, result.Message) } if result.Version > executableSupportedMaxVersion || result.Version < 0 { - return "", unsupportedVersionError(result.Version) + return "", unsupportedVersionError(source, result.Version) } if result.ExpirationTime == 0 { - return "", missingFieldError("expiration_time") + return "", missingFieldError(source, "expiration_time") } if result.TokenType == "" { - return "", missingFieldError("token_type") + return "", missingFieldError(source, "token_type") } if result.ExpirationTime < now().Unix() { @@ -180,32 +191,61 @@ func parseSubjectToken(response []byte) (string, error) { 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 "", missingFieldError(source, "id_token") } return result.IdToken, nil } if result.TokenType == "urn:ietf:params:oauth:token-type:saml2" { if result.SamlResponse == "" { - return "", missingFieldError("saml_response") + return "", missingFieldError(source, "saml_response") } return result.SamlResponse, nil } - return "", tokenTypeError() + return "", tokenTypeError(source) } func (cs executableCredentialSource) subjectToken() (string, error) { - if token, ok := cs.getTokenFromOutputFile(); ok { - return token, nil + if token, err, ok := cs.getTokenFromOutputFile(); ok { + return token, err } return cs.getTokenFromExecutableCommand() } -func (cs executableCredentialSource) getTokenFromOutputFile() (string, bool) { - // TODO - return "", false +func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bool) { + if cs.OutputFile == "" { + // This ExecutableCredentialSource doesn't use an OutputFile + return "", nil, false + } + + file, err := os.Open(cs.OutputFile) + if err != nil { + // No OutputFile found. Hasn't been created yet, so skip it. + return "", nil, false + } + + data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20)) + if err != nil { + // An error reading the file. Not necessarily under the developer's control, so ignore it + return "", nil, false + } + + token, err := parseSubjectTokenFromSource(data, outputFileSource) + + if err == nil { + // Token parsing succeeded. Use found token. + return token, nil, true + } + + if _, ok := err.(timeoutException); ok { + // Cached token expired. Go through regular flow to find new token. + return "", nil, false + } + + // There was an error in the cached token, and the developer should be aware of it. + return "", err, true } func (cs executableCredentialSource) getEnvironment() []string { @@ -249,6 +289,6 @@ func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, er if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil { return "", err } else { - return parseSubjectToken(output) + return parseSubjectTokenFromSource(output, executableSource) } } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 59f1e82..3ba7a92 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -8,6 +8,8 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" "testing" "time" ) @@ -317,7 +319,7 @@ func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), jsonParsingError().Error(); got != want { + if got, want := err.Error(), jsonParsingError(executableSource).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -363,7 +365,7 @@ func TestRetrieveExecutableSubjectTokenMissingVersion(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("version").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "version").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -409,7 +411,7 @@ func TestRetrieveExecutableSubjectTokenMissingSuccess(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("success").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "success").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -458,7 +460,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), userDefinedError("404", "Token Not Found").Error(); got != want { + if got, want := err.Error(), userDefinedError(executableSource, "404", "Token Not Found").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -506,7 +508,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError().Error(); got != want { + if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -554,7 +556,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError().Error(); got != want { + if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -601,7 +603,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError().Error(); got != want { + if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -648,7 +650,7 @@ func TestRetrieveExecutableSubjectTokenNewerVersion(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), unsupportedVersionError(2).Error(); got != want { + if got, want := err.Error(), unsupportedVersionError(executableSource, 2).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -696,7 +698,7 @@ func TestRetrieveExecutableSubjectTokenMissingExpiration(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("expiration_time").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "expiration_time").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -744,7 +746,7 @@ func TestRetrieveExecutableSubjectTokenTokenTypeMissing(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("token_type").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "token_type").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -793,7 +795,7 @@ func TestRetrieveExecutableSubjectTokenInvalidTokenType(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), tokenTypeError().Error(); got != want { + if got, want := err.Error(), tokenTypeError(executableSource).Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -942,7 +944,7 @@ func TestRetrieveExecutableSubjectTokenJwtMissingIdToken(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("id_token").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "id_token").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -1093,7 +1095,7 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), missingFieldError("saml_response").Error(); got != want { + if got, want := err.Error(), missingFieldError(executableSource, "saml_response").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -1103,3 +1105,905 @@ func TestRetrieveExecutableSubjectTokenSamlMissingResponse(t *testing.T) { t.Errorf("Command run with incorrect deadline") } } + +func TestRetrieveOutputFileSubjectTokenInvalidFormat(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if _, err = outputFile.Write([]byte("tokentokentoken")); err != nil { + t.Fatalf("error writing to file: %v", err) + } + + 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(), jsonParsingError(outputFileSource).Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenMissingVersion(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "version").Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenMissingSuccess(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Version: 1, + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "success").Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithFields(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(false), + Version: 1, + Code: "404", + Message: "Token Not Found", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(), userDefinedError(outputFileSource, "404", "Token Not Found").Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithCode(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(false), + Version: 1, + Code: "404", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(), malformedFailureError(outputFileSource).Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithMessage(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(false), + Version: 1, + Message: "Token Not Found", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(), malformedFailureError(outputFileSource).Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithoutFields(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(false), + Version: 1, + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(), malformedFailureError(outputFileSource).Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenNewerVersion(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 2, + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(), unsupportedVersionError(outputFileSource, 2).Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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 got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenJwtMissingIdToken(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "id_token").Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenIdToken(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:id_token", + IdToken: "tokentokentoken", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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 got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenSaml(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:saml2", + SamlResponse: "tokentokentoken", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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 got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenSamlMissingResponse(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:saml2", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "saml_response").Error(); got != want { + t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) + } +} + +func TestRetrieveOutputFileSubjectTokenMissingExpiration(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "expiration_time").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestRetrieveOutputFileSubjectTokenTokenTypeMissing(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix(), + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource, "token_type").Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestRetrieveOutputFileSubjectTokenInvalidTokenType(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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) + runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) { + t.Fatalf("Executable called when it should not have been") + return []byte{}, nil + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix(), + TokenType: "urn:ietf:params:oauth:token-type:invalid", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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(outputFileSource).Error(); got != want { + t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) + } +} + +func TestRetrieveOutputFileSubjectTokenExpired(t *testing.T) { + outputFile, err := ioutil.TempFile("testdata", "result.*.json") + if err != nil { + t.Fatalf("Tempfile failed: %v", err) + } + defer os.Remove(outputFile.Name()) + + cs := CredentialSource{ + Executable: &ExecutableConfig{ + Command: "blarg", + TimeoutMillis: Int(5000), + OutputFile: outputFile.Name(), + }, + } + + 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: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }) + } + + if err = json.NewEncoder(outputFile).Encode(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() - 1, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + }); err != nil { + t.Fatalf("Error encoding to file: %v", err) + } + + 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) + } +} From d19f296b5953dbfa0d2c3d3c303081764f36aa30 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Wed, 27 Apr 2022 11:27:17 -0700 Subject: [PATCH 2/4] Update Output File behavior to match spec --- .../externalaccount/executablecredsource.go | 44 ++++--- .../executablecredsource_test.go | 124 +++++++++++++----- 2 files changed, 113 insertions(+), 55 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 9d35f6d..8709c08 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -28,35 +28,36 @@ const ( outputFileSource = "output file" ) +type nonCacheableError struct { + message string +} + +func (nce nonCacheableError) Error() string { + return nce.message +} + func missingFieldError(source, field string) error { return fmt.Errorf("oauth2/google: %v missing `%v` field", source, field) } -func jsonParsingError(source string) error { - return fmt.Errorf("oauth2/google: unable to parse %v JSON", source) +func jsonParsingError(source, data string) error { + return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data) } -func malformedFailureError(source string) error { - return fmt.Errorf("oauth2/google: %v must include `error` and `message` fields when unsuccessful", source) +func malformedFailureError() error { + return nonCacheableError{"oauth2/google: response must include `error` and `message` fields when unsuccessful"} } -func userDefinedError(source, code, message string) error { - return fmt.Errorf("oauth2/google: %v contains unsuccessful response: (%v) %v", source, code, message) +func userDefinedError(code, message string) error { + return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", code, message)} } func unsupportedVersionError(source string, version int) error { return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version) } -type timeoutException struct { -} - -func (t timeoutException) Error() string { - return "oauth2/google: the token returned by the executable is expired" -} - func tokenExpiredError() error { - return timeoutException{} + return nonCacheableError{"oauth2/google: the token returned by the executable is expired"} } func tokenTypeError(source string) error { @@ -155,7 +156,7 @@ type executableResponse struct { func parseSubjectTokenFromSource(response []byte, source string) (string, error) { var result executableResponse if err := json.Unmarshal(response, &result); err != nil { - return "", jsonParsingError(source) + return "", jsonParsingError(source, string(response)) } if result.Version == 0 { @@ -168,9 +169,9 @@ func parseSubjectTokenFromSource(response []byte, source string) (string, error) if !*result.Success { if result.Code == "" || result.Message == "" { - return "", malformedFailureError(source) + return "", malformedFailureError() } - return "", userDefinedError(source, result.Code, result.Message) + return "", userDefinedError(result.Code, result.Message) } if result.Version > executableSupportedMaxVersion || result.Version < 0 { @@ -227,8 +228,8 @@ func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bo } data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20)) - if err != nil { - // An error reading the file. Not necessarily under the developer's control, so ignore it + if err != nil || len(data) == 0 { + // Cachefile exists, but no data found. Get new credential. return "", nil, false } @@ -239,8 +240,9 @@ func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bo return token, nil, true } - if _, ok := err.(timeoutException); ok { - // Cached token expired. Go through regular flow to find new token. + if _, ok := err.(nonCacheableError); ok { + // If the cached token is expired we need a new token, + // and if the cache contains a failure, we need to try again. return "", nil, false } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index 3ba7a92..9a9318a 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -319,7 +319,7 @@ func TestRetrieveExecutableSubjectTokenInvalidFormat(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), jsonParsingError(executableSource).Error(); got != want { + if got, want := err.Error(), jsonParsingError(executableSource, "tokentokentoken").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -460,7 +460,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithFields(t *testing if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), userDefinedError(executableSource, "404", "Token Not Found").Error(); got != want { + if got, want := err.Error(), userDefinedError("404", "Token Not Found").Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -508,7 +508,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithCode(t *testing.T if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { + if got, want := err.Error(), malformedFailureError().Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -556,7 +556,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithMessage(t *testin if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { + if got, want := err.Error(), malformedFailureError().Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -603,7 +603,7 @@ func TestRetrieveExecutableSubjectTokenUnsuccessfulResponseWithoutFields(t *test if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), malformedFailureError(executableSource).Error(); got != want { + if got, want := err.Error(), malformedFailureError().Error(); got != want { t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) } @@ -1149,7 +1149,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidFormat(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), jsonParsingError(outputFileSource).Error(); got != want { + if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want { t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) } } @@ -1279,9 +1279,16 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithFields(t *testing 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) { - t.Fatalf("Executable called when it should not have been") - return []byte{}, nil + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }) } if err = json.NewEncoder(outputFile).Encode(executableResponse{ @@ -1298,12 +1305,19 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithFields(t *testing t.Fatalf("parse() failed %v", err) } - _, err = base.subjectToken() - if err == nil { - t.Fatalf("Expected error but found none") + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) } - if got, want := err.Error(), userDefinedError(outputFileSource, "404", "Token Not Found").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") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) } } @@ -1332,9 +1346,16 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithCode(t *testing.T 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) { - t.Fatalf("Executable called when it should not have been") - return []byte{}, nil + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }) } if err = json.NewEncoder(outputFile).Encode(executableResponse{ @@ -1350,12 +1371,19 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithCode(t *testing.T t.Fatalf("parse() failed %v", err) } - _, err = base.subjectToken() - if err == nil { - t.Fatalf("Expected error but found none") + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) } - if got, want := err.Error(), malformedFailureError(outputFileSource).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") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) } } @@ -1384,9 +1412,16 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithMessage(t *testin 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) { - t.Fatalf("Executable called when it should not have been") - return []byte{}, nil + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }) } if err = json.NewEncoder(outputFile).Encode(executableResponse{ @@ -1402,12 +1437,19 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithMessage(t *testin t.Fatalf("parse() failed %v", err) } - _, err = base.subjectToken() - if err == nil { - t.Fatalf("Expected error but found none") + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) } - if got, want := err.Error(), malformedFailureError(outputFileSource).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") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) } } @@ -1436,9 +1478,16 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithoutFields(t *test 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) { - t.Fatalf("Executable called when it should not have been") - return []byte{}, nil + deadline, deadlineSet = ctx.Deadline() + return json.Marshal(executableResponse{ + Success: Bool(true), + Version: 1, + ExpirationTime: now().Unix() + 3600, + TokenType: "urn:ietf:params:oauth:token-type:jwt", + IdToken: "tokentokentoken", + }) } if err = json.NewEncoder(outputFile).Encode(executableResponse{ @@ -1453,12 +1502,19 @@ func TestRetrieveOutputFileSubjectTokenUnsuccessfulResponseWithoutFields(t *test t.Fatalf("parse() failed %v", err) } - _, err = base.subjectToken() - if err == nil { - t.Fatalf("Expected error but found none") + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) } - if got, want := err.Error(), malformedFailureError(outputFileSource).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") + } + + if got, want := out, "tokentokentoken"; got != want { + t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) } } From b777222fcec73f65431e41886240d5a1c7507b84 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Wed, 27 Apr 2022 12:20:25 -0700 Subject: [PATCH 3/4] Changes requested by @codyoss --- .../externalaccount/executablecredsource.go | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 8709c08..69aaa0b 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -226,6 +226,7 @@ func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bo // No OutputFile found. Hasn't been created yet, so skip it. return "", nil, false } + defer file.Close() data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20)) if err != nil || len(data) == 0 { @@ -234,20 +235,18 @@ func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bo } token, err := parseSubjectTokenFromSource(data, outputFileSource) + if err != nil { + if _, ok := err.(nonCacheableError); ok { + // If the cached token is expired we need a new token, + // and if the cache contains a failure, we need to try again. + return "", nil, false + } - if err == nil { - // Token parsing succeeded. Use found token. - return token, nil, true + // There was an error in the cached token, and the developer should be aware of it. + return "", err, true } - - if _, ok := err.(nonCacheableError); ok { - // If the cached token is expired we need a new token, - // and if the cache contains a failure, we need to try again. - return "", nil, false - } - - // There was an error in the cached token, and the developer should be aware of it. - return "", err, true + // Token parsing succeeded. Use found token. + return token, nil, true } func (cs executableCredentialSource) getEnvironment() []string { From 64cf1161995e81c5eb49f10057cc85711a9ac4c8 Mon Sep 17 00:00:00 2001 From: Ryan Kohler Date: Thu, 28 Apr 2022 06:19:18 -0700 Subject: [PATCH 4/4] Changes requested by @lsirac --- google/internal/externalaccount/executablecredsource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 69aaa0b..0069a8f 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -217,7 +217,7 @@ func (cs executableCredentialSource) subjectToken() (string, error) { func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bool) { if cs.OutputFile == "" { - // This ExecutableCredentialSource doesn't use an OutputFile + // This ExecutableCredentialSource doesn't use an OutputFile. return "", nil, false }