From 401d80c88b1205abfb1521b342108fae60f933a9 Mon Sep 17 00:00:00 2001 From: Damian Debkowski Date: Tue, 10 Sep 2024 21:35:09 +0000 Subject: [PATCH] backport of commit 972fcc16661453f0405a3ef0d3d7578bd5dec41e --- .../storage_bucket_credential_test.go | 115 +++++ .../storage_bucket_credential.go | 3 + .../storage_bucket_credential_test.go | 397 ++++++++++++++++++ 3 files changed, 515 insertions(+) create mode 100644 internal/storage/storagebucketcredential/environmental/storage_bucket_credential_test.go create mode 100644 internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential_test.go diff --git a/internal/storage/storagebucketcredential/environmental/storage_bucket_credential_test.go b/internal/storage/storagebucketcredential/environmental/storage_bucket_credential_test.go new file mode 100644 index 0000000000..b53f3bacdf --- /dev/null +++ b/internal/storage/storagebucketcredential/environmental/storage_bucket_credential_test.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package environmental + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/storage/plugin/store" + "github.com/hashicorp/boundary/internal/storage/storagebucketcredential" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +func Test_HmacSecrets(t *testing.T) { + wrapper := db.TestWrapper(t) + sbc := &StorageBucketCredential{ + StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{}, + } + acutalHmac, err := sbc.HmacSecrets(context.Background(), wrapper) + require.Error(t, err) + require.ErrorContains(t, err, "HmacSecrets not implemented") + require.Empty(t, acutalHmac) +} + +func Test_Encrypt(t *testing.T) { + wrapper := db.TestWrapper(t) + sbc := &StorageBucketCredential{ + StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{}, + } + err := sbc.Encrypt(context.Background(), wrapper) + require.Error(t, err) + require.ErrorContains(t, err, "Encrypt not implemented") +} + +func Test_Decrypt(t *testing.T) { + wrapper := db.TestWrapper(t) + sbc := &StorageBucketCredential{ + StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{}, + } + err := sbc.Decrypt(context.Background(), wrapper) + require.Error(t, err) + require.ErrorContains(t, err, "Decrypt not implemented") +} + +func Test_ToPersisted(t *testing.T) { + sbc := &StorageBucketCredential{ + StorageBucketCredentialEnvironmental: &store.StorageBucketCredentialEnvironmental{}, + } + actualPersistedData, err := sbc.ToPersisted(context.Background()) + require.Error(t, err) + require.ErrorContains(t, err, "ToPersisted not implemented") + require.Nil(t, actualPersistedData) +} + +func Test_NewStorageBucketCredential(t *testing.T) { + wrapper := db.TestWrapper(t) + testKeyId, err := wrapper.KeyId(context.Background()) + require.NoError(t, err) + tests := []struct { + name string + storageBucketId string + opts []storagebucketcredential.Option + expectedErrMsg string + }{ + { + name: "missing-storage-bucket-id", + expectedErrMsg: "missing storage bucket id", + }, + { + name: "ignore-secrets", + storageBucketId: "sb_1234567890", + opts: []storagebucketcredential.Option{ + storagebucketcredential.WithSecret(&structpb.Struct{ + Fields: make(map[string]*structpb.Value), + }), + }, + }, + { + name: "ignore-key", + storageBucketId: "sb_1234567890", + opts: []storagebucketcredential.Option{ + storagebucketcredential.WithKeyId(testKeyId), + }, + }, + { + name: "valid", + storageBucketId: "sb_1234567890", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + sbc := sbcHooks{} + actualSBC, err := sbc.NewStorageBucketCredential(context.Background(), tt.storageBucketId, tt.opts...) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.Nil(actualSBC) + assert.ErrorContains(err, tt.expectedErrMsg) + return + } + require.NoError(err) + require.NotNil(actualSBC) + assert.Equal(tt.storageBucketId, actualSBC.GetStorageBucketId()) + assert.Empty(actualSBC.GetPrivateId()) + assert.Empty(actualSBC.GetSecrets()) + assert.Empty(actualSBC.GetCtSecrets()) + assert.Empty(actualSBC.GetKeyId()) + }) + } +} diff --git a/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential.go b/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential.go index f0878bb361..90a3cf2250 100644 --- a/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential.go +++ b/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential.go @@ -60,6 +60,9 @@ func (h sbcHooks) NewStorageBucketCredential( if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to marshal secrets")) } + if len(secrets) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty secret") + } } sbc := &StorageBucketCredential{ diff --git a/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential_test.go b/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential_test.go new file mode 100644 index 0000000000..e656046fc2 --- /dev/null +++ b/internal/storage/storagebucketcredential/managedsecret/storage_bucket_credential_test.go @@ -0,0 +1,397 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package managedsecret + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/storage/plugin/store" + "github.com/hashicorp/boundary/internal/storage/storagebucketcredential" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +func Test_HmacSecrets(t *testing.T) { + // emptyPersistedData represents the data returned by the AWS plugin when using an unknown or dynamic credential type. + emptyPersistedData, err := structpb.NewStruct(map[string]any{}) + require.NoError(t, err) + testEmptySecret, err := proto.Marshal(emptyPersistedData) + require.NoError(t, err) + + wrapper := db.TestWrapper(t) + tests := []struct { + name string + cipher wrapping.Wrapper + sbc *StorageBucketCredential + expectedErrMsg string + }{ + { + name: "missing-cipher", + expectedErrMsg: "missing cipher", + }, + { + name: "nil-secret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{}, + }, + cipher: wrapper, + expectedErrMsg: "failed to hmac secrets: encryption issue: error #300: plugin.hmacField: unknown, unknown: error #0: crypto.HmacSha256: missing data: Invalid parameter", + }, + { + name: "empty-secret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte{}, + }, + }, + cipher: wrapper, + }, + { + name: "aws-empty-persisted-data", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: testEmptySecret, + }, + }, + cipher: wrapper, + }, + { + name: "valid", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte("hello world"), + }, + }, + cipher: wrapper, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + actualHmac, err := tt.sbc.HmacSecrets(context.Background(), tt.cipher) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.ErrorContains(err, tt.expectedErrMsg) + assert.Empty(actualHmac) + return + } + require.NoError(err) + assert.NotEmpty(actualHmac) + }) + } +} + +func Test_Encrypt(t *testing.T) { + // emptyPersistedData represents the data returned by the AWS plugin when using an unknown or dynamic credential type. + emptyPersistedData, err := structpb.NewStruct(map[string]any{}) + require.NoError(t, err) + testEmptySecret, err := proto.Marshal(emptyPersistedData) + require.NoError(t, err) + + wrapper := db.TestWrapper(t) + tests := []struct { + name string + cipher wrapping.Wrapper + sbc *StorageBucketCredential + expectedErrMsg string + }{ + { + name: "missing-cipher", + expectedErrMsg: "missing cipher", + }, + { + name: "nil-secret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{}, + }, + cipher: wrapper, + expectedErrMsg: "error occurred during encrypt, encryption issue: error #300: plaintext byte slice is nil", + }, + { + name: "empty-secret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte{}, + }, + }, + cipher: wrapper, + }, + { + name: "aws-empty-persisted-data", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: testEmptySecret, + }, + }, + cipher: wrapper, + }, + { + name: "valid", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte("hello world"), + }, + }, + cipher: wrapper, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + err := tt.sbc.Encrypt(context.Background(), tt.cipher) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.ErrorContains(err, tt.expectedErrMsg) + if tt.sbc != nil { + assert.Empty(tt.sbc.GetCtSecrets()) + assert.Empty(tt.sbc.GetKeyId()) + } + return + } + require.NoError(err) + assert.NotEmpty(tt.sbc.GetCtSecrets()) + assert.NotEmpty(tt.sbc.GetKeyId()) + }) + } +} + +func Test_Decrypt(t *testing.T) { + // emptyPersistedData represents the data returned by the AWS plugin when using an unknown or dynamic credential type. + emptyPersistedData, err := structpb.NewStruct(map[string]any{}) + require.NoError(t, err) + testEmptySecret, err := proto.Marshal(emptyPersistedData) + require.NoError(t, err) + + wrapper := db.TestWrapper(t) + tests := []struct { + name string + cipher wrapping.Wrapper + sbc *StorageBucketCredential + expectedSecret []byte + expectedErrMsg string + }{ + { + name: "missing-cipher", + expectedErrMsg: "missing cipher", + }, + { + name: "nil-ctsecret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{}, + }, + cipher: wrapper, + expectedErrMsg: "error occurred during decrypt, encryption issue: error #301: ciphertext pointer is nil", + }, + { + name: "empty-ctsecret", + sbc: func() *StorageBucketCredential { + sbc := &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte{}, + }, + } + require.NoError(t, sbc.Encrypt(context.Background(), wrapper)) + return sbc + }(), + cipher: wrapper, + expectedSecret: []byte(nil), + }, + { + name: "aws-empty-persisted-data", + sbc: func() *StorageBucketCredential { + sbc := &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: testEmptySecret, + }, + } + require.NoError(t, sbc.Encrypt(context.Background(), wrapper)) + return sbc + }(), + cipher: wrapper, + expectedSecret: []byte(nil), + }, + { + name: "valid", + sbc: func() *StorageBucketCredential { + sbc := &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: []byte("hello world"), + }, + } + require.NoError(t, sbc.Encrypt(context.Background(), wrapper)) + return sbc + }(), + cipher: wrapper, + expectedSecret: []byte("hello world"), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + err := tt.sbc.Decrypt(context.Background(), tt.cipher) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.ErrorContains(err, tt.expectedErrMsg) + if tt.sbc != nil { + assert.Empty(tt.sbc.GetCtSecrets()) + assert.Empty(tt.sbc.GetSecrets()) + } + return + } + require.NoError(err) + assert.Empty(tt.sbc.GetCtSecrets()) + assert.Equal(tt.expectedSecret, tt.sbc.GetSecrets()) + }) + } +} + +func Test_ToPersisted(t *testing.T) { + secret := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"), + "AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"), + }, + } + testByteSecret, err := proto.Marshal(secret) + require.NoError(t, err) + tests := []struct { + name string + sbc *StorageBucketCredential + expectedErrMsg string + }{ + { + name: "nil-secret", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{}, + }, + expectedErrMsg: "secret data not populated", + }, + { + name: "valid", + sbc: &StorageBucketCredential{ + StorageBucketCredentialManagedSecret: &store.StorageBucketCredentialManagedSecret{ + Secrets: testByteSecret, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + + actualPersistedData, err := tt.sbc.ToPersisted(context.Background()) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.ErrorContains(err, tt.expectedErrMsg) + return + } + require.NoError(err) + require.NotNil(actualPersistedData) + require.NotNil(actualPersistedData.GetData()) + actualSecret := actualPersistedData.GetData().GetFields() + assert.NotEmpty(actualSecret) + assert.Len(actualSecret, len(secret.GetFields())) + for key, expectedValue := range secret.GetFields() { + actualValue, ok := actualSecret[key] + require.True(ok) + require.Equal(expectedValue.GetKind(), actualValue.GetKind()) + require.Equal(expectedValue.GetStringValue(), actualValue.GetStringValue()) + } + }) + } +} + +func Test_NewStorageBucketCredential(t *testing.T) { + wrapper := db.TestWrapper(t) + testKeyId, err := wrapper.KeyId(context.Background()) + require.NoError(t, err) + tests := []struct { + name string + storageBucketId string + opts []storagebucketcredential.Option + expectedKeyId string + expectedErrMsg string + }{ + { + name: "missing-storage-bucket-id", + expectedErrMsg: "missing storage bucket id", + }, + { + name: "nil secret", + storageBucketId: "sb_1234567890", + expectedErrMsg: "missing secret", + }, + { + name: "empty secret", + storageBucketId: "sb_1234567890", + opts: []storagebucketcredential.Option{ + storagebucketcredential.WithSecret(&structpb.Struct{ + Fields: make(map[string]*structpb.Value), + }), + }, + expectedErrMsg: "empty secret", + }, + { + name: "valid", + storageBucketId: "sb_1234567890", + opts: []storagebucketcredential.Option{ + storagebucketcredential.WithSecret(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "hello": structpb.NewStringValue("world"), + }, + }), + }, + }, + { + name: "valid-with-key", + storageBucketId: "sb_1234567890", + opts: []storagebucketcredential.Option{ + storagebucketcredential.WithSecret(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "hello": structpb.NewStringValue("world"), + }, + }), + storagebucketcredential.WithKeyId(testKeyId), + }, + expectedKeyId: testKeyId, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + sbc := sbcHooks{} + actualSBC, err := sbc.NewStorageBucketCredential(context.Background(), tt.storageBucketId, tt.opts...) + if tt.expectedErrMsg != "" { + require.Error(err) + assert.Nil(actualSBC) + assert.ErrorContains(err, tt.expectedErrMsg) + return + } + require.NoError(err) + require.NotNil(actualSBC) + assert.NotEmpty(actualSBC.GetSecrets()) + assert.Equal(tt.storageBucketId, actualSBC.GetStorageBucketId()) + assert.Empty(actualSBC.GetPrivateId()) + assert.Empty(actualSBC.GetCtSecrets()) + if tt.expectedKeyId != "" { + assert.Equal(tt.expectedKeyId, actualSBC.GetKeyId()) + } else { + assert.Empty(actualSBC.GetKeyId()) + } + }) + } +}