diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index f2b15b1..e95355a 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" @@ -23,34 +25,44 @@ 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) +type nonCacheableError struct { + message string } -func jsonParsingError() error { - return errors.New("oauth2/google: unable to parse response JSON") +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, data string) error { + return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data) } func malformedFailureError() error { - return errors.New("oauth2/google: response must include `error` and `message` fields when unsuccessful") + return nonCacheableError{"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) + return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", 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) } func tokenExpiredError() error { - return errors.New("oauth2/google: the token returned by the executable is expired") + return nonCacheableError{"oauth2/google: the token returned by the executable is expired"} } -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 { @@ -143,18 +155,18 @@ 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, string(response)) } 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 { @@ -165,15 +177,15 @@ func parseSubjectToken(response []byte) (string, error) { } 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() { @@ -182,32 +194,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 + } + defer file.Close() + + data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20)) + if err != nil || len(data) == 0 { + // Cachefile exists, but no data found. Get new credential. + return "", nil, false + } + + 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 + } + + // 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 { @@ -251,6 +292,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..9a9318a 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, "tokentokentoken").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) } @@ -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,961 @@ 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, "tokentokentoken").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) + 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(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) + } + + 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 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) + 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(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) + } + + 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 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) + 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(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) + } + + 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 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) + 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(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) + } + + 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 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) + } +}