Просмотр исходного кода

Add support for Yandex Lockbox: tests

zamysel 4 лет назад
Родитель
Сommit
0815fcf857

+ 32 - 0
pkg/provider/yandex/lockbox/client/client.go

@@ -0,0 +1,32 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package client
+
+import (
+	"context"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+)
+
+// Creates LockboxClient with the given authorized key.
+type LockboxClientCreator interface {
+	Create(ctx context.Context, authorizedKey *iamkey.Key) (LockboxClient, error)
+}
+
+// Responsible for accessing Lockbox secrets.
+type LockboxClient interface {
+	GetPayloadEntries(ctx context.Context, secretID string, versionID string) ([]*lockbox.Payload_Entry, error)
+	Close(ctx context.Context) error
+}

+ 131 - 0
pkg/provider/yandex/lockbox/client/fake/fake.go

@@ -0,0 +1,131 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package fake
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Fake implementation of LockboxClientCreator.
+type LockboxClientCreator struct {
+	Backend *LockboxBackend
+}
+
+func (lcc *LockboxClientCreator) Create(ctx context.Context, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	return &LockboxClient{lcc.Backend, authorizedKey}, nil
+}
+
+// Fake implementation of LockboxClient.
+type LockboxClient struct {
+	fakeLockboxBackend *LockboxBackend
+	authorizedKey      *iamkey.Key
+}
+
+func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	return lc.fakeLockboxBackend.getEntries(lc.authorizedKey, secretID, versionID)
+}
+
+func (lc *LockboxClient) Close(ctx context.Context) error {
+	return nil
+}
+
+// Fakes Yandex Lockbox service backend.
+type LockboxBackend struct {
+	lastSecretID  int               // new secret IDs are generated by incrementing lastSecretID
+	lastVersionID map[secretKey]int // new version IDs are generated by incrementing lastVersionID[secretKey]
+
+	secretMap  map[secretKey]secretValue   // secret specific data
+	versionMap map[versionKey]versionValue // version specific data
+}
+
+type secretKey struct {
+	secretID string
+}
+
+type secretValue struct {
+	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
+}
+
+type versionKey struct {
+	secretID  string
+	versionID string
+}
+
+type versionValue struct {
+	entries []*lockbox.Payload_Entry
+}
+
+func NewLockboxBackend() *LockboxBackend {
+	return &LockboxBackend{
+		lastSecretID:  0,
+		lastVersionID: make(map[secretKey]int),
+		secretMap:     make(map[secretKey]secretValue),
+		versionMap:    make(map[versionKey]versionValue),
+	}
+}
+
+func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
+	secretID := lb.genSecretID()
+	versionID := lb.genVersionID(secretID)
+
+	lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
+	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return secretID, versionID
+}
+
+func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
+	versionID := lb.genVersionID(secretID)
+
+	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return versionID
+}
+
+func (lb *LockboxBackend) getEntries(authorizedKey *iamkey.Key, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	if _, ok := lb.secretMap[secretKey{secretID}]; !ok {
+		return nil, fmt.Errorf("secret not found")
+	}
+	if _, ok := lb.versionMap[versionKey{secretID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if !cmp.Equal(authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey) {
+		return nil, fmt.Errorf("permission denied")
+	}
+	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
+}
+
+func (lb *LockboxBackend) genSecretID() string {
+	lb.lastSecretID++
+	return intToString(lb.lastSecretID)
+}
+
+func (lb *LockboxBackend) genVersionID(secretID string) string {
+	lb.lastVersionID[secretKey{secretID}]++
+	return intToString(lb.lastVersionID[secretKey{secretID}])
+}
+
+func intToString(i int) string {
+	return strconv.FormatInt(int64(i), 10)
+}

+ 68 - 0
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -0,0 +1,68 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package grpc
+
+import (
+	"context"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	ycsdk "github.com/yandex-cloud/go-sdk"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Implementation of LockboxClientCreator.
+type LockboxClientCreator struct {
+}
+
+func (lb *LockboxClientCreator) Create(ctx context.Context, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	credentials, err := ycsdk.ServiceAccountKey(authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
+		Credentials: credentials,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return &LockboxClient{sdk}, nil
+}
+
+// Implementation of LockboxClient.
+type LockboxClient struct {
+	sdk *ycsdk.SDK
+}
+
+func (lb *LockboxClient) GetPayloadEntries(ctx context.Context, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	payload, err := lb.sdk.LockboxPayload().Payload().Get(ctx, &lockbox.GetPayloadRequest{
+		SecretId:  secretID,
+		VersionId: versionID,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return payload.Entries, nil
+}
+
+func (lb *LockboxClient) Close(ctx context.Context) error {
+	err := lb.sdk.Shutdown(ctx)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 28 - 48
pkg/provider/yandex/lockbox/lockbox.go

@@ -19,7 +19,6 @@ import (
 	"fmt"
 
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
-	ycsdk "github.com/yandex-cloud/go-sdk"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -28,16 +27,17 @@ import (
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/pkg/provider"
 	"github.com/external-secrets/external-secrets/pkg/provider/schema"
-	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
 )
 
-// providerLockbox is a provider for Yandex Lockbox.
-type providerLockbox struct {
-	sdk *ycsdk.SDK
+// lockboxProvider is a provider for Yandex Lockbox.
+type lockboxProvider struct {
+	lockboxClientCreator client.LockboxClientCreator
 }
 
 // NewClient constructs a Yandex Lockbox Provider.
-func (p *providerLockbox) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
+func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
 		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
@@ -78,28 +78,24 @@ func (p *providerLockbox) NewClient(ctx context.Context, store esv1alpha1.Generi
 		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
 	}
 
-	credentials, err := ycsdk.ServiceAccountKey(&authorizedKey)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create credentials: %w", err)
-	}
-
-	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
-		Credentials: credentials,
-	})
+	lb, err := p.lockboxClientCreator.Create(ctx, &authorizedKey)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create Yandex.Cloud SDK: %w", err)
 	}
 
-	p.sdk = sdk
+	return &lockboxSecretsClient{lb}, nil
+}
 
-	return p, nil
+// lockboxSecretsClient is a secrets client for Yandex Lockbox.
+type lockboxSecretsClient struct {
+	lockboxClient client.LockboxClient
 }
 
 // GetSecret returns a single secret from the provider.
-func (p *providerLockbox) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	entries, err := requestPayload(ctx, p.sdk, ref.Key, ref.Version)
+func (p *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	entries, err := p.lockboxClient.GetPayloadEntries(ctx, ref.Key, ref.Version)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
 	}
 
 	if ref.Property == "" {
@@ -125,28 +121,11 @@ func (p *providerLockbox) GetSecret(ctx context.Context, ref esv1alpha1.External
 	return getValueAsBinary(entry)
 }
 
-// GetSecret returns a single secret from the provider.
-func requestPayload(ctx context.Context, sdk *ycsdk.SDK, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
-	if utils.IsNil(sdk) {
-		return nil, fmt.Errorf("provider Yandex Lockbox is not initialized")
-	}
-
-	payload, err := sdk.LockboxPayload().Payload().Get(ctx, &lockbox.GetPayloadRequest{
-		SecretId:  secretID,
-		VersionId: versionID,
-	})
-	if err != nil {
-		return nil, fmt.Errorf("unable to get secret payload: %w", err)
-	}
-
-	return payload.Entries, nil
-}
-
 // GetSecretMap returns multiple k/v pairs from the provider.
-func (p *providerLockbox) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	entries, err := requestPayload(ctx, p.sdk, ref.Key, ref.Version)
+func (p *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	entries, err := p.lockboxClient.GetPayloadEntries(ctx, ref.Key, ref.Version)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
 	}
 
 	secretMap := make(map[string][]byte, len(entries))
@@ -160,12 +139,8 @@ func (p *providerLockbox) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	return secretMap, nil
 }
 
-func (p *providerLockbox) Close(ctx context.Context) error {
-	err := p.sdk.Shutdown(ctx)
-	if err != nil {
-		return fmt.Errorf("failed to shutdown Yandex.Cloud SDK: %w", err)
-	}
-	return nil
+func (p *lockboxSecretsClient) Close(ctx context.Context) error {
+	return p.lockboxClient.Close(ctx)
 }
 
 func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
@@ -200,7 +175,12 @@ func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payl
 }
 
 func init() {
-	schema.Register(&providerLockbox{}, &esv1alpha1.SecretStoreProvider{
-		YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
-	})
+	schema.Register(
+		&lockboxProvider{
+			lockboxClientCreator: &grpc.LockboxClientCreator{},
+		},
+		&esv1alpha1.SecretStoreProvider{
+			YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+		},
+	)
 }

+ 440 - 0
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -0,0 +1,440 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package lockbox
+
+import (
+	"context"
+	b64 "encoding/base64"
+	"encoding/json"
+	"testing"
+
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
+)
+
+func TestNewClient(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+
+	store := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+			},
+		},
+	}
+	provider, err := schema.GetProvider(store)
+	tassert.Nil(t, err)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Name = authorizedKeySecretName
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Key = authorizedKeySecretKey
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey("0"))
+	tassert.Nil(t, err)
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "failed to create Yandex.Cloud SDK: private key parsing failed: Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
+	tassert.Nil(t, secretClient)
+}
+
+func TestGetSecretForAllEntries(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string]string{
+			k1: v1,
+			k2: base64(v2),
+		},
+		unmarshalStringMap(t, data),
+	)
+}
+
+func TestGetSecretForTextEntry(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v1, string(data))
+}
+
+func TestGetSecretForBinaryEntry(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v2, data)
+}
+
+func TestGetSecretByVersionID(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{newKey: newVal}, unmarshalStringMap(t, data))
+}
+
+func TestGetSecretUnauthorized(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKeyA := newFakeAuthorizedKey("A")
+	authorizedKeyB := newFakeAuthorizedKey("B")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
+		textEntry("k1", "v1"),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+}
+
+func TestGetSecretNotFound(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry("k1", "v1"),
+	)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
+}
+
+func TestGetSecretMap(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			k1: []byte(v1),
+			k2: v2,
+		},
+		data,
+	)
+}
+
+func TestGetSecretMapByVersionID(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+	authorizedKey := newFakeAuthorizedKey("0")
+
+	lockboxBackend := fake.NewLockboxBackend()
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := &lockboxProvider{&fake.LockboxClientCreator{
+		Backend: lockboxBackend,
+	}}
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{newKey: []byte(newVal)}, data)
+}
+
+// helper functions
+
+func newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{
+					Auth: esv1alpha1.YandexLockboxAuth{
+						AuthorizedKey: esmeta.SecretKeySelector{
+							Name: authorizedKeySecretName,
+							Key:  authorizedKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, secretKey string, secretContent interface{}) error {
+	data, err := json.Marshal(secretContent)
+	if err != nil {
+		return err
+	}
+
+	err = k8sClient.Create(ctx, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      secretName,
+		},
+		Data: map[string][]byte{secretKey: data},
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func newFakeAuthorizedKey(uniqueLabel string) *iamkey.Key {
+	return &iamkey.Key{
+		Id: uniqueLabel,
+		Subject: &iamkey.Key_ServiceAccountId{
+			ServiceAccountId: uniqueLabel,
+		},
+		PrivateKey: uniqueLabel,
+	}
+}
+
+func textEntry(key, value string) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_TextValue{
+			TextValue: value,
+		},
+	}
+}
+
+func binaryEntry(key string, value []byte) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_BinaryValue{
+			BinaryValue: value,
+		},
+	}
+}
+
+func unmarshalStringMap(t *testing.T, data []byte) map[string]string {
+	stringMap := make(map[string]string)
+	err := json.Unmarshal(data, &stringMap)
+	tassert.Nil(t, err)
+	return stringMap
+}
+
+func base64(data []byte) string {
+	return b64.StdEncoding.EncodeToString(data)
+}