Browse Source

Add support for Yandex Lockbox: long lived connections to lockbox api

zamysel 4 years ago
parent
commit
1e66d123b9

+ 1 - 0
go.mod

@@ -74,6 +74,7 @@ require (
 	golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
 	google.golang.org/api v0.30.0
 	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
+	google.golang.org/grpc v1.31.0
 	honnef.co/go/tools v0.1.4 // indirect
 	k8s.io/api v0.21.2
 	k8s.io/apimachinery v0.21.2

+ 12 - 5
pkg/provider/yandex/lockbox/client/client.go

@@ -15,18 +15,25 @@ package client
 
 import (
 	"context"
+	"time"
 
 	"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, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
+// Creates Lockbox clients and Yandex.Cloud IAM tokens.
+type YandexCloudCreator interface {
+	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
+	CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
+	Now() time.Time
+}
+
+type IamToken struct {
+	Token     string
+	ExpiresAt time.Time
 }
 
 // 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
+	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
 }

+ 59 - 39
pkg/provider/yandex/lockbox/client/fake/fake.go

@@ -16,45 +16,50 @@ package fake
 import (
 	"context"
 	"fmt"
-	"strconv"
+	"time"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/uuid"
 	"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 {
+// Fake implementation of YandexCloudCreator.
+type YandexCloudCreator struct {
 	Backend *LockboxBackend
 }
 
-func (lcc *LockboxClientCreator) Create(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
-	return &LockboxClient{lcc.Backend, authorizedKey}, nil
+func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	return &LockboxClient{c.Backend}, nil
+}
+
+func (c *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	return c.Backend.getToken(authorizedKey)
+}
+
+func (c *YandexCloudCreator) Now() time.Time {
+	return c.Backend.now
 }
 
 // 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
+func (c *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	return c.fakeLockboxBackend.getEntries(iamToken, secretID, versionID)
 }
 
 // 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
+	tokenMap   map[tokenKey]tokenValue     // token specific data
+
+	tokenExpirationDuration time.Duration
+	now                     time.Time // fakes the current time
 }
 
 type secretKey struct {
@@ -74,18 +79,28 @@ type versionValue struct {
 	entries []*lockbox.Payload_Entry
 }
 
-func NewLockboxBackend() *LockboxBackend {
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
 	return &LockboxBackend{
-		lastSecretID:  0,
-		lastVersionID: make(map[secretKey]int),
-		secretMap:     make(map[secretKey]secretValue),
-		versionMap:    make(map[versionKey]versionValue),
+		secretMap:               make(map[secretKey]secretValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		now:                     time.Time{},
 	}
 }
 
 func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
-	secretID := lb.genSecretID()
-	versionID := lb.genVersionID(secretID)
+	secretID := uuid.NewString()
+	versionID := uuid.NewString()
 
 	lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
 	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
@@ -95,7 +110,7 @@ func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lo
 }
 
 func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
-	versionID := lb.genVersionID(secretID)
+	versionID := uuid.NewString()
 
 	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
 	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
@@ -103,29 +118,34 @@ func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payloa
 	return versionID
 }
 
-func (lb *LockboxBackend) getEntries(authorizedKey *iamkey.Key, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+func (lb *LockboxBackend) AdvanceClock(duration time.Duration) {
+	lb.now = lb.now.Add(duration)
+}
+
+func (lb *LockboxBackend) getToken(authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	token := uuid.NewString()
+	expiresAt := lb.now.Add(lb.tokenExpirationDuration)
+	lb.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &client.IamToken{Token: token, ExpiresAt: expiresAt}, nil
+}
+
+func (lb *LockboxBackend) getEntries(iamToken, 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")
+	if _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
 	}
-	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}])
-}
+	if lb.tokenMap[tokenKey{iamToken}].expiresAt.Before(lb.now) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(lb.tokenMap[tokenKey{iamToken}].authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey) {
+		return nil, fmt.Errorf("permission denied")
+	}
 
-func intToString(i int) string {
-	return strconv.FormatInt(int64(i), 10)
+	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
 }

