Files
threshold-plugin-gcp/internal/credential/config_test.go
T
Elim Tsiagbey be892acbd9 feat: GCP authentication (#5)
* feat: GCP authentication

Add authentication support for:
- authentication with service account key
- authentication with impersonating service account key
- authentication with application default credentials which are read from the environment

A HTTP client with a token set will be returned and used for authentication with all clients. for example
```
gcpClient, err := gcpConfig.GetClient(ctx)
if err != nil {
	log.Fatalf("Failed to get client: %v", err)
}

client, err := computeCloud.NewInstancesRESTClient(ctx, option.WithHTTPClient(gcpClient))
if err != nil {
	log.Fatalf("Failed to create client: %v", err)
}
```
2024-09-22 23:21:58 -04:00

327 lines
9.4 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 TestGetClient(t *testing.T) {
ctx := context.Background()
config := &Config{
PrivateKey: "test_private_key",
ClientEmail: "test_email@example.com",
ProjectId: "test_project",
PrivateKeyId: "test_key_id",
}
client, err := config.GetClient(ctx)
require.NoError(t, err)
require.NotNil(t, client)
// Call GetClient again to ensure the cached client is returned
cachedClient, err := config.GetClient(ctx)
require.NoError(t, err)
require.Equal(t, client, cachedClient)
}
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)
})
}
}