diff --git a/pkg/registry/apis/secret/decrypt/service.go b/pkg/registry/apis/secret/decrypt/service.go new file mode 100644 index 00000000000..115c073aee2 --- /dev/null +++ b/pkg/registry/apis/secret/decrypt/service.go @@ -0,0 +1,36 @@ +package decrypt + +import ( + "context" + + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +type OSSDecryptService struct { + decryptStore contracts.DecryptStorage +} + +var _ service.DecryptService = &OSSDecryptService{} + +func ProvideDecryptService(decryptStore contracts.DecryptStorage) *OSSDecryptService { + return &OSSDecryptService{ + decryptStore: decryptStore, + } +} + +func (d *OSSDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { + results := make(map[string]service.DecryptResult, len(names)) + + for _, name := range names { + exposedSecureValue, err := d.decryptStore.Decrypt(ctx, xkube.Namespace(namespace), name) + if err != nil { + results[name] = service.NewDecryptResultErr(err) + } else { + results[name] = service.NewDecryptResultValue(&exposedSecureValue) + } + } + + return results, nil +} diff --git a/pkg/registry/apis/secret/decrypt/service_test.go b/pkg/registry/apis/secret/decrypt/service_test.go new file mode 100644 index 00000000000..665a6730527 --- /dev/null +++ b/pkg/registry/apis/secret/decrypt/service_test.go @@ -0,0 +1,101 @@ +package decrypt + +import ( + "context" + "errors" + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestDecryptService(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("when there are only errors from the storage, the service returns them in the map", func(t *testing.T) { + t.Parallel() + + mockErr := errors.New("mock error") + mockStorage := &MockDecryptStorage{} + mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv0alpha1.ExposedSecureValue(""), mockErr) + decryptedValuesResp := map[string]service.DecryptResult{ + "secure-value-1": service.NewDecryptResultErr(mockErr), + } + + decryptService := &OSSDecryptService{ + decryptStore: mockStorage, + } + + resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1") + require.NotNil(t, resp) + require.NoError(t, err) + require.EqualValues(t, decryptedValuesResp, resp) + }) + + t.Run("when there is no error from the storage, it returns a map of the decrypted values", func(t *testing.T) { + t.Parallel() + + mockStorage := &MockDecryptStorage{} + // Set up the mock to return a different value for each name in the test + exposedSecureValue1 := secretv0alpha1.NewExposedSecureValue("value1") + exposedSecureValue2 := secretv0alpha1.NewExposedSecureValue("value2") + mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). + Return(exposedSecureValue1, nil) + mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). + Return(exposedSecureValue2, nil) + + decryptedValuesResp := map[string]service.DecryptResult{ + "secure-value-1": service.NewDecryptResultValue(&exposedSecureValue1), + "secure-value-2": service.NewDecryptResultValue(&exposedSecureValue2), + } + + decryptService := &OSSDecryptService{ + decryptStore: mockStorage, + } + + resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1", "secure-value-2") + require.NotNil(t, resp) + require.NoError(t, err) + require.EqualValues(t, decryptedValuesResp, resp) + }) + + t.Run("when there is an error from the storage, the service returns a map of errors and decrypted values", func(t *testing.T) { + t.Parallel() + + mockErr := errors.New("mock error") + mockStorage := &MockDecryptStorage{} + exposedSecureValue := secretv0alpha1.NewExposedSecureValue("value") + mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). + Return(exposedSecureValue, nil) + mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). + Return(secretv0alpha1.ExposedSecureValue(""), mockErr) + + decryptedValuesResp := map[string]service.DecryptResult{ + "secure-value-1": service.NewDecryptResultValue(&exposedSecureValue), + "secure-value-2": service.NewDecryptResultErr(mockErr), + } + + decryptService := &OSSDecryptService{ + decryptStore: mockStorage, + } + + resp, err := decryptService.Decrypt(ctx, "default", "secure-value-1", "secure-value-2") + require.NotNil(t, resp) + require.NoError(t, err) + require.EqualValues(t, decryptedValuesResp, resp) + }) +} + +type MockDecryptStorage struct { + mock.Mock +} + +func (m *MockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv0alpha1.ExposedSecureValue, error) { + args := m.Called(ctx, namespace, name) + return args.Get(0).(secretv0alpha1.ExposedSecureValue), args.Error(1) +} diff --git a/pkg/registry/apis/secret/service/decrypt.go b/pkg/registry/apis/secret/service/decrypt.go new file mode 100644 index 00000000000..d9b19ed2e76 --- /dev/null +++ b/pkg/registry/apis/secret/service/decrypt.go @@ -0,0 +1,36 @@ +package service + +import ( + "context" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" +) + +// DecryptResult is the (union) result of a decryption operation. +// It contains the decrypted `value` when the decryption succeeds, and the `err` when it fails. +// It is not possible to construct a `DecryptResult` where both `value` and `err` are set from another package. +type DecryptResult struct { + value *secretv0alpha1.ExposedSecureValue + err error +} + +func (d DecryptResult) Error() error { + return d.err +} + +func (d DecryptResult) Value() *secretv0alpha1.ExposedSecureValue { + return d.value +} + +func NewDecryptResultErr(err error) DecryptResult { + return DecryptResult{err: err} +} + +func NewDecryptResultValue(value *secretv0alpha1.ExposedSecureValue) DecryptResult { + return DecryptResult{value: value} +} + +// DecryptService is the inferface for the decrypt service. +type DecryptService interface { + Decrypt(ctx context.Context, namespace string, names ...string) (map[string]DecryptResult, error) +}