+ 93 - 18
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -15,55 +15,130 @@ package grpc
 
 import (
 	"context"
+	"crypto/tls"
+	"time"
 
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	ycsdk "github.com/yandex-cloud/go-sdk"
 	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/keepalive"
 
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 )
 
-// Implementation of LockboxClientCreator.
-type LockboxClientCreator struct {
+// Implementation of YandexCloudCreator.
+type YandexCloudCreator struct {
 }
 
-func (lb *LockboxClientCreator) Create(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
-	credentials, err := ycsdk.ServiceAccountKey(authorizedKey)
+func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
+		ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
+		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+			Time:                time.Second * 30,
+			Timeout:             time.Second * 10,
+			PermitWithoutStream: false,
+		}),
+		grpc.WithUserAgent("external-secrets"),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return &LockboxClient{lockbox.NewPayloadServiceClient(conn)}, nil
+}
+
+func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	iamToken, err := sdk.CreateIAMToken(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
+}
+
+func (lb *YandexCloudCreator) Now() time.Time {
+	return time.Now()
+}
+
+func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
+	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
 	if err != nil {
 		return nil, err
 	}
 
 	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
-		Credentials: credentials,
+		Credentials: creds,
 		Endpoint:    apiEndpoint,
 	})
 	if err != nil {
 		return nil, err
 	}
 
-	return &LockboxClient{sdk}, nil
+	return sdk, nil
+}
+
+func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
+	return sdk.Shutdown(ctx)
 }
 
 // Implementation of LockboxClient.
 type LockboxClient struct {
-	sdk *ycsdk.SDK
+	lockboxPayloadClient lockbox.PayloadServiceClient
 }
 
-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,
-	})
+func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
+	payload, err := lc.lockboxPayloadClient.Get(
+		ctx,
+		&lockbox.GetPayloadRequest{
+			SecretId:  secretID,
+			VersionId: versionID,
+		},
+		grpc.PerRPCCredentials(perRPCCredentials{iamToken: iamToken}),
+	)
 	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
+type perRPCCredentials struct {
+	iamToken string
+}
+
+func (t perRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
+	return map[string]string{"Authorization": "Bearer " + t.iamToken}, nil
+}
+
+func (perRPCCredentials) RequireTransportSecurity() bool {
+	return true
 }

+ 126 - 13
pkg/provider/yandex/lockbox/lockbox.go

@@ -15,13 +15,18 @@ package lockbox
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"sync"
+	"time"
 
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
+	ctrl "sigs.k8s.io/controller-runtime"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -31,9 +36,33 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
 )
 
+const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
+const iamTokenCleanupDelay = 1 * time.Hour       // specifies how often cleanUpIamTokenMap() is performed
+
+var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
+
+type iamTokenKey struct {
+	authorizedKeyID  string
+	serviceAccountID string
+	privateKeyHash   string
+}
+
 // lockboxProvider is a provider for Yandex Lockbox.
 type lockboxProvider struct {
-	lockboxClientCreator client.LockboxClientCreator
+	yandexCloudCreator client.YandexCloudCreator
+
+	lockboxClientMap      map[string]client.LockboxClient // apiEndpoint -> LockboxClient
+	lockboxClientMapMutex sync.Mutex
+	iamTokenMap           map[iamTokenKey]*client.IamToken
+	iamTokenMapMutex      sync.Mutex
+}
+
+func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
+	return &lockboxProvider{
+		yandexCloudCreator: yandexCloudCreator,
+		lockboxClientMap:   make(map[string]client.LockboxClient),
+		iamTokenMap:        make(map[iamTokenKey]*client.IamToken),
+	}
 }
 
 // NewClient constructs a Yandex Lockbox Provider.
@@ -78,22 +107,99 @@ func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.Generi
 		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
 	}
 
