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