mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
With https://go.dev/issue/61417 implemented, we can use the token type directly to unmarshal the JSON fields for the wire format. While here, remove all uses of the deprecated ioutil package as suggested by gopls while making these changes. Change-Id: I79d82374643007a21b5b3d9a8117bed81273eca5 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/614415 Reviewed-by: Sean Liao <sean@liao.dev> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Michael Pratt <mpratt@google.com>
1056 lines
27 KiB
Go
1056 lines
27 KiB
Go
// 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"
|
|
"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)
|
|
}
|
|
if ecs.credentialSourceType() != "executable" {
|
|
t.Errorf("ecs.CredentialSourceType() got %s but want executable", ecs.credentialSourceType())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 := os.CreateTemp("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\nReceived: %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 := os.CreateTemp("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\nReceived: %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 := os.CreateTemp("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\nReceived: %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 := os.CreateTemp("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\nReceived: %s", want, got)
|
|
}
|
|
|
|
if _, deadlineSet := te.getDeadline(); deadlineSet {
|
|
t.Errorf("Executable called when it should not have been")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceAccountImpersonationRE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serviceAccountImpersonationURL string
|
|
want string
|
|
}{
|
|
{
|
|
name: "universe domain Google Default Universe (GDU) googleapis.com",
|
|
serviceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
|
want: "test@project.iam.gserviceaccount.com",
|
|
},
|
|
{
|
|
name: "email does not match",
|
|
serviceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "universe domain non-GDU",
|
|
serviceAccountImpersonationURL: "https://iamcredentials.apis-tpclp.goog/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
|
want: "test@project.iam.gserviceaccount.com",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
matches := serviceAccountImpersonationRE.FindStringSubmatch(tt.serviceAccountImpersonationURL)
|
|
if matches == nil {
|
|
if tt.want != "" {
|
|
t.Errorf("%q: got nil, want %q", tt.name, tt.want)
|
|
}
|
|
} else if matches[1] != tt.want {
|
|
t.Errorf("%q: got %q, want %q", tt.name, matches[1], tt.want)
|
|
}
|
|
}
|
|
}
|