-	lb, err := p.lockboxClientCreator.Create(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
+	}
+
+	iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
 	if err != nil {
-		return nil, fmt.Errorf("failed to create Yandex.Cloud SDK: %w", err)
+		return nil, fmt.Errorf("failed to create IAM token: %w", err)
+	}
+
+	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
+}
+
+func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	p.lockboxClientMapMutex.Lock()
+	defer p.lockboxClientMapMutex.Unlock()
+
+	if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
+		log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
+
+		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
+		if err != nil {
+			return nil, err
+		}
+		p.lockboxClientMap[apiEndpoint] = lockboxClient
+	}
+	return p.lockboxClientMap[apiEndpoint], nil
+}
+
+func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	iamTokenKey := buildIamTokenKey(authorizedKey)
+	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
+		log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
+
+		iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
+		if err != nil {
+			return nil, err
+		}
+
+		log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
+
+		p.iamTokenMap[iamTokenKey] = iamToken
 	}
+	return p.iamTokenMap[iamTokenKey], nil
+}
+
+func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
+	now := p.yandexCloudCreator.Now()
+	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
+}
 
-	return &lockboxSecretsClient{lb}, nil
+func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
+	privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
+	return iamTokenKey{
+		authorizedKey.GetId(),
+		authorizedKey.GetServiceAccountId(),
+		hex.EncodeToString(privateKeyHash[:]),
+	}
+}
+
+// Used for testing.
+func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
+	return ok
+}
+
+func (p *lockboxProvider) cleanUpIamTokenMap() {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	for key, value := range p.iamTokenMap {
+		if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
+			log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
+			delete(p.iamTokenMap, key)
+		}
+	}
 }
 
 // lockboxSecretsClient is a secrets client for Yandex Lockbox.
 type lockboxSecretsClient struct {
 	lockboxClient client.LockboxClient
+	iamToken      string
 }
 
 // GetSecret returns a single secret from the provider.
