* feat: update retry logic for validation call - update validation for service account validation call to use `select` instead of `sleep` - add `validateServiceAccountKeyTimeout` to config. This makes it easier to control the timeout from tests so tests do not have to run for long with the default timeout
474 lines
14 KiB
Go
474 lines
14 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package credential
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/impersonate"
|
|
googleOption "google.golang.org/api/option"
|
|
)
|
|
|
|
func TestGenerateCredentials(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
testImpersonateServiceFn := func(ctx context.Context, config impersonate.CredentialsConfig, opts ...googleOption.ClientOption) (oauth2.TokenSource, error) {
|
|
return oauth2.StaticTokenSource(&oauth2.Token{}), nil
|
|
}
|
|
impersonateServiceAccountFn = testImpersonateServiceFn
|
|
|
|
const fakePrivateKey = `-----BEGIN PRIVATE KEY-----
|
|
MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA1y+Vmxg1hK6j0ef6
|
|
-----END PRIVATE KEY-----`
|
|
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
wantErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Service Account Key Authentication",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test_email@example.com",
|
|
ProjectId: "test_project",
|
|
PrivateKeyId: "test_key_id",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Service Account Impersonation",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test_email@example.com",
|
|
ProjectId: "test_project",
|
|
PrivateKeyId: "test_key_id",
|
|
TargetServiceAccountId: "target_service_account@example.com",
|
|
Scopes: []string{"test_scope"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Application Default Credentials",
|
|
config: &Config{},
|
|
wantErr: true, // Since ADC might not be available in test environment
|
|
expectedErr: "failed to find default credentials",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
creds, err := tt.config.GenerateCredentials(ctx)
|
|
if tt.wantErr {
|
|
require.ErrorContains(t, err, tt.expectedErr)
|
|
require.Nil(t, creds)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, creds)
|
|
require.Equal(t, tt.config.ProjectId, creds.ProjectID)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCredentialsFromServiceAccountKey(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Define a fake but correctly formatted PEM-encoded private key.
|
|
const fakePrivateKey = `-----BEGIN PRIVATE KEY-----
|
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC
|
|
-----END PRIVATE KEY-----`
|
|
|
|
// Test cases
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
wantErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Successful Credential Generation",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Missing Private Key",
|
|
config: &Config{
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
},
|
|
wantErr: true,
|
|
expectedErr: "private_key is required",
|
|
},
|
|
{
|
|
name: "Missing Client Email",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
},
|
|
wantErr: true,
|
|
expectedErr: "client_email is required",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
creds, err := tt.config.credentialsFromServiceAccountKey(ctx)
|
|
if tt.wantErr {
|
|
require.ErrorContains(t, err, tt.expectedErr)
|
|
require.Nil(t, creds)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, creds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCredentialsFromADC(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
mockTokenSource := oauth2.StaticTokenSource(&oauth2.Token{
|
|
AccessToken: "mock_access_token",
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
mockFindDefaultCred func(ctx context.Context, scopes ...string) (*google.Credentials, error)
|
|
wantErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Successful ADC Retrieval",
|
|
mockFindDefaultCred: func(ctx context.Context, scopes ...string) (*google.Credentials, error) {
|
|
return &google.Credentials{
|
|
ProjectID: "test-project-id",
|
|
TokenSource: mockTokenSource,
|
|
}, nil
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ADC Not Available",
|
|
mockFindDefaultCred: func(ctx context.Context, scopes ...string) (*google.Credentials, error) {
|
|
return nil, errors.New("ADC not available")
|
|
},
|
|
wantErr: true,
|
|
expectedErr: "failed to find default credentials",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Override findDefaultCredentials with the mock implementation.
|
|
findDefaultCredentialsFn = tt.mockFindDefaultCred
|
|
|
|
config := &Config{}
|
|
|
|
creds, err := config.credentialsFromADC(ctx)
|
|
if tt.wantErr {
|
|
require.ErrorContains(t, err, tt.expectedErr)
|
|
require.Nil(t, creds)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, creds)
|
|
require.Equal(t, "test-project-id", creds.ProjectID, "Expected the project ID to be 'test-project-id'")
|
|
require.Equal(t, mockTokenSource, creds.TokenSource, "Expected the mock token source")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCredentialsFromImpersonation(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Define a fake but correctly formatted PEM-encoded private key.
|
|
const fakePrivateKey = `-----BEGIN PRIVATE KEY-----
|
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC
|
|
-----END PRIVATE KEY-----`
|
|
|
|
testImpersonateServiceFn := func(ctx context.Context, config impersonate.CredentialsConfig, opts ...googleOption.ClientOption) (oauth2.TokenSource, error) {
|
|
return oauth2.StaticTokenSource(&oauth2.Token{}), nil
|
|
}
|
|
impersonateServiceAccountFn = testImpersonateServiceFn
|
|
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
mockImpersonateServiceAccountFn func(ctx context.Context, config impersonate.CredentialsConfig, opts ...googleOption.ClientOption) (oauth2.TokenSource, error)
|
|
wantErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "Successful Impersonation",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
Scopes: []string{"test-scope"},
|
|
},
|
|
mockImpersonateServiceAccountFn: func(ctx context.Context, config impersonate.CredentialsConfig, opts ...googleOption.ClientOption) (oauth2.TokenSource, error) {
|
|
return oauth2.StaticTokenSource(&oauth2.Token{}), nil
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Error during Impersonation",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
Scopes: []string{"test-scope"},
|
|
},
|
|
mockImpersonateServiceAccountFn: func(ctx context.Context, config impersonate.CredentialsConfig, opts ...googleOption.ClientOption) (oauth2.TokenSource, error) {
|
|
return nil, errors.New("mock impersonation error")
|
|
},
|
|
wantErr: true,
|
|
expectedErr: "failed to create impersonated token source",
|
|
},
|
|
{
|
|
name: "Missing Private Key",
|
|
config: &Config{
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
},
|
|
wantErr: true,
|
|
expectedErr: "private_key is required",
|
|
},
|
|
{
|
|
name: "Missing Client Email",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
},
|
|
mockImpersonateServiceAccountFn: nil,
|
|
wantErr: true,
|
|
expectedErr: "client_email is required",
|
|
},
|
|
{
|
|
name: "Missing Target Service Account ID",
|
|
config: &Config{
|
|
PrivateKey: fakePrivateKey,
|
|
ClientEmail: "test-email@example.com",
|
|
ProjectId: "test-project-id",
|
|
PrivateKeyId: "test-private-key-id",
|
|
},
|
|
mockImpersonateServiceAccountFn: nil,
|
|
wantErr: true,
|
|
expectedErr: "target_service_account_id is required",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.mockImpersonateServiceAccountFn != nil {
|
|
impersonateServiceAccountFn = tt.mockImpersonateServiceAccountFn
|
|
}
|
|
|
|
creds, err := tt.config.credentialsFromImpersonation(ctx)
|
|
if tt.wantErr {
|
|
require.ErrorContains(t, err, tt.expectedErr)
|
|
require.Nil(t, creds)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, creds)
|
|
require.Equal(t, tt.config.ProjectId, creds.ProjectID)
|
|
|
|
token, err := creds.TokenSource.Token()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, token)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClone(t *testing.T) {
|
|
originalConfig := &Config{
|
|
ProjectId: "original-project-id",
|
|
PrivateKey: "original-private-key",
|
|
PrivateKeyId: "original-private-key-id",
|
|
ClientEmail: "original-client-email@example.com",
|
|
TargetServiceAccountId: "original-target-service-account-id",
|
|
Scopes: []string{"scope1", "scope2"},
|
|
}
|
|
|
|
clonedConfig := originalConfig.clone()
|
|
|
|
require.NotNil(t, clonedConfig)
|
|
require.Equal(t, originalConfig.ProjectId, clonedConfig.ProjectId)
|
|
require.Equal(t, originalConfig.PrivateKey, clonedConfig.PrivateKey)
|
|
require.Equal(t, originalConfig.PrivateKeyId, clonedConfig.PrivateKeyId)
|
|
require.Equal(t, originalConfig.ClientEmail, clonedConfig.ClientEmail)
|
|
require.Equal(t, originalConfig.TargetServiceAccountId, clonedConfig.TargetServiceAccountId)
|
|
require.Equal(t, originalConfig.Scopes, clonedConfig.Scopes)
|
|
|
|
// Ensure that modifying the cloned config does not affect the original config
|
|
clonedConfig.ProjectId = "modified-project-id"
|
|
clonedConfig.PrivateKey = "modified-private-key"
|
|
clonedConfig.PrivateKeyId = "modified-private-key-id"
|
|
clonedConfig.ClientEmail = "modified-client-email@example.com"
|
|
clonedConfig.TargetServiceAccountId = "modified-target-service-account-id"
|
|
clonedConfig.Scopes = []string{"modified-scope1", "modified-scope2"}
|
|
|
|
require.NotEqual(t, originalConfig.ProjectId, clonedConfig.ProjectId)
|
|
require.NotEqual(t, originalConfig.PrivateKey, clonedConfig.PrivateKey)
|
|
require.NotEqual(t, originalConfig.PrivateKeyId, clonedConfig.PrivateKeyId)
|
|
require.NotEqual(t, originalConfig.ClientEmail, clonedConfig.ClientEmail)
|
|
require.NotEqual(t, originalConfig.TargetServiceAccountId, clonedConfig.TargetServiceAccountId)
|
|
require.NotEqual(t, originalConfig.Scopes, clonedConfig.Scopes)
|
|
}
|
|
|
|
func TestIsRotatable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
want bool
|
|
}{
|
|
{
|
|
name: "Nil Config",
|
|
config: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "Service Account Impersonation Config",
|
|
config: &Config{
|
|
PrivateKey: "fake-private-key",
|
|
ClientEmail: "test-email@example.com",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Static GCP Config",
|
|
config: &Config{
|
|
PrivateKey: "fake-private-key",
|
|
ClientEmail: "test-email@example.com",
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Default Config",
|
|
config: &Config{},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.config.IsRotatable()
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
options []Option
|
|
expected *Config
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "Valid Options",
|
|
options: []Option{
|
|
WithProjectId("test-project-id"),
|
|
WithPrivateKey("test-private-key"),
|
|
WithPrivateKeyId("test-private-key-id"),
|
|
WithClientEmail("test-email@example.com"),
|
|
WithTargetServiceAccountId("target-service-account@example.com"),
|
|
WithZone("test-zone"),
|
|
WithScopes([]string{"scope1", "scope2"}),
|
|
},
|
|
expected: &Config{
|
|
ProjectId: "test-project-id",
|
|
PrivateKey: "test-private-key",
|
|
PrivateKeyId: "test-private-key-id",
|
|
ClientEmail: "test-email@example.com",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
Zone: "test-zone",
|
|
Scopes: []string{"scope1", "scope2"},
|
|
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "No Options",
|
|
options: []Option{},
|
|
expected: &Config{
|
|
Scopes: []string{defaultGCPScope},
|
|
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Invalid Option",
|
|
options: []Option{
|
|
func(opts *Options) error {
|
|
return errors.New("invalid option")
|
|
},
|
|
},
|
|
expected: nil,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "no scope set",
|
|
options: []Option{
|
|
WithProjectId("test-project-id"),
|
|
WithPrivateKey("test-private-key"),
|
|
WithPrivateKeyId("test-private-key-id"),
|
|
WithClientEmail("test-email@example.com"),
|
|
WithTargetServiceAccountId("target-service-account@example.com"),
|
|
WithZone("test-zone"),
|
|
},
|
|
expected: &Config{
|
|
ProjectId: "test-project-id",
|
|
PrivateKey: "test-private-key",
|
|
PrivateKeyId: "test-private-key-id",
|
|
ClientEmail: "test-email@example.com",
|
|
TargetServiceAccountId: "target-service-account@example.com",
|
|
Zone: "test-zone",
|
|
Scopes: []string{defaultGCPScope},
|
|
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
|
},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config, err := NewConfig(tt.options...)
|
|
if tt.expectError {
|
|
require.Error(t, err)
|
|
require.Nil(t, config)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, config)
|
|
require.Equal(t, tt.expected, config)
|
|
})
|
|
}
|
|
}
|