| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package externalaccount | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "sort" | |
| "testing" | |
| "time" | |
| "github.com/google/go-cmp/cmp" | |
| ) | |
| type testEnvironment struct { | |
| envVars map[string]string | |
| deadline time.Time | |
| deadlineSet bool | |
| byteResponse []byte | |
| jsonResponse *executableResponse | |
| } | |
| var executablesAllowed = map[string]string{ | |
| "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", | |
| } | |
| func (t *testEnvironment) existingEnv() []string { | |
| result := []string{} | |
| for k, v := range t.envVars { | |
| result = append(result, fmt.Sprintf("%v=%v", k, v)) | |
| } | |
| return result | |
| } | |
| func (t *testEnvironment) getenv(key string) string { | |
| return t.envVars[key] | |
| } | |
| func (t *testEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) { | |
| t.deadline, t.deadlineSet = ctx.Deadline() | |
| if t.jsonResponse != nil { | |
| return json.Marshal(t.jsonResponse) | |
| } | |
| return t.byteResponse, nil | |
| } | |
| func (t *testEnvironment) getDeadline() (time.Time, bool) { | |
| return t.deadline, t.deadlineSet | |
| } | |
| func (t *testEnvironment) now() time.Time { | |
| return defaultTime | |
| } | |
| func Bool(b bool) *bool { | |
| return &b | |
| } | |
| func Int(i int) *int { | |
| return &i | |
| } | |
| var creationTests = []struct { | |
| name string | |
| executableConfig ExecutableConfig | |
| expectedErr error | |
| expectedTimeout time.Duration | |
| }{ | |
| { | |
| name: "Basic Creation", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(50000), | |
| }, | |
| expectedTimeout: 50000 * time.Millisecond, | |
| }, | |
| { | |
| name: "Without Timeout", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| }, | |
| expectedTimeout: 30000 * time.Millisecond, | |
| }, | |
| { | |
| name: "Without Command", | |
| executableConfig: ExecutableConfig{}, | |
| expectedErr: commandMissingError(), | |
| }, | |
| { | |
| name: "Timeout Too Low", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(4999), | |
| }, | |
| expectedErr: timeoutRangeError(), | |
| }, | |
| { | |
| name: "Timeout Lower Bound", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(5000), | |
| }, | |
| expectedTimeout: 5000 * time.Millisecond, | |
| }, | |
| { | |
| name: "Timeout Upper Bound", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(120000), | |
| }, | |
| expectedTimeout: 120000 * time.Millisecond, | |
| }, | |
| { | |
| name: "Timeout Too High", | |
| executableConfig: ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(120001), | |
| }, | |
| expectedErr: timeoutRangeError(), | |
| }, | |
| } | |
| func TestCreateExecutableCredential(t *testing.T) { | |
| for _, tt := range creationTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| ecs, err := CreateExecutableCredential(context.Background(), &tt.executableConfig, nil) | |
| if tt.expectedErr != nil { | |
| if err == nil { | |
| t.Fatalf("Expected error but found none") | |
| } | |
| if got, want := err.Error(), tt.expectedErr.Error(); got != want { | |
| t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) | |
| } | |
| } else if err != nil { | |
| ecJson := "{???}" | |
| if ecBytes, err2 := json.Marshal(tt.executableConfig); err2 != nil { | |
| ecJson = string(ecBytes) | |
| } | |
| t.Fatalf("CreateExecutableCredential with %v returned error: %v", ecJson, err) | |
| } else { | |
| if ecs.Command != "blarg" { | |
| t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg") | |
| } | |
| if ecs.Timeout != tt.expectedTimeout { | |
| t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| var getEnvironmentTests = []struct { | |
| name string | |
| config Config | |
| environment testEnvironment | |
| expectedEnvironment []string | |
| }{ | |
| { | |
| name: "Minimal Executable Config", | |
| config: Config{ | |
| Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| CredentialSource: CredentialSource{ | |
| Executable: &ExecutableConfig{ | |
| Command: "blarg", | |
| }, | |
| }, | |
| }, | |
| environment: testEnvironment{ | |
| envVars: map[string]string{ | |
| "A": "B", | |
| }, | |
| }, | |
| expectedEnvironment: []string{ | |
| "A=B", | |
| "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", | |
| "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", | |
| }, | |
| }, | |
| { | |
| name: "Full Impersonation URL", | |
| config: Config{ | |
| Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", | |
| SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| CredentialSource: CredentialSource{ | |
| Executable: &ExecutableConfig{ | |
| Command: "blarg", | |
| OutputFile: "/path/to/generated/cached/credentials", | |
| }, | |
| }, | |
| }, | |
| environment: testEnvironment{ | |
| envVars: map[string]string{ | |
| "A": "B", | |
| }, | |
| }, | |
| expectedEnvironment: []string{ | |
| "A=B", | |
| "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", | |
| "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=test@project.iam.gserviceaccount.com", | |
| "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", | |
| "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials", | |
| }, | |
| }, | |
| { | |
| name: "Impersonation Email", | |
| config: Config{ | |
| Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", | |
| SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| CredentialSource: CredentialSource{ | |
| Executable: &ExecutableConfig{ | |
| Command: "blarg", | |
| OutputFile: "/path/to/generated/cached/credentials", | |
| }, | |
| }, | |
| }, | |
| environment: testEnvironment{ | |
| envVars: map[string]string{ | |
| "A": "B", | |
| }, | |
| }, | |
| expectedEnvironment: []string{ | |
| "A=B", | |
| "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", | |
| "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt", | |
| "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0", | |
| "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials", | |
| }, | |
| }, | |
| } | |
| func TestExecutableCredentialGetEnvironment(t *testing.T) { | |
| for _, tt := range getEnvironmentTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| config := tt.config | |
| ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) | |
| if err != nil { | |
| t.Fatalf("creation failed %v", err) | |
| } | |
| ecs.env = &tt.environment | |
| // This Transformer sorts a []string. | |
| sorter := cmp.Transformer("Sort", func(in []string) []string { | |
| out := append([]string(nil), in...) // Copy input to avoid mutating it | |
| sort.Strings(out) | |
| return out | |
| }) | |
| if got, want := ecs.executableEnvironment(), tt.expectedEnvironment; !cmp.Equal(got, want, sorter) { | |
| t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| var failureTests = []struct { | |
| name string | |
| testEnvironment testEnvironment | |
| noExecution bool | |
| expectedErr error | |
| }{ | |
| { | |
| name: "Environment Variable Not Set", | |
| testEnvironment: testEnvironment{ | |
| byteResponse: []byte{}, | |
| }, | |
| noExecution: true, | |
| expectedErr: executablesDisallowedError(), | |
| }, | |
| { | |
| name: "Invalid Token", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| byteResponse: []byte("tokentokentoken"), | |
| }, | |
| expectedErr: jsonParsingError(executableSource, "tokentokentoken"), | |
| }, | |
| { | |
| name: "Version Field Missing", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "version"), | |
| }, | |
| { | |
| name: "Success Field Missing", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Version: 1, | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "success"), | |
| }, | |
| { | |
| name: "User defined error", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Code: "404", | |
| Message: "Token Not Found", | |
| }, | |
| }, | |
| expectedErr: userDefinedError("404", "Token Not Found"), | |
| }, | |
| { | |
| name: "User defined error without code", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Message: "Token Not Found", | |
| }, | |
| }, | |
| expectedErr: malformedFailureError(), | |
| }, | |
| { | |
| name: "User defined error without message", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Code: "404", | |
| }, | |
| }, | |
| expectedErr: malformedFailureError(), | |
| }, | |
| { | |
| name: "User defined error without fields", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| }, | |
| }, | |
| expectedErr: malformedFailureError(), | |
| }, | |
| { | |
| name: "Newer Version", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 2, | |
| }, | |
| }, | |
| expectedErr: unsupportedVersionError(executableSource, 2), | |
| }, | |
| { | |
| name: "Missing Token Type", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "token_type"), | |
| }, | |
| { | |
| name: "Token Expired", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() - 1, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| }, | |
| expectedErr: tokenExpiredError(), | |
| }, | |
| { | |
| name: "Invalid Token Type", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| TokenType: "urn:ietf:params:oauth:token-type:invalid", | |
| }, | |
| }, | |
| expectedErr: tokenTypeError(executableSource), | |
| }, | |
| { | |
| name: "Missing JWT", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "id_token"), | |
| }, | |
| { | |
| name: "Missing ID Token", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| TokenType: "urn:ietf:params:oauth:token-type:id_token", | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "id_token"), | |
| }, | |
| { | |
| name: "Missing SAML Token", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| TokenType: "urn:ietf:params:oauth:token-type:saml2", | |
| }, | |
| }, | |
| expectedErr: missingFieldError(executableSource, "saml_response"), | |
| }, | |
| } | |
| func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) { | |
| cs := CredentialSource{ | |
| Executable: &ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(5000), | |
| }, | |
| } | |
| tfc := testFileConfig | |
| tfc.CredentialSource = cs | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| for _, tt := range failureTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| ecs.env = &tt.testEnvironment | |
| if _, err = ecs.subjectToken(); err == nil { | |
| t.Fatalf("Expected error but found none") | |
| } else if got, want := err.Error(), tt.expectedErr.Error(); got != want { | |
| t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want) | |
| } | |
| deadline, deadlineSet := tt.testEnvironment.getDeadline() | |
| if tt.noExecution { | |
| if deadlineSet { | |
| t.Errorf("Executable called when it should not have been") | |
| } | |
| } else { | |
| if !deadlineSet { | |
| t.Errorf("Command run without a deadline") | |
| } else if deadline != defaultTime.Add(5*time.Second) { | |
| t.Errorf("Command run with incorrect deadline") | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| var successTests = []struct { | |
| name string | |
| testEnvironment testEnvironment | |
| }{ | |
| { | |
| name: "JWT", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| IdToken: "tokentokentoken", | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "ID Token", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:id_token", | |
| IdToken: "tokentokentoken", | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "SAML", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:saml2", | |
| SamlResponse: "tokentokentoken", | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Missing Expiration", | |
| testEnvironment: testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| IdToken: "tokentokentoken", | |
| }, | |
| }, | |
| }, | |
| } | |
| func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) { | |
| cs := CredentialSource{ | |
| Executable: &ExecutableConfig{ | |
| Command: "blarg", | |
| TimeoutMillis: Int(5000), | |
| }, | |
| } | |
| tfc := testFileConfig | |
| tfc.CredentialSource = cs | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| for _, tt := range successTests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| ecs.env = &tt.testEnvironment | |
| out, err := ecs.subjectToken() | |
| if err != nil { | |
| t.Fatalf("retrieveSubjectToken() failed: %v", err) | |
| } | |
| deadline, deadlineSet := tt.testEnvironment.getDeadline() | |
| if !deadlineSet { | |
| t.Errorf("Command run without a deadline") | |
| } else if deadline != defaultTime.Add(5*time.Second) { | |
| t.Errorf("Command run with incorrect deadline") | |
| } | |
| if got, want := out, "tokentokentoken"; got != want { | |
| t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestRetrieveOutputFileSubjectTokenNotJSON(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 | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| if _, err = outputFile.Write([]byte("tokentokentoken")); err != nil { | |
| t.Fatalf("error writing to file: %v", err) | |
| } | |
| te := testEnvironment{ | |
| envVars: executablesAllowed, | |
| byteResponse: []byte{}, | |
| } | |
| ecs.env = &te | |
| if _, err = base.subjectToken(); err == nil { | |
| t.Fatalf("Expected error but found none") | |
| } else if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want { | |
| t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) | |
| } | |
| _, deadlineSet := te.getDeadline() | |
| if deadlineSet { | |
| t.Errorf("Executable called when it should not have been") | |
| } | |
| } | |
| // These are errors in the output file that should be reported to the user. | |
| // Most of these will help the developers debug their code. | |
| var cacheFailureTests = []struct { | |
| name string | |
| outputFileContents executableResponse | |
| expectedErr error | |
| }{ | |
| { | |
| name: "Missing Version", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "version"), | |
| }, | |
| { | |
| name: "Missing Success", | |
| outputFileContents: executableResponse{ | |
| Version: 1, | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "success"), | |
| }, | |
| { | |
| name: "Newer Version", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 2, | |
| }, | |
| expectedErr: unsupportedVersionError(outputFileSource, 2), | |
| }, | |
| { | |
| name: "Missing Token Type", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "token_type"), | |
| }, | |
| { | |
| name: "Missing Expiration", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "expiration_time"), | |
| }, | |
| { | |
| name: "Invalid Token Type", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix(), | |
| TokenType: "urn:ietf:params:oauth:token-type:invalid", | |
| }, | |
| expectedErr: tokenTypeError(outputFileSource), | |
| }, | |
| { | |
| name: "Missing JWT", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "id_token"), | |
| }, | |
| { | |
| name: "Missing ID Token", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:id_token", | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "id_token"), | |
| }, | |
| { | |
| name: "Missing SAML", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| expectedErr: missingFieldError(outputFileSource, "id_token"), | |
| }, | |
| } | |
| func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) { | |
| for _, tt := range cacheFailureTests { | |
| t.Run(tt.name, func(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 | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| te := testEnvironment{ | |
| envVars: executablesAllowed, | |
| byteResponse: []byte{}, | |
| } | |
| ecs.env = &te | |
| if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { | |
| t.Errorf("Error encoding to file: %v", err) | |
| return | |
| } | |
| if _, err = ecs.subjectToken(); err == nil { | |
| t.Errorf("Expected error but found none") | |
| } else if got, want := err.Error(), tt.expectedErr.Error(); got != want { | |
| t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) | |
| } | |
| if _, deadlineSet := te.getDeadline(); deadlineSet { | |
| t.Errorf("Executable called when it should not have been") | |
| } | |
| }) | |
| } | |
| } | |
| // These tests should ignore the error in the output file, and check the executable. | |
| var invalidCacheTests = []struct { | |
| name string | |
| outputFileContents executableResponse | |
| }{ | |
| { | |
| name: "User Defined Error", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Code: "404", | |
| Message: "Token Not Found", | |
| }, | |
| }, | |
| { | |
| name: "User Defined Error without Code", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Message: "Token Not Found", | |
| }, | |
| }, | |
| { | |
| name: "User Defined Error without Message", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| Code: "404", | |
| }, | |
| }, | |
| { | |
| name: "User Defined Error without Fields", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(false), | |
| Version: 1, | |
| }, | |
| }, | |
| { | |
| name: "Expired Token", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() - 1, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| }, | |
| }, | |
| } | |
| func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) { | |
| for _, tt := range invalidCacheTests { | |
| t.Run(tt.name, func(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 | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| te := testEnvironment{ | |
| envVars: executablesAllowed, | |
| jsonResponse: &executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| IdToken: "tokentokentoken", | |
| }, | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| ecs.env = &te | |
| if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { | |
| t.Errorf("Error encoding to file: %v", err) | |
| return | |
| } | |
| out, err := ecs.subjectToken() | |
| if err != nil { | |
| t.Errorf("retrieveSubjectToken() failed: %v", err) | |
| return | |
| } | |
| if deadline, deadlineSet := te.getDeadline(); !deadlineSet { | |
| t.Errorf("Command run without a deadline") | |
| } else if deadline != defaultTime.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) | |
| } | |
| }) | |
| } | |
| } | |
| var cacheSuccessTests = []struct { | |
| name string | |
| outputFileContents executableResponse | |
| }{ | |
| { | |
| name: "JWT", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:jwt", | |
| IdToken: "tokentokentoken", | |
| }, | |
| }, | |
| { | |
| name: "Id Token", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:id_token", | |
| IdToken: "tokentokentoken", | |
| }, | |
| }, | |
| { | |
| name: "SAML", | |
| outputFileContents: executableResponse{ | |
| Success: Bool(true), | |
| Version: 1, | |
| ExpirationTime: defaultTime.Unix() + 3600, | |
| TokenType: "urn:ietf:params:oauth:token-type:saml2", | |
| SamlResponse: "tokentokentoken", | |
| }, | |
| }, | |
| } | |
| func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { | |
| for _, tt := range cacheSuccessTests { | |
| t.Run(tt.name, func(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 | |
| base, err := tfc.parse(context.Background()) | |
| if err != nil { | |
| t.Fatalf("parse() failed %v", err) | |
| } | |
| te := testEnvironment{ | |
| envVars: executablesAllowed, | |
| byteResponse: []byte{}, | |
| } | |
| ecs, ok := base.(executableCredentialSource) | |
| if !ok { | |
| t.Fatalf("Wrong credential type created.") | |
| } | |
| ecs.env = &te | |
| if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil { | |
| t.Errorf("Error encoding to file: %v", err) | |
| return | |
| } | |
| if out, err := ecs.subjectToken(); err != nil { | |
| t.Errorf("retrieveSubjectToken() failed: %v", err) | |
| } else if got, want := out, "tokentokentoken"; got != want { | |
| t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got) | |
| } | |
| if _, deadlineSet := te.getDeadline(); deadlineSet { | |
| t.Errorf("Executable called when it should not have been") | |
| } | |
| }) | |
| } | |
| } |