-func (p *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	entries, err := p.lockboxClient.GetPayloadEntries(ctx, ref.Key, ref.Version)
+func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
 	if err != nil {
 		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
 	}
@@ -122,8 +228,8 @@ func (p *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.Ext
 }
 
 // GetSecretMap returns multiple k/v pairs from the provider.
-func (p *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	entries, err := p.lockboxClient.GetPayloadEntries(ctx, ref.Key, ref.Version)
+func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
 	if err != nil {
 		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
 	}
@@ -139,8 +245,8 @@ func (p *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.
 	return secretMap, nil
 }
 
-func (p *lockboxSecretsClient) Close(ctx context.Context) error {
-	return p.lockboxClient.Close(ctx)
+func (c *lockboxSecretsClient) Close(ctx context.Context) error {
+	return nil
 }
 
 func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
@@ -175,10 +281,17 @@ func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payl
 }
 
 func init() {
+	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
+
+	go func() {
+		for {
+			time.Sleep(iamTokenCleanupDelay)
+			lockboxProvider.cleanUpIamTokenMap()
+		}
+	}()
+
 	schema.Register(
-		&lockboxProvider{
-			lockboxClientCreator: &grpc.LockboxClientCreator{},
-		},
+		lockboxProvider,
 		&esv1alpha1.SecretStoreProvider{
 			YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
 		},

+ 290 - 53
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -18,7 +18,9 @@ import (
 	b64 "encoding/base64"
 	"encoding/json"
 	"testing"
+	"time"
 
+	"github.com/google/uuid"
 	tassert "github.com/stretchr/testify/assert"
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	"github.com/yandex-cloud/go-sdk/iamkey"
@@ -73,19 +75,19 @@ func TestNewClient(t *testing.T) {
 	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"))
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
 	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.EqualError(t, err, "failed to create Yandex Lockbox client: 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")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
@@ -98,11 +100,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		Backend: lockboxBackend,
-	}}
+	})
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -120,10 +122,10 @@ func TestGetSecretForAllEntries(t *testing.T) {
 
 func TestGetSecretForTextEntry(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
@@ -136,11 +138,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		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})
@@ -151,10 +153,10 @@ func TestGetSecretForTextEntry(t *testing.T) {
 
 func TestGetSecretForBinaryEntry(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
@@ -167,11 +169,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		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})
@@ -182,10 +184,10 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 
 func TestGetSecretByVersionID(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
 	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
@@ -196,11 +198,11 @@ func TestGetSecretByVersionID(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		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})
@@ -224,11 +226,11 @@ func TestGetSecretByVersionID(t *testing.T) {
 
 func TestGetSecretUnauthorized(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKeyA := newFakeAuthorizedKey("A")
-	authorizedKeyB := newFakeAuthorizedKey("B")
+	namespace := uuid.NewString()
+	authorizedKeyA := newFakeAuthorizedKey()
+	authorizedKeyB := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
 		textEntry("k1", "v1"),
 	)
@@ -238,11 +240,11 @@ func TestGetSecretUnauthorized(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		Backend: lockboxBackend,
-	}}
+	})
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -251,21 +253,21 @@ func TestGetSecretUnauthorized(t *testing.T) {
 
 func TestGetSecretNotFound(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 
 	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)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		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"})
@@ -278,12 +280,245 @@ func TestGetSecretNotFound(t *testing.T) {
 	tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
 }
 
+func TestGetSecretWithTwoNamespaces(t *testing.T) {
+	ctx := context.Background()
+	namespace1 := uuid.NewString()
+	namespace2 := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
+	tassert.Nil(t, err)
+	err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
+	tassert.Nil(t, err)
+	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
+	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
+	tassert.Nil(t, err)
+
+	data, err := secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
+	ctx := context.Background()
+	apiEndpoint1 := uuid.NewString()
+	apiEndpoint2 := uuid.NewString()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	tassert.Nil(t, err)
+
+	store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider1 := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend1,
+	})
+	provider2 := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend2,
+	})
+
+	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider2.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+
+	var data []byte
+
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenExpiration(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	tokenExpirationTime := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
+	k1, v1 := "k1", "v1"
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+	)
+
+	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 := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	var data []byte
+
+	oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+
+	lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
+
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: iam token expired")
+
+	newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = newSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenCleanup(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	tokenExpirationDuration := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry("k1", "v1"),
+	)
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry("k2", "v2"),
+	)
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	tassert.Nil(t, err)
+
+	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+
+	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
+	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
+
+	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
+	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	provider.cleanUpIamTokenMap()
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	provider.cleanUpIamTokenMap()
+
+	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+}
+
 func TestGetSecretMap(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
@@ -296,11 +531,11 @@ func TestGetSecretMap(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		Backend: lockboxBackend,
-	}}
+	})
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -318,10 +553,10 @@ func TestGetSecretMap(t *testing.T) {
 
 func TestGetSecretMapByVersionID(t *testing.T) {
 	ctx := context.Background()
-	const namespace = "namespace"
-	authorizedKey := newFakeAuthorizedKey("0")
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend()
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
 	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
@@ -332,11 +567,11 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
 	tassert.Nil(t, err)
-	store := newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := &lockboxProvider{&fake.LockboxClientCreator{
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
 		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})
@@ -360,7 +595,7 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 
 // helper functions
 
-func newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
+func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
 	return &esv1alpha1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
@@ -368,6 +603,7 @@ func newYandexLockboxSecretStore(namespace, authorizedKeySecretName, authorizedK
 		Spec: esv1alpha1.SecretStoreSpec{
 			Provider: &esv1alpha1.SecretStoreProvider{
 				YandexLockbox: &esv1alpha1.YandexLockboxProvider{
+					APIEndpoint: apiEndpoint,
 					Auth: esv1alpha1.YandexLockboxAuth{
 						AuthorizedKey: esmeta.SecretKeySelector{
 							Name: authorizedKeySecretName,
@@ -400,7 +636,8 @@ func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, se
 	return nil
 }
 
-func newFakeAuthorizedKey(uniqueLabel string) *iamkey.Key {
+func newFakeAuthorizedKey() *iamkey.Key {
+	uniqueLabel := uuid.NewString()
 	return &iamkey.Key{
 		Id: uniqueLabel,
 		Subject: &iamkey.Key_ServiceAccountId{