Files
threshold-plugin-gcp/internal/credential/rotate_test.go
T
Elim Tsiagbey 877d285eef feat: update retry logic for validation call (#26)
* 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
2025-01-07 23:23:46 -05:00

463 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"context"
"errors"
"testing"
"time"
"cloud.google.com/go/iam/admin/apiv1/adminpb"
"cloud.google.com/go/iam/apiv1/iampb"
"cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"github.com/stretchr/testify/require"
googleOption "google.golang.org/api/option"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
)
func TestRotateServiceAccountKey(t *testing.T) {
testIAMAdminServer := &testIAMAdminServer{}
testResourceServer := &testResourceServer{
testIamPermissionsResponse: &iampb.TestIamPermissionsResponse{
Permissions: []string{
ComputeInstancesListPermission,
IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission,
},
},
}
testValidateTimeout := 1 * time.Second
tests := []struct {
name string
config *Config
setup func()
expectedError error
}{
{
name: "PrivateKey is empty",
config: &Config{
PrivateKey: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot rotate credentials when private key is not set"),
},
{
name: "PrivateKeyId is empty",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot rotate credentials when private key ID is not set"),
},
{
name: "ClientEmail is empty",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot rotate credentials when client email is not set"),
},
{
name: "CreateServiceAccountKey returns error",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
},
setup: func() {
testIAMAdminServer.testCreateServiceAccountKeyError = errors.New("create key error")
},
expectedError: errors.New("create key error"),
},
{
name: "DeleteServiceAccountKey returns error",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
},
setup: func() {
testIAMAdminServer.testCreateServiceAccountKeyError = nil
testIAMAdminServer.testDeleteServiceAccountKeyError = errors.New("delete key error")
},
expectedError: errors.New("delete key error"),
},
{
name: "Successful rotation",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
},
setup: func() {
testIAMAdminServer.testCreateServiceAccountKeyError = nil
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
},
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gsrv := NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
addr, err := gsrv.Start()
require.NoError(t, err)
ctx := context.Background()
if tt.setup != nil {
tt.setup()
}
testOptions := []googleOption.ClientOption{
googleOption.WithEndpoint(addr),
googleOption.WithoutAuthentication(),
googleOption.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
googleOption.WithTokenSource(nil),
}
initialPrivateKey := tt.config.PrivateKey
initialPrivateKeyId := tt.config.PrivateKeyId
initialClientEmail := tt.config.ClientEmail
permissions := []string{
ComputeInstancesListPermission,
IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission,
}
validateCredsCallbackCalled := false
validateCredsCallBack := func(c *Config, opts ...googleOption.ClientOption) error {
validateCredsCallbackCalled = true
return nil
}
err = tt.config.RotateServiceAccountKey(ctx, permissions, validateCredsCallBack, testOptions...)
if tt.expectedError != nil {
require.ErrorContains(t, err, tt.expectedError.Error())
require.Equal(t, initialClientEmail, tt.config.ClientEmail)
require.Equal(t, initialPrivateKey, tt.config.PrivateKey)
require.Equal(t, initialPrivateKeyId, tt.config.PrivateKeyId)
return
}
require.Equal(t, initialClientEmail, tt.config.ClientEmail)
require.NotEmpty(t, tt.config.PrivateKey)
require.NotEmpty(t, tt.config.PrivateKeyId)
require.NotEqual(t, initialPrivateKey, tt.config.PrivateKey)
require.NotEqual(t, initialPrivateKeyId, tt.config.PrivateKeyId)
require.True(t, validateCredsCallbackCalled)
})
}
}
func TestValidateIamPermissions(t *testing.T) {
ctx := context.Background()
testResourceServer := &testResourceServer{}
tests := []struct {
name string
config *Config
testIamResponse *iampb.TestIamPermissionsResponse
testIamPermissionsError error
permissions []string
wantErr bool
expectedErr string
expectedPermissions []string
}{
{
name: "Successful IAM Permissions Test",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
},
testIamResponse: &iampb.TestIamPermissionsResponse{
Permissions: []string{"permission1", "permission2"},
},
permissions: []string{"permission1", "permission2"},
expectedPermissions: []string{"permission1", "permission2"},
},
{
name: "Missing Permissions",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
},
wantErr: true,
expectedErr: "permissions are required",
},
{
name: "Failed to Test IAM Permissions",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
},
testIamPermissionsError: errors.New("failed to test IAM permissions"),
permissions: []string{"permission1", "permission2"},
wantErr: true,
expectedErr: "failed to test IAM permissions",
},
{
name: "No Permissions Granted",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
},
testIamResponse: &iampb.TestIamPermissionsResponse{
Permissions: []string{},
},
testIamPermissionsError: nil,
permissions: []string{"permission1", "permission2"},
wantErr: true,
expectedErr: "no permissions granted",
},
{
name: "Missing Permissions",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
},
testIamResponse: &iampb.TestIamPermissionsResponse{
Permissions: []string{"permission1"},
},
testIamPermissionsError: nil,
permissions: []string{"permission1", "permission2"},
wantErr: true,
expectedErr: "missing permissions: [permission2]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testResourceServer.testIamPermissionsResponse = tt.testIamResponse
testResourceServer.testIamPermissionsError = tt.testIamPermissionsError
gsrv := NewGRPCServer()
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
addr, err := gsrv.Start()
require.NoError(t, err)
testOptions := []googleOption.ClientOption{
googleOption.WithEndpoint(addr),
googleOption.WithoutAuthentication(),
googleOption.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
googleOption.WithTokenSource(nil),
}
permissions, err := tt.config.ValidateIamPermissions(ctx, tt.permissions, testOptions...)
if tt.wantErr {
require.ErrorContains(t, err, tt.expectedErr)
require.Nil(t, permissions)
return
}
require.NoError(t, err)
require.Equal(t, tt.expectedPermissions, permissions)
})
}
}
func TestValidateServiceAccountKey(t *testing.T) {
ctx := context.Background()
testResourceServer := &testResourceServer{}
testValidateTimeout := 1 * time.Second
tests := []struct {
name string
config *Config
permissions []string
validateCredsCallback ValidateCredsCallback
testIamPermissionsError error
expectedError error
}{
{
name: "Successful validation",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
},
permissions: []string{
ComputeInstancesListPermission,
IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission,
},
validateCredsCallback: func(c *Config, opts ...googleOption.ClientOption) error {
return nil
},
expectedError: nil,
},
{
name: "Validation callback returns error",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
},
permissions: []string{
ComputeInstancesListPermission,
IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission,
},
validateCredsCallback: func(c *Config, opts ...googleOption.ClientOption) error {
return errors.New("validation callback error")
},
expectedError: errors.New("validation callback error"),
},
{
name: "IAM permissions validation returns error",
config: &Config{
ProjectId: "test-project-id",
PrivateKey: "test-private-key",
ClientEmail: "test@test.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
},
permissions: []string{
ComputeInstancesListPermission,
IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission,
},
testIamPermissionsError: errors.New("IAM permissions error"),
expectedError: errors.New("failed to validate IAM permissions"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testResourceServer.testIamPermissionsResponse = &iampb.TestIamPermissionsResponse{
Permissions: tt.permissions,
}
testResourceServer.testIamPermissionsError = tt.testIamPermissionsError
gsrv := NewGRPCServer()
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
addr, err := gsrv.Start()
require.NoError(t, err)
testOptions := []googleOption.ClientOption{
googleOption.WithEndpoint(addr),
googleOption.WithoutAuthentication(),
googleOption.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
googleOption.WithTokenSource(nil),
}
err = tt.config.ValidateServiceAccountKey(ctx, tt.permissions, tt.validateCredsCallback, testOptions...)
if tt.expectedError != nil {
require.ErrorContains(t, err, tt.expectedError.Error())
return
}
require.NoError(t, err)
})
}
}
func TestDeletePrivateKey(t *testing.T) {
testIAMAdminServer := &testIAMAdminServer{}
tests := []struct {
name string
config *Config
setup func()
expectedError error
}{
{
name: "PrivateKey is empty",
config: &Config{
PrivateKey: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot delete credentials when private key is not set"),
},
{
name: "PrivateKeyId is empty",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot delete credentials when private key ID is not set"),
},
{
name: "ClientEmail is empty",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "",
},
expectedError: status.Error(codes.InvalidArgument, "cannot delete credentials when client email is not set"),
},
{
name: "GenerateCredentials returns error",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
TargetServiceAccountId: "test-service-account-id",
},
expectedError: status.Errorf(codes.Unauthenticated, "error generating credentials"),
},
{
name: "DeleteServiceAccountKey returns error",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
},
setup: func() {
testIAMAdminServer.testDeleteServiceAccountKeyError = errors.New("delete key error")
},
expectedError: status.Errorf(codes.Unknown, "error deleting service account key"),
},
{
name: "Successful deletion",
config: &Config{
PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com",
},
setup: func() {
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
},
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gsrv := NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
addr, err := gsrv.Start()
require.NoError(t, err)
ctx := context.Background()
if tt.setup != nil {
tt.setup()
}
testOptions := []googleOption.ClientOption{
googleOption.WithEndpoint(addr),
googleOption.WithoutAuthentication(),
googleOption.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
googleOption.WithTokenSource(nil),
}
err = tt.config.DeletePrivateKey(ctx, testOptions...)
if tt.expectedError != nil {
require.ErrorContains(t, err, tt.expectedError.Error())
return
}
require.NoError(t, err)
})
}
}