Compare commits

..
Author SHA1 Message Date
Elim Tsiagbey bd3ec42668 fix: extra credential generation
When rotating credentials, `ValidatePermission` function generates a credentials but credentials are already passed to it with client.Options. This is currently causing a bug where GCP sometimes does not accept the second generated token
2024-12-13 10:08:04 -05:00
12 changed files with 78 additions and 418 deletions

No files matched your search

+22 -27
View File
@@ -119,17 +119,15 @@ func TestGetCredentialsConfig(t *testing.T) {
Zone: "us-central-1a", Zone: "us-central-1a",
}, },
expected: &Config{ expected: &Config{
Zone: "us-central-1a", Zone: "us-central-1a",
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
}, },
{ {
name: "nil attributes", name: "nil attributes",
attrs: nil, attrs: nil,
expected: &Config{ expected: &Config{
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
}, },
{ {
@@ -156,13 +154,12 @@ func TestGetCredentialsConfig(t *testing.T) {
ClientEmail: "test@test.com", ClientEmail: "test@test.com",
}, },
expected: &Config{ expected: &Config{
ProjectId: "test-project", ProjectId: "test-project",
Zone: "us-central-1a", Zone: "us-central-1a",
ClientEmail: "test@test.com", ClientEmail: "test@test.com",
PrivateKey: "test-private-key", PrivateKey: "test-private-key",
PrivateKeyId: "test-private-key-id", PrivateKeyId: "test-private-key-id",
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
}, },
{ {
@@ -223,13 +220,12 @@ func TestGetCredentialsConfig(t *testing.T) {
Zone: "us-central-1a", Zone: "us-central-1a",
}, },
expected: &Config{ expected: &Config{
ProjectId: "test-project", ProjectId: "test-project",
Zone: "us-central-1a", Zone: "us-central-1a",
PrivateKey: "test-private-key", PrivateKey: "test-private-key",
PrivateKeyId: "test-private-key-id", PrivateKeyId: "test-private-key-id",
ClientEmail: "test@test.com", ClientEmail: "test@test.com",
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
}, },
{ {
@@ -245,14 +241,13 @@ func TestGetCredentialsConfig(t *testing.T) {
TargetServiceAccountId: "test-target-service-account", TargetServiceAccountId: "test-target-service-account",
}, },
expected: &Config{ expected: &Config{
ProjectId: "test-project", ProjectId: "test-project",
Zone: "us-central-1a", Zone: "us-central-1a",
PrivateKey: "test-private-key", PrivateKey: "test-private-key",
PrivateKeyId: "test-private-key-id", PrivateKeyId: "test-private-key-id",
ClientEmail: "test@test.com", ClientEmail: "test@test.com",
TargetServiceAccountId: "test-target-service-account", TargetServiceAccountId: "test-target-service-account",
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
}, },
} }
+20 -27
View File
@@ -41,21 +41,17 @@ const (
// and then controlling the service account's access by granting it IAM roles. // and then controlling the service account's access by granting it IAM roles.
// https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice // https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
defaultGCPScope = "https://www.googleapis.com/auth/cloud-platform" defaultGCPScope = "https://www.googleapis.com/auth/cloud-platform"
// defaultValidateServiceAccountKeyTimeout is the timeout for validating
// a service account key.
defaultValidateServiceAccountKeyTimeout = 5 * time.Second
) )
// Config is the configuration for the GCP credential. // Config is the configuration for the GCP credential.
type Config struct { type Config struct {
ProjectId string ProjectId string
PrivateKey string PrivateKey string
PrivateKeyId string PrivateKeyId string
Zone string Zone string
ClientEmail string ClientEmail string
TargetServiceAccountId string TargetServiceAccountId string
Scopes []string Scopes []string
validateServiceAccountKeyTimeout time.Duration
} }
// credentials represents a simplified version of the GCP credentials file format. // credentials represents a simplified version of the GCP credentials file format.
@@ -76,14 +72,13 @@ func NewConfig(opt ...Option) (*Config, error) {
return nil, err return nil, err
} }
c := &Config{ c := &Config{
ProjectId: opts.WithProjectId, ProjectId: opts.WithProjectId,
PrivateKey: opts.WithPrivateKey, PrivateKey: opts.WithPrivateKey,
PrivateKeyId: opts.WithPrivateKeyId, PrivateKeyId: opts.WithPrivateKeyId,
ClientEmail: opts.WithClientEmail, ClientEmail: opts.WithClientEmail,
TargetServiceAccountId: opts.WithTargetServiceAccountId, TargetServiceAccountId: opts.WithTargetServiceAccountId,
Zone: opts.WithZone, Zone: opts.WithZone,
Scopes: opts.WithScopes, Scopes: opts.WithScopes,
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
} }
return c, nil return c, nil
} }
@@ -102,14 +97,12 @@ func (c *Config) toCredentials() *credentials {
// clone returns a copy of the configuration. // clone returns a copy of the configuration.
func (c *Config) clone() *Config { func (c *Config) clone() *Config {
return &Config{ return &Config{
ProjectId: c.ProjectId, ProjectId: c.ProjectId,
Zone: c.Zone, PrivateKey: c.PrivateKey,
PrivateKey: c.PrivateKey, PrivateKeyId: c.PrivateKeyId,
PrivateKeyId: c.PrivateKeyId, ClientEmail: c.ClientEmail,
ClientEmail: c.ClientEmail, TargetServiceAccountId: c.TargetServiceAccountId,
TargetServiceAccountId: c.TargetServiceAccountId, Scopes: c.Scopes,
Scopes: c.Scopes,
validateServiceAccountKeyTimeout: c.validateServiceAccountKeyTimeout,
} }
} }
+15 -18
View File
@@ -403,14 +403,13 @@ func TestNewConfig(t *testing.T) {
WithScopes([]string{"scope1", "scope2"}), WithScopes([]string{"scope1", "scope2"}),
}, },
expected: &Config{ expected: &Config{
ProjectId: "test-project-id", ProjectId: "test-project-id",
PrivateKey: "test-private-key", PrivateKey: "test-private-key",
PrivateKeyId: "test-private-key-id", PrivateKeyId: "test-private-key-id",
ClientEmail: "test-email@example.com", ClientEmail: "test-email@example.com",
TargetServiceAccountId: "target-service-account@example.com", TargetServiceAccountId: "target-service-account@example.com",
Zone: "test-zone", Zone: "test-zone",
Scopes: []string{"scope1", "scope2"}, Scopes: []string{"scope1", "scope2"},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
expectError: false, expectError: false,
}, },
@@ -418,8 +417,7 @@ func TestNewConfig(t *testing.T) {
name: "No Options", name: "No Options",
options: []Option{}, options: []Option{},
expected: &Config{ expected: &Config{
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
expectError: false, expectError: false,
}, },
@@ -444,14 +442,13 @@ func TestNewConfig(t *testing.T) {
WithZone("test-zone"), WithZone("test-zone"),
}, },
expected: &Config{ expected: &Config{
ProjectId: "test-project-id", ProjectId: "test-project-id",
PrivateKey: "test-private-key", PrivateKey: "test-private-key",
PrivateKeyId: "test-private-key-id", PrivateKeyId: "test-private-key-id",
ClientEmail: "test-email@example.com", ClientEmail: "test-email@example.com",
TargetServiceAccountId: "target-service-account@example.com", TargetServiceAccountId: "target-service-account@example.com",
Zone: "test-zone", Zone: "test-zone",
Scopes: []string{defaultGCPScope}, Scopes: []string{defaultGCPScope},
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
}, },
expectError: false, expectError: false,
}, },
+9 -62
View File
@@ -6,10 +6,8 @@ package credential
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"slices" "slices"
"time"
admin "cloud.google.com/go/iam/admin/apiv1" admin "cloud.google.com/go/iam/admin/apiv1"
"cloud.google.com/go/iam/admin/apiv1/adminpb" "cloud.google.com/go/iam/admin/apiv1/adminpb"
@@ -21,16 +19,8 @@ import (
) )
const ( const (
// ComputeInstancesListPermission is the IAM permission required ComputeInstancesListPermission = "compute.instances.list"
// to list compute instances.
ComputeInstancesListPermission = "compute.instances.list"
// IAMServiceAccountKeysCreatePermission is the IAM permission
// required to create a service account key.
// This permission is required to rotate service account keys.
IAMServiceAccountKeysCreatePermission = "iam.serviceAccountKeys.create" IAMServiceAccountKeysCreatePermission = "iam.serviceAccountKeys.create"
// IAMServiceAccountKeysDeletePermission is the IAM permission
// required to delete a service account key.
// This permission is required to rotate service account keys.
IAMServiceAccountKeysDeletePermission = "iam.serviceAccountKeys.delete" IAMServiceAccountKeysDeletePermission = "iam.serviceAccountKeys.delete"
) )
@@ -50,10 +40,6 @@ type ServiceAccountPrivateKey struct {
ClientX509CertURL string `json:"client_x509_cert_url"` ClientX509CertURL string `json:"client_x509_cert_url"`
} }
// ValidateCredsCallback is called to validate the credentials after rotating the service account key.
// The callback should return an error if the credentials are invalid.
type ValidateCredsCallback func(*Config, ...option.ClientOption) error
// RotateServiceAccountKey takes the private key from this credentials config // RotateServiceAccountKey takes the private key from this credentials config
// and first creates a new private key and private key id, then deletes the // and first creates a new private key and private key id, then deletes the
// old private key. // old private key.
@@ -61,12 +47,7 @@ type ValidateCredsCallback func(*Config, ...option.ClientOption) error
// If deletion of the old private key is successful, the new private key and // If deletion of the old private key is successful, the new private key and
// private key id are written into the credentials config and nil is returned. // private key id are written into the credentials config and nil is returned.
// On any error, the old credentials are not overwritten. // On any error, the old credentials are not overwritten.
func (c *Config) RotateServiceAccountKey( func (c *Config) RotateServiceAccountKey(ctx context.Context, permissions []string, opts ...option.ClientOption) error {
ctx context.Context,
permissions []string,
validateCredsCallback ValidateCredsCallback,
opts ...option.ClientOption,
) error {
if c.PrivateKey == "" { if c.PrivateKey == "" {
return status.Error(codes.InvalidArgument, "cannot rotate credentials when private key is not set") return status.Error(codes.InvalidArgument, "cannot rotate credentials when private key is not set")
} }
@@ -123,10 +104,10 @@ func (c *Config) RotateServiceAccountKey(
clientOptions = []option.ClientOption{option.WithTokenSource(newCreds.TokenSource)} clientOptions = []option.ClientOption{option.WithTokenSource(newCreds.TokenSource)}
clientOptions = append(clientOptions, opts...) clientOptions = append(clientOptions, opts...)
// Validate the new service account key // Validate that the new credentials have the necessary permissions.
err = newConfig.ValidateServiceAccountKey(ctx, permissions, validateCredsCallback, clientOptions...) _, err = newConfig.ValidateIamPermissions(ctx, permissions, clientOptions...)
if err != nil { if err != nil {
return status.Errorf(codes.PermissionDenied, "error validating rotated service account key: %v", err) return status.Errorf(codes.PermissionDenied, "error testing IAM permissions with rotated service account key: %v", err)
} }
iamClient, err = admin.NewIamClient(ctx, clientOptions...) iamClient, err = admin.NewIamClient(ctx, clientOptions...)
@@ -147,44 +128,6 @@ func (c *Config) RotateServiceAccountKey(
return nil return nil
} }
// ValidateServiceAccountKey validates the service account key by checking the IAM permissions
// and calling the validation callback.
// The function will retry validation until the timeout is reached.
func (c *Config) ValidateServiceAccountKey(
ctx context.Context,
permissions []string,
validateCredsCallback ValidateCredsCallback,
opts ...option.ClientOption) error {
var validatePermissionsErr error
var validationCallbackErr error
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(ctx, c.validateServiceAccountKeyTimeout)
defer cancel()
for {
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("failed to validate service account key before timeout: %w", errors.Join(validatePermissionsErr, validationCallbackErr))
}
return ctx.Err()
case <-ticker.C:
_, validatePermissionsErr = c.ValidateIamPermissions(ctx, permissions, opts...)
if validateCredsCallback != nil {
validationCallbackErr = validateCredsCallback(c, opts...)
}
if validatePermissionsErr == nil && validationCallbackErr == nil {
return nil
}
}
}
}
func (c *Config) DeletePrivateKey(ctx context.Context, opts ...option.ClientOption) error { func (c *Config) DeletePrivateKey(ctx context.Context, opts ...option.ClientOption) error {
if c.PrivateKey == "" { if c.PrivateKey == "" {
return status.Error(codes.InvalidArgument, "cannot delete credentials when private key is not set") return status.Error(codes.InvalidArgument, "cannot delete credentials when private key is not set")
@@ -224,6 +167,10 @@ func (c *Config) DeletePrivateKey(ctx context.Context, opts ...option.ClientOpti
// ValidateIamPermissions tests the IAM permissions for the credentials. // ValidateIamPermissions tests the IAM permissions for the credentials.
// It returns the granted permissions if successful. // It returns the granted permissions if successful.
//
// The caller needs to ensure either a token source is passed in for
// authentication or authentication is provided by Application Default
// Credentials
func (c *Config) ValidateIamPermissions(ctx context.Context, permissions []string, opts ...option.ClientOption) ([]string, error) { func (c *Config) ValidateIamPermissions(ctx context.Context, permissions []string, opts ...option.ClientOption) ([]string, error) {
if len(permissions) == 0 { if len(permissions) == 0 {
return nil, status.Error(codes.InvalidArgument, "permissions are required") return nil, status.Error(codes.InvalidArgument, "permissions are required")
+7 -116
View File
@@ -7,7 +7,6 @@ import (
"context" "context"
"errors" "errors"
"testing" "testing"
"time"
"cloud.google.com/go/iam/admin/apiv1/adminpb" "cloud.google.com/go/iam/admin/apiv1/adminpb"
"cloud.google.com/go/iam/apiv1/iampb" "cloud.google.com/go/iam/apiv1/iampb"
@@ -31,7 +30,6 @@ func TestRotateServiceAccountKey(t *testing.T) {
}, },
}, },
} }
testValidateTimeout := 1 * time.Second
tests := []struct { tests := []struct {
name string name string
@@ -78,10 +76,9 @@ func TestRotateServiceAccountKey(t *testing.T) {
{ {
name: "DeleteServiceAccountKey returns error", name: "DeleteServiceAccountKey returns error",
config: &Config{ config: &Config{
PrivateKey: "some-private-key", PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id", PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com", ClientEmail: "client@example.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
}, },
setup: func() { setup: func() {
testIAMAdminServer.testCreateServiceAccountKeyError = nil testIAMAdminServer.testCreateServiceAccountKeyError = nil
@@ -92,10 +89,9 @@ func TestRotateServiceAccountKey(t *testing.T) {
{ {
name: "Successful rotation", name: "Successful rotation",
config: &Config{ config: &Config{
PrivateKey: "some-private-key", PrivateKey: "some-private-key",
PrivateKeyId: "some-private-key-id", PrivateKeyId: "some-private-key-id",
ClientEmail: "client@example.com", ClientEmail: "client@example.com",
validateServiceAccountKeyTimeout: testValidateTimeout,
}, },
setup: func() { setup: func() {
testIAMAdminServer.testCreateServiceAccountKeyError = nil testIAMAdminServer.testCreateServiceAccountKeyError = nil
@@ -132,14 +128,8 @@ func TestRotateServiceAccountKey(t *testing.T) {
IAMServiceAccountKeysCreatePermission, IAMServiceAccountKeysCreatePermission,
IAMServiceAccountKeysDeletePermission, IAMServiceAccountKeysDeletePermission,
} }
validateCredsCallbackCalled := false
validateCredsCallBack := func(c *Config, opts ...googleOption.ClientOption) error { err = tt.config.RotateServiceAccountKey(ctx, permissions, testOptions...)
validateCredsCallbackCalled = true
return nil
}
err = tt.config.RotateServiceAccountKey(ctx, permissions, validateCredsCallBack, testOptions...)
if tt.expectedError != nil { if tt.expectedError != nil {
require.ErrorContains(t, err, tt.expectedError.Error()) require.ErrorContains(t, err, tt.expectedError.Error())
require.Equal(t, initialClientEmail, tt.config.ClientEmail) require.Equal(t, initialClientEmail, tt.config.ClientEmail)
@@ -153,7 +143,6 @@ func TestRotateServiceAccountKey(t *testing.T) {
require.NotEmpty(t, tt.config.PrivateKeyId) require.NotEmpty(t, tt.config.PrivateKeyId)
require.NotEqual(t, initialPrivateKey, tt.config.PrivateKey) require.NotEqual(t, initialPrivateKey, tt.config.PrivateKey)
require.NotEqual(t, initialPrivateKeyId, tt.config.PrivateKeyId) require.NotEqual(t, initialPrivateKeyId, tt.config.PrivateKeyId)
require.True(t, validateCredsCallbackCalled)
}) })
} }
} }
@@ -265,104 +254,6 @@ func TestValidateIamPermissions(t *testing.T) {
}) })
} }
} }
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) { func TestDeletePrivateKey(t *testing.T) {
testIAMAdminServer := &testIAMAdminServer{} testIAMAdminServer := &testIAMAdminServer{}
+2 -6
View File
@@ -38,11 +38,7 @@ func NewPersistedState(opt ...Option) (*PersistedState, error) {
} }
// RotateCreds rotates the credentials for the GCP catalog and updates the last rotated time. // RotateCreds rotates the credentials for the GCP catalog and updates the last rotated time.
func (s *PersistedState) RotateCreds( func (s *PersistedState) RotateCreds(ctx context.Context, permissions []string, opts ...option.ClientOption) error {
ctx context.Context,
permissions []string,
validateCredsCallback ValidateCredsCallback,
opts ...option.ClientOption) error {
if s.CredentialsConfig == nil { if s.CredentialsConfig == nil {
return status.Error(codes.InvalidArgument, "missing credentials config") return status.Error(codes.InvalidArgument, "missing credentials config")
} }
@@ -50,7 +46,7 @@ func (s *PersistedState) RotateCreds(
return status.Error(codes.InvalidArgument, "cannot rotate non-rotatable credentials") return status.Error(codes.InvalidArgument, "cannot rotate non-rotatable credentials")
} }
err := s.CredentialsConfig.RotateServiceAccountKey(ctx, permissions, validateCredsCallback, opts...) err := s.CredentialsConfig.RotateServiceAccountKey(ctx, permissions, opts...)
if err != nil { if err != nil {
return status.Errorf(codes.Internal, "failed to rotate credentials: %v", err) return status.Errorf(codes.Internal, "failed to rotate credentials: %v", err)
} }
+1 -1
View File
@@ -108,7 +108,7 @@ func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttrib
return nil, fmt.Errorf("error building filters: %w", err) return nil, fmt.Errorf("error building filters: %w", err)
} }
if len(filters) > 0 { if len(filters) > 1 {
filters := strings.Join(filters, " AND ") filters := strings.Join(filters, " AND ")
request.Filter = &filters request.Filter = &filters
} }
-17
View File
@@ -183,23 +183,6 @@ func TestBuildListInstancesRequest(t *testing.T) {
}, },
expectedErrContains: "error building filters", expectedErrContains: "error building filters",
}, },
{
name: "valid request with no filters",
attributes: &SetAttributes{
Filters: []string{},
},
catalog: &CatalogAttributes{
CredentialAttributes: &cred.CredentialAttributes{
ProjectId: "test-12345",
Zone: "us-central-1a",
},
},
expected: &computepb.ListInstancesRequest{
Project: "test-12345",
Zone: "us-central-1a",
Filter: stringPtr("status = running"),
},
},
} }
for _, tc := range cases { for _, tc := range cases {
+2 -26
View File
@@ -87,22 +87,8 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
credential.IAMServiceAccountKeysDeletePermission, credential.IAMServiceAccountKeysDeletePermission,
} }
if !credState.CredentialsConfig.IsRotatable() && !catalogAttributes.DisableCredentialRotation {
return nil, status.Error(codes.InvalidArgument, "cannot rotate credentials for non-rotatable credentials")
}
if credState.CredentialsConfig.IsRotatable() && !catalogAttributes.DisableCredentialRotation { if credState.CredentialsConfig.IsRotatable() && !catalogAttributes.DisableCredentialRotation {
state, err := newGCPCatalogPersistedState( if err := credState.RotateCreds(ctx, permissions, p.testGCPClientOpts...); err != nil {
append([]gcpCatalogPersistedStateOption{
withCredentials(credState),
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
}
validateCredsCallback := state.ValidateCredsCallbackFn(ctx, p.testGCPClientOpts...)
if err := credState.RotateCreds(ctx, permissions, validateCredsCallback, p.testGCPClientOpts...); err != nil {
return nil, err return nil, err
} }
} }
@@ -229,17 +215,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
// If we're enabling rotation now but didn't before, or have // If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here. // freshly replaced credentials, we can rotate here.
if !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() { if !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
state, err := newGCPCatalogPersistedState( if err := credState.RotateCreds(ctx, permissions, p.testGCPClientOpts...); err != nil {
append([]gcpCatalogPersistedStateOption{
withCredentials(credState),
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
}
validateCredsCallback := state.ValidateCredsCallbackFn(ctx, p.testGCPClientOpts...)
if err := credState.RotateCreds(ctx, permissions, validateCredsCallback, p.testGCPClientOpts...); err != nil {
return nil, err return nil, err
} }
} }
-47
View File
@@ -878,53 +878,6 @@ func TestCreateCatalog(t *testing.T) {
}, },
}, },
}, },
{
name: "create with application default credentials and credential rotation disabled",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
},
},
},
{
name: "create with application default credentials and credential rotation enabled",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
},
},
},
},
},
expectedErr: "cannot rotate credentials for non-rotatable credentials",
},
} }
for _, tc := range cases { for _, tc := range cases {
-28
View File
@@ -13,7 +13,6 @@ import (
"github.com/googleapis/gax-go/v2" "github.com/googleapis/gax-go/v2"
"github.com/hashicorp/boundary-plugin-gcp/internal/credential" "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin" pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/api/iterator"
"google.golang.org/api/option" "google.golang.org/api/option"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
) )
@@ -100,30 +99,3 @@ func (s *gcpCatalogPersistedState) InstancesClient(
return client, nil return client, nil
} }
// validateCredsCallbackFn returns a ValidateCredsCallback function that
// validates the credentials by listing instances in the project and zone.
func (s *gcpCatalogPersistedState) ValidateCredsCallbackFn(
ctx context.Context,
opts ...option.ClientOption,
) credential.ValidateCredsCallback {
return func(config *credential.Config, opts ...option.ClientOption) error {
s.CredentialsConfig = config
instancesClient, err := s.InstancesClient(ctx, opts...)
if err != nil {
return fmt.Errorf("error creating instances client: %w", err)
}
it := instancesClient.List(ctx, &computepb.ListInstancesRequest{
Project: s.CredentialsConfig.ProjectId,
Zone: s.CredentialsConfig.Zone,
})
_, err = it.Next()
if err != nil && err != iterator.Done {
return fmt.Errorf("error listing instances: %w", err)
}
return nil
}
}
-43
View File
@@ -5,11 +5,9 @@ package host
import ( import (
"context" "context"
"errors"
"testing" "testing"
"time" "time"
computepb "cloud.google.com/go/compute/apiv1/computepb"
"github.com/hashicorp/boundary-plugin-gcp/internal/credential" "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -89,44 +87,3 @@ func TestInstancesClient(t *testing.T) {
assert.Contains(t, err.Error(), "error creating instances client") assert.Contains(t, err.Error(), "error creating instances client")
}) })
} }
func TestValidateCredsCallbackFn(t *testing.T) {
ctx := context.Background()
cred := &credential.PersistedState{
CredentialsConfig: &credential.Config{
ProjectId: "test-project-id",
Zone: "test-zone",
},
}
state := &gcpCatalogPersistedState{PersistedState: cred}
t.Run("List instances error", func(t *testing.T) {
state.testInstancesAPIFunc = newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(nil),
testMockInstancesWithListInstancesError(errors.New("error listing instances")),
)
callback := state.ValidateCredsCallbackFn(ctx)
err := callback(&credential.Config{
ProjectId: "test-project-id",
Zone: "test-zone",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "error listing instances")
})
t.Run("Success", func(t *testing.T) {
state.testInstancesAPIFunc = newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)
callback := state.ValidateCredsCallbackFn(ctx)
err := callback(&credential.Config{
ProjectId: "test-project-id",
Zone: "test-zone",
})
assert.NoError(t, err)
})
}