Files
threshold-plugin-gcp/internal/credential/config_test.go
T
Elim Tsiagbey 89c8ee2d6d feat: Set default GCP Scope (#16)
* feat: Set default GCP Scope

Update the GCP credential configuration to include a default scope for GCP access. The default scope is set to `https://www.googleapis.com/auth/cloud-platform` as recommended by [GCP's Best Practice](https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice). This allows controlling the service account's access by granting it IAM roles.
2024-10-30 13:58:45 -04:00

471 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"},
},
expectError: false,
},
{
name: "No Options",
options: []Option{},
expected: &Config{
Scopes: []string{defaultGCPScope},
},
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},
},
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)
})
}
}