Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3ec42668 |
No files matched your search
@@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,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{}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user