Explorar o código

Merge pull request #1040 from AndreyZamyslov/yandex-certificate-manager

Support for Yandex Certificate Manager
paul-the-alien[bot] %!s(int64=4) %!d(string=hai) anos
pai
achega
3de2cc8bee
Modificáronse 30 ficheiros con 2549 adicións e 656 borrados
  1. 4 0
      apis/externalsecrets/v1beta1/secretstore_types.go
  2. 43 0
      apis/externalsecrets/v1beta1/secretstore_yandexcertificatemanager_types.go
  3. 58 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  4. 58 0
      config/crds/bases/external-secrets.io_clustersecretstores.yaml
  5. 58 0
      config/crds/bases/external-secrets.io_secretstores.yaml
  6. 86 0
      deploy/crds/bundle.yaml
  7. 95 0
      docs/provider-yandex-certificate-manager.md
  8. 130 0
      docs/spec.md
  9. 1 0
      hack/api-docs/mkdocs.yml
  10. 1 0
      pkg/provider/register/register.go
  11. 80 0
      pkg/provider/yandex/certificatemanager/certificatemanager.go
  12. 717 0
      pkg/provider/yandex/certificatemanager/certificatemanager_test.go
  13. 85 0
      pkg/provider/yandex/certificatemanager/certificatemanagersecretgetter.go
  14. 25 0
      pkg/provider/yandex/certificatemanager/client/client.go
  15. 136 0
      pkg/provider/yandex/certificatemanager/client/fakeclient.go
  16. 57 0
      pkg/provider/yandex/certificatemanager/client/grpcclient.go
  17. 22 0
      pkg/provider/yandex/common/clock/clock.go
  18. 32 0
      pkg/provider/yandex/common/clock/fakeclock.go
  19. 27 0
      pkg/provider/yandex/common/clock/realclock.go
  20. 260 0
      pkg/provider/yandex/common/provider.go
  21. 43 58
      pkg/provider/yandex/common/sdk.go
  22. 24 0
      pkg/provider/yandex/common/secretgetter.go
  23. 51 0
      pkg/provider/yandex/common/secretsclient.go
  24. 3 17
      pkg/provider/yandex/lockbox/client/client.go
  25. 0 152
      pkg/provider/yandex/lockbox/client/fake/fake.go
  26. 136 0
      pkg/provider/yandex/lockbox/client/fakeclient.go
  27. 58 0
      pkg/provider/yandex/lockbox/client/grpcclient.go
  28. 24 286
      pkg/provider/yandex/lockbox/lockbox.go
  29. 122 143
      pkg/provider/yandex/lockbox/lockbox_test.go
  30. 113 0
      pkg/provider/yandex/lockbox/lockboxsecretgetter.go

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -70,6 +70,10 @@ type SecretStoreProvider struct {
 	// +optional
 	// +optional
 	IBM *IBMProvider `json:"ibm,omitempty"`
 	IBM *IBMProvider `json:"ibm,omitempty"`
 
 
+	// YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+	// +optional
+	YandexCertificateManager *YandexCertificateManagerProvider `json:"yandexcertificatemanager,omitempty"`
+
 	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
 	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
 	// +optional
 	// +optional
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`

+ 43 - 0
apis/externalsecrets/v1beta1/secretstore_yandexcertificatemanager_types.go

@@ -0,0 +1,43 @@
+/*
+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 v1beta1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type YandexCertificateManagerAuth struct {
+	// The authorized key used for authentication
+	// +optional
+	AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
+}
+
+type YandexCertificateManagerCAProvider struct {
+	Certificate esmeta.SecretKeySelector `json:"certSecretRef,omitempty"`
+}
+
+// YandexCertificateManagerProvider Configures a store to sync secrets using the Yandex Certificate Manager provider.
+type YandexCertificateManagerProvider struct {
+	// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+	// +optional
+	APIEndpoint string `json:"apiEndpoint,omitempty"`
+
+	// Auth defines the information necessary to authenticate against Yandex Certificate Manager
+	Auth YandexCertificateManagerAuth `json:"auth"`
+
+	// The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+	// +optional
+	CAProvider *YandexCertificateManagerCAProvider `json:"caProvider,omitempty"`
+}

+ 58 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1384,6 +1384,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(IBMProvider)
 		*out = new(IBMProvider)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.YandexCertificateManager != nil {
+		in, out := &in.YandexCertificateManager, &out.YandexCertificateManager
+		*out = new(YandexCertificateManagerProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.YandexLockbox != nil {
 	if in.YandexLockbox != nil {
 		in, out := &in.YandexLockbox, &out.YandexLockbox
 		in, out := &in.YandexLockbox, &out.YandexLockbox
 		*out = new(YandexLockboxProvider)
 		*out = new(YandexLockboxProvider)
@@ -1969,6 +1974,59 @@ func (in *WebhookSecret) DeepCopy() *WebhookSecret {
 	return out
 	return out
 }
 }
 
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerAuth) DeepCopyInto(out *YandexCertificateManagerAuth) {
+	*out = *in
+	in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerAuth.
+func (in *YandexCertificateManagerAuth) DeepCopy() *YandexCertificateManagerAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerCAProvider) DeepCopyInto(out *YandexCertificateManagerCAProvider) {
+	*out = *in
+	in.Certificate.DeepCopyInto(&out.Certificate)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerCAProvider.
+func (in *YandexCertificateManagerCAProvider) DeepCopy() *YandexCertificateManagerCAProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerCAProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerProvider) DeepCopyInto(out *YandexCertificateManagerProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(YandexCertificateManagerCAProvider)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerProvider.
+func (in *YandexCertificateManagerProvider) DeepCopy() *YandexCertificateManagerProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
 func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
 	*out = *in
 	*out = *in

+ 58 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2749,6 +2749,64 @@ spec:
                     - result
                     - result
                     - url
                     - url
                     type: object
                     type: object
+                  yandexcertificatemanager:
+                    description: YandexCertificateManager configures this store to
+                      sync secrets using Yandex Certificate Manager provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Certificate Manager
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                   yandexlockbox:
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider
                       using Yandex Lockbox provider

+ 58 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2752,6 +2752,64 @@ spec:
                     - result
                     - result
                     - url
                     - url
                     type: object
                     type: object
+                  yandexcertificatemanager:
+                    description: YandexCertificateManager configures this store to
+                      sync secrets using Yandex Certificate Manager provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Certificate Manager
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                   yandexlockbox:
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider
                       using Yandex Lockbox provider

+ 86 - 0
deploy/crds/bundle.yaml

@@ -2351,6 +2351,49 @@ spec:
                         - result
                         - result
                         - url
                         - url
                       type: object
                       type: object
+                    yandexcertificatemanager:
+                      description: YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+                      properties:
+                        apiEndpoint:
+                          description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                          type: string
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Yandex Certificate Manager
+                          properties:
+                            authorizedKeySecretRef:
+                              description: The authorized key used for authentication
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+                          properties:
+                            certSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                      required:
+                        - auth
+                      type: object
                     yandexlockbox:
                     yandexlockbox:
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       properties:
                       properties:
@@ -4986,6 +5029,49 @@ spec:
                         - result
                         - result
                         - url
                         - url
                       type: object
                       type: object
+                    yandexcertificatemanager:
+                      description: YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+                      properties:
+                        apiEndpoint:
+                          description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                          type: string
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Yandex Certificate Manager
+                          properties:
+                            authorizedKeySecretRef:
+                              description: The authorized key used for authentication
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+                          properties:
+                            certSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                      required:
+                        - auth
+                      type: object
                     yandexlockbox:
                     yandexlockbox:
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       properties:
                       properties:

+ 95 - 0
docs/provider-yandex-certificate-manager.md

@@ -0,0 +1,95 @@
+## Yandex Certificate Manager
+
+External Secrets Operator integrates with [Yandex Certificate Manager](https://cloud.yandex.com/docs/certificate-manager/)
+for secret management.
+
+### Prerequisites
+* [External Secrets Operator installed](../guides-getting-started/#installing-with-helm)
+* [Yandex.Cloud CLI installed](https://cloud.yandex.com/docs/cli/quickstart)
+
+### Authentication
+At the moment, [authorized key](https://cloud.yandex.com/docs/iam/concepts/authorization/key) authentication is only supported:
+
+* Create a [service account](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) in Yandex.Cloud:
+```bash
+yc iam service-account create --name eso-service-account
+```
+* Create an authorized key for the service account and save it to `authorized-key.json` file:
+```bash
+yc iam key create \
+  --service-account-name eso-service-account \
+  --output authorized-key.json
+```
+* Create a k8s secret containing the authorized key saved above:
+```bash
+kubectl create secret generic yc-auth --from-file=authorized-key=authorized-key.json
+```
+* Create a [SecretStore](../api-secretstore/) pointing to `yc-auth` k8s secret:
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    yandexcertificatemanager:
+      auth:
+        authorizedKeySecretRef:
+          name: yc-auth
+          key: authorized-key
+```
+
+**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in all `authorizedKeySecretRef` with the namespace where the secret resides.
+
+### Creating external secret
+To make External Secrets Operator sync a k8s secret with a Certificate Manager certificate:
+
+* Create a Certificate Manager certificate (follow
+  [the instructions](https://cloud.yandex.com/en-ru/docs/certificate-manager/operations/)), if not already created.
+* Assign the [`certificate-manager.certificates.downloader`](https://cloud.yandex.com/en-ru/docs/certificate-manager/security/#roles-list) role
+  for accessing the certificate content to the service account used for authentication (`*****` is the certificate ID):
+```bash
+yc cm certificate add-access-binding \
+  --id ***** \
+  --service-account-name eso-service-account \
+  --role certificate-manager.certificates.downloader
+```
+Run the following command to ensure that the correct access binding has been added:
+```bash
+yc cm certificate list-access-bindings --id *****
+```
+* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and the certificate in Certificate Manager:
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: external-secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secret-store
+    kind: SecretStore
+  target:
+    name: k8s-secret # the target k8s secret name
+    template:
+      type: kubernetes.io/tls
+  data:
+    - secretKey: tls.crt # the target k8s secret key
+      remoteRef:
+        key: ***** # the certificate ID
+        property: chain
+    - secretKey: tls.key # the target k8s secret key
+      remoteRef:
+        key: ***** # the certificate ID
+        property: privateKey
+```
+The following property values are possible:
+    * `chain` – to fetch PEM-encoded certificate chain
+    * `privateKey` – to fetch PEM-encoded private key
+    * `chainAndPrivateKey` or missing property – to fetch both chain and private key
+
+The operator will fetch the Yandex Certificate Manager certificate and inject it as a `Kind=Secret`
+```yaml
+kubectl get secret k8s-secret -ojson | jq '."data"."tls\.crt"' -r | base64 --decode
+kubectl get secret k8s-secret -ojson | jq '."data"."tls\.key"' -r | base64 --decode
+```

+ 130 - 0
docs/spec.md

@@ -3350,6 +3350,20 @@ IBMProvider
 </tr>
 </tr>
 <tr>
 <tr>
 <td>
 <td>
+<code>yandexcertificatemanager</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">
+YandexCertificateManagerProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>yandexlockbox</code></br>
 <code>yandexlockbox</code></br>
 <em>
 <em>
 <a href="#external-secrets.io/v1beta1.YandexLockboxProvider">
 <a href="#external-secrets.io/v1beta1.YandexLockboxProvider">
@@ -5004,6 +5018,122 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 </tr>
 </tr>
 </tbody>
 </tbody>
 </table>
 </table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerAuth">YandexCertificateManagerAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>authorizedKeySecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The authorized key used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerCAProvider">YandexCertificateManagerCAProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>certSecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>YandexCertificateManagerProvider Configures a store to sync secrets using the Yandex Certificate Manager provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiEndpoint</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Yandex.Cloud API endpoint (e.g. &lsquo;api.cloud.yandex.net:443&rsquo;)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerAuth">
+YandexCertificateManagerAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the information necessary to authenticate against Yandex Certificate Manager</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerCAProvider">
+YandexCertificateManagerCAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The provider for the CA bundle to use to validate Yandex.Cloud server certificate.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.YandexLockboxAuth">YandexLockboxAuth
 <h3 id="external-secrets.io/v1beta1.YandexLockboxAuth">YandexLockboxAuth
 </h3>
 </h3>
 <p>
 <p>

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -60,6 +60,7 @@ nav:
     - Akeyless: provider-akeyless.md
     - Akeyless: provider-akeyless.md
     - HashiCorp Vault: provider-hashicorp-vault.md
     - HashiCorp Vault: provider-hashicorp-vault.md
     - Yandex:
     - Yandex:
+        - Certificate Manager: provider-yandex-certificate-manager.md
         - Lockbox: provider-yandex-lockbox.md
         - Lockbox: provider-yandex-lockbox.md
     - Gitlab:
     - Gitlab:
       - Gitlab Project Variables: provider-gitlab-project-variables.md
       - Gitlab Project Variables: provider-gitlab-project-variables.md

+ 1 - 0
pkg/provider/register/register.go

@@ -31,5 +31,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 )
 )

+ 80 - 0
pkg/provider/yandex/certificatemanager/certificatemanager.go

@@ -0,0 +1,80 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	ctrl "sigs.k8s.io/controller-runtime"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("certificatemanager")
+
+func adaptInput(store esv1beta1.GenericStore) (*common.SecretsClientInput, error) {
+	storeSpec := store.GetSpec()
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexCertificateManager == nil {
+		return nil, fmt.Errorf("received invalid Yandex Certificate Manager SecretStore resource")
+	}
+	storeSpecYandexCertificateManager := storeSpec.Provider.YandexCertificateManager
+
+	if storeSpecYandexCertificateManager.Auth.AuthorizedKey.Name == "" {
+		return nil, fmt.Errorf("invalid Yandex Certificate Manager SecretStore resource: missing AuthorizedKey Name")
+	}
+
+	var caCertificate *esmeta.SecretKeySelector
+	if storeSpecYandexCertificateManager.CAProvider != nil {
+		caCertificate = &storeSpecYandexCertificateManager.CAProvider.Certificate
+	}
+
+	return &common.SecretsClientInput{
+		APIEndpoint:   storeSpecYandexCertificateManager.APIEndpoint,
+		AuthorizedKey: storeSpecYandexCertificateManager.Auth.AuthorizedKey,
+		CACertificate: caCertificate,
+	}, nil
+}
+
+func newSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+	grpcClient, err := client.NewGrpcCertificateManagerClient(ctx, apiEndpoint, authorizedKey, caCertificate)
+	if err != nil {
+		return nil, err
+	}
+	return newCertificateManagerSecretGetter(grpcClient)
+}
+
+func init() {
+	provider := common.InitYandexCloudProvider(
+		log,
+		clock.NewRealClock(),
+		adaptInput,
+		newSecretGetter,
+		common.NewIamToken,
+		time.Hour,
+	)
+
+	esv1beta1.Register(
+		provider,
+		&esv1beta1.SecretStoreProvider{
+			YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{},
+		},
+	)
+}

+ 717 - 0
pkg/provider/yandex/certificatemanager/certificatemanager_test.go

@@ -0,0 +1,717 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"encoding/json"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+const (
+	errMissingKey                    = "invalid Yandex Certificate Manager SecretStore resource: missing AuthorizedKey Name"
+	errSecretPayloadPermissionDenied = "unable to request certificate content to get secret: permission denied"
+	errSecretPayloadNotFound         = "unable to request certificate content to get secret: certificate not found"
+)
+
+func TestNewClient(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+
+	store := &esv1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{},
+			},
+		},
+	}
+	provider, err := esv1beta1.GetProvider(store)
+	tassert.Nil(t, err)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexCertificateManager.Auth = esv1beta1.YandexCertificateManagerAuth{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexCertificateManager.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	store.Spec.Provider.YandexCertificateManager.Auth.AuthorizedKey.Name = authorizedKeySecretName
+	store.Spec.Provider.YandexCertificateManager.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, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, newFakeAuthorizedKey()))
+	tassert.Nil(t, err)
+
+	const caCertificateSecretName = "caCertificateSecretName"
+	const caCertificateSecretKey = "caCertificateSecretKey"
+	store.Spec.Provider.YandexCertificateManager.CAProvider = &esv1beta1.YandexCertificateManagerCAProvider{
+		Certificate: esmeta.SecretKeySelector{
+			Key:  caCertificateSecretKey,
+			Name: caCertificateSecretName,
+		},
+	}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, t, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, []byte("it-is-not-a-certificate"))
+	tassert.Nil(t, err)
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "failed to create Yandex.Cloud client: unable to read trusted CA certificates")
+	tassert.Nil(t, secretClient)
+}
+
+func TestGetSecretWithoutProperty(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2, privateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+}
+
+func TestGetSecretWithProperty(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+
+	chainData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: chainProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2}, "\n")),
+		strings.TrimSpace(string(chainData)),
+	)
+
+	privateKeyData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(privateKey),
+		strings.TrimSpace(string(privateKeyData)),
+	)
+
+	chainAndPrivateKeyData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: chainAndPrivateKeyProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2, privateKey}, "\n")),
+		strings.TrimSpace(string(chainAndPrivateKeyData)),
+	)
+}
+
+func TestGetSecretByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	oldCertificate1 := uuid.NewString()
+	oldCertificate2 := uuid.NewString()
+	oldPrivateKey := uuid.NewString()
+	certificateID, oldVersionID := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{oldCertificate1, oldCertificate2},
+		PrivateKey:       oldPrivateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{oldCertificate1, oldCertificate2, oldPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+
+	newCertificate1 := uuid.NewString()
+	newCertificate2 := uuid.NewString()
+	newPrivateKey := uuid.NewString()
+	newVersionID := fakeCertificateManagerServer.AddVersion(certificateID, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{newCertificate1, newCertificate2},
+		PrivateKey:       newPrivateKey,
+	})
+
+	data, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{oldCertificate1, oldCertificate2, oldPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+
+	data, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{newCertificate1, newCertificate2, newPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+}
+
+func TestGetSecretUnauthorized(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKeyA := newFakeAuthorizedKey()
+	authorizedKeyB := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKeyA, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKeyB))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+}
+
+func TestGetSecretNotFound(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: "no-version-with-this-id"})
+	tassert.EqualError(t, err, "unable to request certificate content to get secret: version not found")
+}
+
+func TestGetSecretWithTwoNamespaces(t *testing.T) {
+	ctx := context.Background()
+	namespace1 := uuid.NewString()
+	namespace2 := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	privateKey1 := uuid.NewString()
+	certificateID1, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	certificate2 := uuid.NewString()
+	privateKey2 := uuid.NewString()
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	err = createK8sSecret(ctx, t, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+	store1 := newYandexCertificateManagerSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
+	store2 := newYandexCertificateManagerSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	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, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey1, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey2, strings.TrimSpace(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()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer1 := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	privateKey1 := uuid.NewString()
+	certificateID1, _ := fakeCertificateManagerServer1.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	fakeCertificateManagerServer2 := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate2 := uuid.NewString()
+	privateKey2 := uuid.NewString()
+	certificateID2, _ := fakeCertificateManagerServer2.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+
+	store1 := newYandexCertificateManagerSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexCertificateManagerSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider1 := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer1)
+	provider2 := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer2)
+
+	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, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: chainProperty})
+	tassert.Equal(t, certificate1, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: chainProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: chainProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: chainProperty})
+	tassert.Equal(t, certificate2, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenExpiration(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	tokenExpirationTime := time.Hour
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, tokenExpirationTime)
+	certificate := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+
+	var data []byte
+
+	oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+
+	fakeClock.AddDuration(2 * tokenExpirationTime)
+
+	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request certificate content to get secret: iam token expired")
+
+	newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = newSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenCleanup(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	tokenExpirationDuration := time.Hour
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, tokenExpirationDuration)
+	certificateID1, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+
+	store1 := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+
+	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, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
+
+	fakeClock.AddDuration(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, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
+
+	fakeClock.AddDuration(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))
+
+	fakeClock.AddDuration(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()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(strings.Join([]string{certificate1, certificate2}, "\n")),
+			privateKeyProperty: []byte(privateKey),
+		},
+		data,
+	)
+}
+
+func TestGetSecretMapByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	oldCertificate := uuid.NewString()
+	oldPrivateKey := uuid.NewString()
+	certificateID, oldVersionID := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{oldCertificate},
+		PrivateKey:       oldPrivateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(oldCertificate),
+			privateKeyProperty: []byte(oldPrivateKey),
+		},
+		data,
+	)
+
+	newCertificate := uuid.NewString()
+	newPrivateKey := uuid.NewString()
+	newVersionID := fakeCertificateManagerServer.AddVersion(certificateID, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{newCertificate},
+		PrivateKey:       newPrivateKey,
+	})
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(oldCertificate),
+			privateKeyProperty: []byte(oldPrivateKey),
+		},
+		data,
+	)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(newCertificate),
+			privateKeyProperty: []byte(newPrivateKey),
+		},
+		data,
+	)
+}
+
+// helper functions
+
+func newCertificateManagerProvider(clock clock.Clock, fakeCertificateManagerServer *client.FakeCertificateManagerServer) *common.YandexCloudProvider {
+	return common.InitYandexCloudProvider(
+		ctrl.Log.WithName("provider").WithName("yandex").WithName("certificatemanager"),
+		clock,
+		adaptInput,
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+			return newCertificateManagerSecretGetter(client.NewFakeCertificateManagerClient(fakeCertificateManagerServer))
+		},
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*common.IamToken, error) {
+			return fakeCertificateManagerServer.NewIamToken(authorizedKey), nil
+		},
+		0,
+	)
+}
+
+func newYandexCertificateManagerSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
+	return &esv1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{
+					APIEndpoint: apiEndpoint,
+					Auth: esv1beta1.YandexCertificateManagerAuth{
+						AuthorizedKey: esmeta.SecretKeySelector{
+							Name: authorizedKeySecretName,
+							Key:  authorizedKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func toJSON(t *testing.T, v interface{}) []byte {
+	jsonBytes, err := json.Marshal(v)
+	tassert.Nil(t, err)
+	return jsonBytes
+}
+
+func createK8sSecret(ctx context.Context, t *testing.T, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
+	err := k8sClient.Create(ctx, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      secretName,
+		},
+		Data: map[string][]byte{secretKey: secretValue},
+	})
+	tassert.Nil(t, err)
+	return nil
+}
+
+func newFakeAuthorizedKey() *iamkey.Key {
+	uniqueLabel := uuid.NewString()
+	return &iamkey.Key{
+		Id: uniqueLabel,
+		Subject: &iamkey.Key_ServiceAccountId{
+			ServiceAccountId: uniqueLabel,
+		},
+		PrivateKey: uniqueLabel,
+	}
+}

+ 85 - 0
pkg/provider/yandex/certificatemanager/certificatemanagersecretgetter.go

@@ -0,0 +1,85 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+const (
+	chainProperty              = "chain"
+	privateKeyProperty         = "privateKey"
+	chainAndPrivateKeyProperty = "chainAndPrivateKey"
+)
+
+// Implementation of common.SecretGetter.
+type certificateManagerSecretGetter struct {
+	certificateManagerClient client.CertificateManagerClient
+}
+
+func newCertificateManagerSecretGetter(certificateManagerClient client.CertificateManagerClient) (common.SecretGetter, error) {
+	return &certificateManagerSecretGetter{
+		certificateManagerClient: certificateManagerClient,
+	}, nil
+}
+
+func (g *certificateManagerSecretGetter) GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error) {
+	response, err := g.certificateManagerClient.GetCertificateContent(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request certificate content to get secret: %w", err)
+	}
+
+	chain := trimAndJoin(response.CertificateChain...)
+	privateKey := trimAndJoin(response.PrivateKey)
+
+	switch property {
+	case "", chainAndPrivateKeyProperty:
+		return []byte(trimAndJoin(chain, privateKey)), nil
+	case chainProperty:
+		return []byte(chain), nil
+	case privateKeyProperty:
+		return []byte(privateKey), nil
+	default:
+		return nil, fmt.Errorf("unsupported property '%s'", property)
+	}
+}
+
+func (g *certificateManagerSecretGetter) GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error) {
+	response, err := g.certificateManagerClient.GetCertificateContent(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request certificate content to get secret map: %w", err)
+	}
+
+	chain := strings.Join(response.CertificateChain, "\n")
+	privateKey := response.PrivateKey
+
+	return map[string][]byte{
+		chainProperty:      []byte(chain),
+		privateKeyProperty: []byte(privateKey),
+	}, nil
+}
+
+func trimAndJoin(elems ...string) string {
+	var sb strings.Builder
+	for _, elem := range elems {
+		sb.WriteString(strings.TrimSpace(elem))
+		sb.WriteRune('\n')
+	}
+	return sb.String()
+}

+ 25 - 0
pkg/provider/yandex/certificatemanager/client/client.go

@@ -0,0 +1,25 @@
+/*
+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"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+)
+
+// Requests the content of the given certificate from Certificate Manager.
+type CertificateManagerClient interface {
+	GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error)
+}

+ 136 - 0
pkg/provider/yandex/certificatemanager/client/fakeclient.go

@@ -0,0 +1,136 @@
+/*
+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"
+	"fmt"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/uuid"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+// Fake implementation of CertificateManagerClient.
+type fakeCertificateManagerClient struct {
+	fakeCertificateManagerServer *FakeCertificateManagerServer
+}
+
+func NewFakeCertificateManagerClient(fakeCertificateManagerServer *FakeCertificateManagerServer) CertificateManagerClient {
+	return &fakeCertificateManagerClient{fakeCertificateManagerServer}
+}
+
+func (c *fakeCertificateManagerClient) GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	return c.fakeCertificateManagerServer.getCertificateContent(iamToken, certificateID, versionID)
+}
+
+// Fakes Yandex Certificate Manager service backend.
+type FakeCertificateManagerServer struct {
+	certificateMap map[certificateKey]certificateValue // certificate specific data
+	versionMap     map[versionKey]versionValue         // version specific data
+	tokenMap       map[tokenKey]tokenValue             // token specific data
+
+	tokenExpirationDuration time.Duration
+	clock                   clock.Clock
+}
+
+type certificateKey struct {
+	certificateID string
+}
+
+type certificateValue struct {
+	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the certificate
+}
+
+type versionKey struct {
+	certificateID string
+	versionID     string
+}
+
+type versionValue struct {
+	content *api.GetCertificateContentResponse
+}
+
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewFakeCertificateManagerServer(clock clock.Clock, tokenExpirationDuration time.Duration) *FakeCertificateManagerServer {
+	return &FakeCertificateManagerServer{
+		certificateMap:          make(map[certificateKey]certificateValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		clock:                   clock,
+	}
+}
+
+func (s *FakeCertificateManagerServer) CreateCertificate(authorizedKey *iamkey.Key, content *api.GetCertificateContentResponse) (string, string) {
+	certificateID := uuid.NewString()
+	versionID := uuid.NewString()
+
+	s.certificateMap[certificateKey{certificateID}] = certificateValue{authorizedKey}
+	s.versionMap[versionKey{certificateID, ""}] = versionValue{content} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{certificateID, versionID}] = versionValue{content}
+
+	return certificateID, versionID
+}
+
+func (s *FakeCertificateManagerServer) AddVersion(certificateID string, content *api.GetCertificateContentResponse) string {
+	versionID := uuid.NewString()
+
+	s.versionMap[versionKey{certificateID, ""}] = versionValue{content} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{certificateID, versionID}] = versionValue{content}
+
+	return versionID
+}
+
+func (s *FakeCertificateManagerServer) NewIamToken(authorizedKey *iamkey.Key) *common.IamToken {
+	token := uuid.NewString()
+	expiresAt := s.clock.CurrentTime().Add(s.tokenExpirationDuration)
+	s.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &common.IamToken{Token: token, ExpiresAt: expiresAt}
+}
+
+func (s *FakeCertificateManagerServer) getCertificateContent(iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	if _, ok := s.certificateMap[certificateKey{certificateID}]; !ok {
+		return nil, fmt.Errorf("certificate not found")
+	}
+	if _, ok := s.versionMap[versionKey{certificateID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if _, ok := s.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
+	}
+
+	if s.tokenMap[tokenKey{iamToken}].expiresAt.Before(s.clock.CurrentTime()) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(s.tokenMap[tokenKey{iamToken}].authorizedKey, s.certificateMap[certificateKey{certificateID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return s.versionMap[versionKey{certificateID, versionID}].content, nil
+}

+ 57 - 0
pkg/provider/yandex/certificatemanager/client/grpcclient.go

@@ -0,0 +1,57 @@
+/*
+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"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+// Real/gRPC implementation of CertificateManagerClient.
+type grpcCertificateManagerClient struct {
+	certificateContentServiceClient api.CertificateContentServiceClient
+}
+
+func NewGrpcCertificateManagerClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (CertificateManagerClient, error) {
+	conn, err := common.NewGrpcConnection(
+		ctx,
+		apiEndpoint,
+		"certificate-manager-data", // taken from https://api.cloud.yandex.net/endpoints
+		authorizedKey,
+		caCertificate,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &grpcCertificateManagerClient{api.NewCertificateContentServiceClient(conn)}, nil
+}
+
+func (c *grpcCertificateManagerClient) GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	response, err := c.certificateContentServiceClient.Get(
+		ctx,
+		&api.GetCertificateContentRequest{
+			CertificateId: certificateID,
+		},
+		grpc.PerRPCCredentials(common.PerRPCCredentials{IamToken: iamToken}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}

+ 22 - 0
pkg/provider/yandex/common/clock/clock.go

@@ -0,0 +1,22 @@
+/*
+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 clock
+
+import (
+	"time"
+)
+
+type Clock interface {
+	CurrentTime() time.Time
+}

+ 32 - 0
pkg/provider/yandex/common/clock/fakeclock.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 clock
+
+import "time"
+
+type FakeClock struct {
+	now time.Time
+}
+
+func NewFakeClock() *FakeClock {
+	return &FakeClock{time.Time{}}
+}
+
+func (c *FakeClock) CurrentTime() time.Time {
+	return c.now
+}
+
+func (c *FakeClock) AddDuration(duration time.Duration) {
+	c.now = c.now.Add(duration)
+}

+ 27 - 0
pkg/provider/yandex/common/clock/realclock.go

@@ -0,0 +1,27 @@
+/*
+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 clock
+
+import "time"
+
+type RealClock struct {
+}
+
+func NewRealClock() *RealClock {
+	return &RealClock{}
+}
+
+func (c *RealClock) CurrentTime() time.Time {
+	return time.Now()
+}

+ 260 - 0
pkg/provider/yandex/common/provider.go

@@ -0,0 +1,260 @@
+/*
+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 common
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	clock2 "github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.Provider = &YandexCloudProvider{}
+
+// Implementation of v1beta1.Provider.
+type YandexCloudProvider struct {
+	logger              logr.Logger
+	clock               clock2.Clock
+	adaptInputFunc      AdaptInputFunc
+	newSecretGetterFunc NewSecretGetterFunc
+	newIamTokenFunc     NewIamTokenFunc
+
+	secretGetteMap       map[string]SecretGetter // apiEndpoint -> SecretGetter
+	secretGetterMapMutex sync.Mutex
+	iamTokenMap          map[iamTokenKey]*IamToken
+	iamTokenMapMutex     sync.Mutex
+}
+
+type iamTokenKey struct {
+	authorizedKeyID  string
+	serviceAccountID string
+	privateKeyHash   string
+}
+
+func InitYandexCloudProvider(
+	logger logr.Logger,
+	clock clock2.Clock,
+	adaptInputFunc AdaptInputFunc,
+	newSecretGetterFunc NewSecretGetterFunc,
+	newIamTokenFunc NewIamTokenFunc,
+	iamTokenCleanupDelay time.Duration,
+) *YandexCloudProvider {
+	provider := &YandexCloudProvider{
+		logger:              logger,
+		clock:               clock,
+		adaptInputFunc:      adaptInputFunc,
+		newSecretGetterFunc: newSecretGetterFunc,
+		newIamTokenFunc:     newIamTokenFunc,
+		secretGetteMap:      make(map[string]SecretGetter),
+		iamTokenMap:         make(map[iamTokenKey]*IamToken),
+	}
+
+	if iamTokenCleanupDelay > 0 {
+		go func() {
+			for {
+				time.Sleep(iamTokenCleanupDelay)
+				provider.CleanUpIamTokenMap()
+			}
+		}()
+	}
+
+	return provider
+}
+
+type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error)
+type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
+type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
+
+type IamToken struct {
+	Token     string
+	ExpiresAt time.Time
+}
+
+type SecretsClientInput struct {
+	APIEndpoint   string
+	AuthorizedKey esmeta.SecretKeySelector
+	CACertificate *esmeta.SecretKeySelector
+}
+
+// NewClient constructs a Yandex.Cloud Provider.
+func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	input, err := p.adaptInputFunc(store)
+	if err != nil {
+		return nil, err
+	}
+
+	objectKey := types.NamespacedName{
+		Name:      input.AuthorizedKey.Name,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+		if input.AuthorizedKey.Namespace == nil {
+			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
+		}
+		objectKey.Namespace = *input.AuthorizedKey.Namespace
+	}
+
+	authorizedKeySecret := &corev1.Secret{}
+	err = kube.Get(ctx, objectKey, authorizedKeySecret)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
+	}
+
+	authorizedKeySecretData := authorizedKeySecret.Data[input.AuthorizedKey.Key]
+	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
+		return nil, fmt.Errorf("missing AuthorizedKey")
+	}
+
+	var authorizedKey iamkey.Key
+	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
+	}
+
+	var caCertificateData []byte
+
+	if input.CACertificate != nil {
+		certObjectKey := types.NamespacedName{
+			Name:      input.CACertificate.Name,
+			Namespace: namespace,
+		}
+
+		if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+			if input.CACertificate.Namespace == nil {
+				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
+			}
+			certObjectKey.Namespace = *input.CACertificate.Namespace
+		}
+
+		caCertificateSecret := &corev1.Secret{}
+		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
+		if err != nil {
+			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
+		}
+
+		caCertificateData = caCertificateSecret.Data[input.CACertificate.Key]
+		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
+			return nil, fmt.Errorf("missing CA Certificate")
+		}
+	}
+
+	secretGetter, err := p.getOrCreateSecretGetter(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Yandex.Cloud client: %w", err)
+	}
+
+	iamToken, err := p.getOrCreateIamToken(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create IAM token: %w", err)
+	}
+
+	return &yandexCloudSecretsClient{secretGetter, iamToken.Token}, nil
+}
+
+func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {
+	p.secretGetterMapMutex.Lock()
+	defer p.secretGetterMapMutex.Unlock()
+
+	if _, ok := p.secretGetteMap[apiEndpoint]; !ok {
+		p.logger.Info("creating SecretGetter", "apiEndpoint", apiEndpoint)
+
+		secretGetter, err := p.newSecretGetterFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
+		if err != nil {
+			return nil, err
+		}
+		p.secretGetteMap[apiEndpoint] = secretGetter
+	}
+	return p.secretGetteMap[apiEndpoint], nil
+}
+
+func (p *YandexCloudProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	iamTokenKey := buildIamTokenKey(authorizedKey)
+	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
+		p.logger.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
+
+		iamToken, err := p.newIamTokenFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
+		if err != nil {
+			return nil, err
+		}
+
+		p.logger.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
+
+		p.iamTokenMap[iamTokenKey] = iamToken
+	}
+	return p.iamTokenMap[iamTokenKey], nil
+}
+
+func (p *YandexCloudProvider) isIamTokenUsable(iamToken *IamToken) bool {
+	now := p.clock.CurrentTime()
+	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
+}
+
+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 *YandexCloudProvider) IsIamTokenCached(authorizedKey *iamkey.Key) bool {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
+	return ok
+}
+
+func (p *YandexCloudProvider) CleanUpIamTokenMap() {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	for key, value := range p.iamTokenMap {
+		if p.clock.CurrentTime().After(value.ExpiresAt) {
+			p.logger.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
+			delete(p.iamTokenMap, key)
+		}
+	}
+}
+
+func (p *YandexCloudProvider) ValidateStore(store esv1beta1.GenericStore) error {
+	_, err := p.adaptInputFunc(store) // adaptInputFunc validates the input store
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 43 - 58
pkg/provider/yandex/lockbox/client/grpc/grpc.go → pkg/provider/yandex/common/sdk.go

@@ -11,7 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package grpc
+package common
 
 
 import (
 import (
 	"context"
 	"context"
@@ -21,22 +21,27 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
 	"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"
 	ycsdk "github.com/yandex-cloud/go-sdk"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/keepalive"
 	"google.golang.org/grpc/keepalive"
-
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 )
 )
 
 
-// Implementation of YandexCloudCreator.
-type YandexCloudCreator struct {
-}
+// Creates a connection to the given Yandex.Cloud API endpoint.
+func NewGrpcConnection(
+	ctx context.Context,
+	apiEndpoint string,
+	apiEndpointID string, // an ID from https://api.cloud.yandex.net/endpoints
+	authorizedKey *iamkey.Key,
+	caCertificate []byte,
+) (*grpc.ClientConn, error) {
+	tlsConfig, err := tlsConfig(caCertificate)
+	if err != nil {
+		return nil, err
+	}
 
 
-func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
-	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -44,26 +49,15 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
 		_ = closeSDK(ctx, sdk)
 		_ = closeSDK(ctx, sdk)
 	}()
 	}()
 
 
-	payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
-		ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
+	serviceAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
+		ApiEndpointId: apiEndpointID,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	tlsConfig := tls.Config{MinVersion: tls.VersionTLS12}
-
-	if caCertificate != nil {
-		caCertPool := x509.NewCertPool()
-		ok := caCertPool.AppendCertsFromPEM(caCertificate)
-		if !ok {
-			return nil, errors.New("unable to read certificate from PEM file")
-		}
-		tlsConfig.RootCAs = caCertPool
-	}
-
-	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
-		grpc.WithTransportCredentials(credentials.NewTLS(&tlsConfig)),
+	return grpc.Dial(serviceAPIEndpoint.Address,
+		grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
 		grpc.WithKeepaliveParams(keepalive.ClientParameters{
 		grpc.WithKeepaliveParams(keepalive.ClientParameters{
 			Time:                time.Second * 30,
 			Time:                time.Second * 30,
 			Timeout:             time.Second * 10,
 			Timeout:             time.Second * 10,
@@ -71,15 +65,16 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
 		}),
 		}),
 		grpc.WithUserAgent("external-secrets"),
 		grpc.WithUserAgent("external-secrets"),
 	)
 	)
+}
+
+// Exchanges the given authorized key to an IAM token.
+func NewIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
+	tlsConfig, err := tlsConfig(caCertificate)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		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)
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -92,14 +87,23 @@ func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint st
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
+	return &IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
 }
 }
 
 
-func (lb *YandexCloudCreator) Now() time.Time {
-	return time.Now()
+func tlsConfig(caCertificate []byte) (*tls.Config, error) {
+	tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
+	if caCertificate != nil {
+		caCertPool := x509.NewCertPool()
+		ok := caCertPool.AppendCertsFromPEM(caCertificate)
+		if !ok {
+			return nil, errors.New("unable to read trusted CA certificates")
+		}
+		tlsConfig.RootCAs = caCertPool
+	}
+	return tlsConfig, nil
 }
 }
 
 
-func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
+func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, tlsConfig *tls.Config) (*ycsdk.SDK, error) {
 	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
 	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -108,6 +112,7 @@ func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key
 	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
 	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
 		Credentials: creds,
 		Credentials: creds,
 		Endpoint:    apiEndpoint,
 		Endpoint:    apiEndpoint,
+		TLSConfig:   tlsConfig,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -120,34 +125,14 @@ func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
 	return sdk.Shutdown(ctx)
 	return sdk.Shutdown(ctx)
 }
 }
 
 
-// Implementation of LockboxClient.
-type LockboxClient struct {
-	lockboxPayloadClient lockbox.PayloadServiceClient
-}
-
-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
-}
-
-type perRPCCredentials struct {
-	iamToken string
+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 (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 {
+func (PerRPCCredentials) RequireTransportSecurity() bool {
 	return true
 	return true
 }
 }

+ 24 - 0
pkg/provider/yandex/common/secretgetter.go

@@ -0,0 +1,24 @@
+/*
+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 common
+
+import (
+	"context"
+)
+
+// Adapts the secrets received from a remote Yandex.Cloud service for the format expected by v1beta1.SecretsClient.
+type SecretGetter interface {
+	GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error)
+	GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error)
+}

+ 51 - 0
pkg/provider/yandex/common/secretsclient.go

@@ -0,0 +1,51 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
+
+// Implementation of v1beta1.SecretsClient.
+type yandexCloudSecretsClient struct {
+	secretGetter SecretGetter
+	iamToken     string
+}
+
+func (c *yandexCloudSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	// TO be implemented
+	return nil, fmt.Errorf("GetAllSecrets not supported")
+}
+
+func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	return c.secretGetter.GetSecret(ctx, c.iamToken, ref.Key, ref.Version, ref.Property)
+}
+
+func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return c.secretGetter.GetSecretMap(ctx, c.iamToken, ref.Key, ref.Version)
+}
+
+func (c *yandexCloudSecretsClient) Close(ctx context.Context) error {
+	return nil
+}
+
+func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultReady, nil
+}

+ 3 - 17
pkg/provider/yandex/lockbox/client/client.go

@@ -15,25 +15,11 @@ package client
 
 
 import (
 import (
 	"context"
 	"context"
-	"time"
 
 
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
-	"github.com/yandex-cloud/go-sdk/iamkey"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 )
 )
 
 
-// Creates Lockbox clients and Yandex.Cloud IAM tokens.
-type YandexCloudCreator interface {
-	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (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.
+// Requests the payload of the given secret from Lockbox.
 type LockboxClient interface {
 type LockboxClient interface {
-	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
+	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error)
 }
 }

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

@@ -1,152 +0,0 @@
-/*
-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"
-	"time"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"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 YandexCloudCreator.
-type YandexCloudCreator struct {
-	Backend *LockboxBackend
-}
-
-func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (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
-}
-
-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 {
-	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 {
-	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
-}
-
-type tokenKey struct {
-	token string
-}
-
-type tokenValue struct {
-	authorizedKey *iamkey.Key
-	expiresAt     time.Time
-}
-
-func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
-	return &LockboxBackend{
-		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 := uuid.NewString()
-	versionID := uuid.NewString()
-
-	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 := uuid.NewString()
-
-	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) 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 _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
-		return nil, fmt.Errorf("unauthenticated")
-	}
-
-	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, cmpopts.IgnoreUnexported(iamkey.Key{})) {
-		return nil, fmt.Errorf("permission denied")
-	}
-
-	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
-}

+ 136 - 0
pkg/provider/yandex/lockbox/client/fakeclient.go

@@ -0,0 +1,136 @@
+/*
+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"
+	"fmt"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/uuid"
+	api "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/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+// Fake implementation of LockboxClient.
+type fakeLockboxClient struct {
+	fakeLockboxServer *FakeLockboxServer
+}
+
+func NewFakeLockboxClient(fakeLockboxServer *FakeLockboxServer) LockboxClient {
+	return &fakeLockboxClient{fakeLockboxServer}
+}
+
+func (c *fakeLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	return c.fakeLockboxServer.getEntries(iamToken, secretID, versionID)
+}
+
+// Fakes Yandex Lockbox service backend.
+type FakeLockboxServer struct {
+	secretMap  map[secretKey]secretValue   // secret specific data
+	versionMap map[versionKey]versionValue // version specific data
+	tokenMap   map[tokenKey]tokenValue     // token specific data
+
+	tokenExpirationDuration time.Duration
+	clock                   clock.Clock
+}
+
+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 []*api.Payload_Entry
+}
+
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewFakeLockboxServer(clock clock.Clock, tokenExpirationDuration time.Duration) *FakeLockboxServer {
+	return &FakeLockboxServer{
+		secretMap:               make(map[secretKey]secretValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		clock:                   clock,
+	}
+}
+
+func (s *FakeLockboxServer) CreateSecret(authorizedKey *iamkey.Key, entries ...*api.Payload_Entry) (string, string) {
+	secretID := uuid.NewString()
+	versionID := uuid.NewString()
+
+	s.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
+	s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return secretID, versionID
+}
+
+func (s *FakeLockboxServer) AddVersion(secretID string, entries ...*api.Payload_Entry) string {
+	versionID := uuid.NewString()
+
+	s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return versionID
+}
+
+func (s *FakeLockboxServer) NewIamToken(authorizedKey *iamkey.Key) *common.IamToken {
+	token := uuid.NewString()
+	expiresAt := s.clock.CurrentTime().Add(s.tokenExpirationDuration)
+	s.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &common.IamToken{Token: token, ExpiresAt: expiresAt}
+}
+
+func (s *FakeLockboxServer) getEntries(iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	if _, ok := s.secretMap[secretKey{secretID}]; !ok {
+		return nil, fmt.Errorf("secret not found")
+	}
+	if _, ok := s.versionMap[versionKey{secretID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if _, ok := s.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
+	}
+
+	if s.tokenMap[tokenKey{iamToken}].expiresAt.Before(s.clock.CurrentTime()) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(s.tokenMap[tokenKey{iamToken}].authorizedKey, s.secretMap[secretKey{secretID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return s.versionMap[versionKey{secretID, versionID}].entries, nil
+}

+ 58 - 0
pkg/provider/yandex/lockbox/client/grpcclient.go

@@ -0,0 +1,58 @@
+/*
+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"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+// Real/gRPC implementation of LockboxClient.
+type grpcLockboxClient struct {
+	lockboxPayloadClient api.PayloadServiceClient
+}
+
+func NewGrpcLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (LockboxClient, error) {
+	conn, err := common.NewGrpcConnection(
+		ctx,
+		apiEndpoint,
+		"lockbox-payload", // taken from https://api.cloud.yandex.net/endpoints
+		authorizedKey,
+		caCertificate,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &grpcLockboxClient{api.NewPayloadServiceClient(conn)}, nil
+}
+
+func (c *grpcLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	payload, err := c.lockboxPayloadClient.Get(
+		ctx,
+		&api.GetPayloadRequest{
+			SecretId:  secretID,
+			VersionId: versionID,
+		},
+		grpc.PerRPCCredentials(common.PerRPCCredentials{IamToken: iamToken}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return payload.Entries, nil
+}

+ 24 - 286
pkg/provider/yandex/lockbox/lockbox.go

@@ -15,326 +15,64 @@ package lockbox
 
 
 import (
 import (
 	"context"
 	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"fmt"
-	"sync"
 	"time"
 	"time"
 
 
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	"github.com/yandex-cloud/go-sdk/iamkey"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
-	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
-	"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")
 var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
 
 
-type iamTokenKey struct {
-	authorizedKeyID  string
-	serviceAccountID string
-	privateKeyHash   string
-}
-
-// https://github.com/external-secrets/external-secrets/issues/644
-var _ esv1beta1.SecretsClient = &lockboxSecretsClient{}
-var _ esv1beta1.Provider = &lockboxProvider{}
-
-// lockboxProvider is a provider for Yandex Lockbox.
-type lockboxProvider struct {
-	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.
-func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+func adaptInput(store esv1beta1.GenericStore) (*common.SecretsClientInput, error) {
 	storeSpec := store.GetSpec()
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
 		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
 		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
 	}
 	}
 	storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
 	storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
 
 
-	authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
-	if authorizedKeySecretName == "" {
+	if storeSpecYandexLockbox.Auth.AuthorizedKey.Name == "" {
 		return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
 		return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
 	}
 	}
-	objectKey := types.NamespacedName{
-		Name:      authorizedKeySecretName,
-		Namespace: namespace,
-	}
-
-	// only ClusterStore is allowed to set namespace (and then it's required)
-	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
-		if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
-			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
-		}
-		objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
-	}
-
-	authorizedKeySecret := &corev1.Secret{}
-	err := kube.Get(ctx, objectKey, authorizedKeySecret)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
-	}
-
-	authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
-	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
-		return nil, fmt.Errorf("missing AuthorizedKey")
-	}
-
-	var authorizedKey iamkey.Key
-	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
-	if err != nil {
-		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
-	}
-
-	var caCertificateData []byte
 
 
+	var caCertificate *esmeta.SecretKeySelector
 	if storeSpecYandexLockbox.CAProvider != nil {
 	if storeSpecYandexLockbox.CAProvider != nil {
-		certObjectKey := types.NamespacedName{
-			Name:      storeSpecYandexLockbox.CAProvider.Certificate.Name,
-			Namespace: namespace,
-		}
-
-		if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
-			if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
-				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
-			}
-			certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
-		}
-
-		caCertificateSecret := &corev1.Secret{}
-		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
-		if err != nil {
-			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
-		}
-
-		caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
-		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
-			return nil, fmt.Errorf("missing CA Certificate")
-		}
-	}
-
-	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
-	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 IAM token: %w", err)
-	}
-
-	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
-}
-
-func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (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, caCertificate)
-		if err != nil {
-			return nil, err
-		}
-		p.lockboxClientMap[apiEndpoint] = lockboxClient
+		caCertificate = &storeSpecYandexLockbox.CAProvider.Certificate
 	}
 	}
-	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
+	return &common.SecretsClientInput{
+		APIEndpoint:   storeSpecYandexLockbox.APIEndpoint,
+		AuthorizedKey: storeSpecYandexLockbox.Auth.AuthorizedKey,
+		CACertificate: caCertificate,
+	}, nil
 }
 }
 
 
-func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
-	now := p.yandexCloudCreator.Now()
-	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
-}
-
-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)
-		}
-	}
-}
-
-func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
-	return nil
-}
-
-// lockboxSecretsClient is a secrets client for Yandex Lockbox.
-type lockboxSecretsClient struct {
-	lockboxClient client.LockboxClient
-	iamToken      string
-}
-
-// Empty GetAllSecrets.
-func (c *lockboxSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
-	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
-}
-
-// GetSecret returns a single secret from the provider.
-func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.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)
-	}
-
-	if ref.Property == "" {
-		keyToValue := make(map[string]interface{}, len(entries))
-		for _, entry := range entries {
-			value, err := getValueAsIs(entry)
-			if err != nil {
-				return nil, err
-			}
-			keyToValue[entry.Key] = value
-		}
-		out, err := json.Marshal(keyToValue)
-		if err != nil {
-			return nil, fmt.Errorf("failed to marshal secret: %w", err)
-		}
-		return out, nil
-	}
-
-	entry, err := findEntryByKey(entries, ref.Property)
+func newSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+	lockboxClient, err := client.NewGrpcLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	return getValueAsBinary(entry)
-}
-
-// GetSecretMap returns multiple k/v pairs from the provider.
-func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.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)
-	}
-
-	secretMap := make(map[string][]byte, len(entries))
-	for _, entry := range entries {
-		value, err := getValueAsBinary(entry)
-		if err != nil {
-			return nil, err
-		}
-		secretMap[entry.Key] = value
-	}
-	return secretMap, nil
-}
-
-func (c *lockboxSecretsClient) Close(ctx context.Context) error {
-	return nil
-}
-
-func (c *lockboxSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
-	return esv1beta1.ValidationResultReady, nil
-}
-
-func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
-	switch entry.Value.(type) {
-	case *lockbox.Payload_Entry_TextValue:
-		return entry.GetTextValue(), nil
-	case *lockbox.Payload_Entry_BinaryValue:
-		return entry.GetBinaryValue(), nil
-	default:
-		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
-	}
-}
-
-func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
-	switch entry.Value.(type) {
-	case *lockbox.Payload_Entry_TextValue:
-		return []byte(entry.GetTextValue()), nil
-	case *lockbox.Payload_Entry_BinaryValue:
-		return entry.GetBinaryValue(), nil
-	default:
-		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
-	}
-}
-
-func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
-	for i := range entries {
-		if entries[i].Key == key {
-			return entries[i], nil
-		}
-	}
-	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
+	return newLockboxSecretGetter(lockboxClient)
 }
 }
 
 
 func init() {
 func init() {
-	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
-
-	go func() {
-		for {
-			time.Sleep(iamTokenCleanupDelay)
-			lockboxProvider.cleanUpIamTokenMap()
-		}
-	}()
+	provider := common.InitYandexCloudProvider(
+		log,
+		clock.NewRealClock(),
+		adaptInput,
+		newSecretGetter,
+		common.NewIamToken,
+		time.Hour,
+	)
 
 
 	esv1beta1.Register(
 	esv1beta1.Register(
-		lockboxProvider,
+		provider,
 		&esv1beta1.SecretStoreProvider{
 		&esv1beta1.SecretStoreProvider{
 			YandexLockbox: &esv1beta1.YandexLockboxProvider{},
 			YandexLockbox: &esv1beta1.YandexLockboxProvider{},
 		},
 		},

+ 122 - 143
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -15,11 +15,8 @@ package lockbox
 
 
 import (
 import (
 	"context"
 	"context"
-	"crypto/x509"
-	"crypto/x509/pkix"
 	b64 "encoding/base64"
 	b64 "encoding/base64"
 	"encoding/json"
 	"encoding/json"
-	"math/big"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -29,12 +26,15 @@ import (
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrl "sigs.k8s.io/controller-runtime"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 )
 )
 
 
 const (
 const (
@@ -83,7 +83,7 @@ func TestNewClient(t *testing.T) {
 	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
 	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
 	tassert.Nil(t, secretClient)
 	tassert.Nil(t, secretClient)
 
 
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, newFakeAuthorizedKey()))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
 	const caCertificateSecretName = "caCertificateSecretName"
 	const caCertificateSecretName = "caCertificateSecretName"
@@ -98,10 +98,10 @@ func TestNewClient(t *testing.T) {
 	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
 	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
 	tassert.Nil(t, secretClient)
 	tassert.Nil(t, secretClient)
 
 
-	err = createK8sSecret(ctx, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, newFakeCACertificate())
+	err = createK8sSecret(ctx, t, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, []byte("it-is-not-a-certificate"))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
 	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
-	tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
+	tassert.EqualError(t, err, "failed to create Yandex.Cloud client: unable to read trusted CA certificates")
 	tassert.Nil(t, secretClient)
 	tassert.Nil(t, secretClient)
 }
 }
 
 
@@ -110,10 +110,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 		binaryEntry(k2, v2),
 	)
 	)
@@ -121,13 +122,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -148,10 +147,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 		binaryEntry(k2, v2),
 	)
 	)
@@ -159,13 +159,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
@@ -179,10 +177,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 		binaryEntry(k2, v2),
 	)
 	)
@@ -190,13 +189,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
@@ -210,22 +207,21 @@ func TestGetSecretByVersionID(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
 	oldKey, oldVal := "oldKey", "oldVal"
-	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
 		textEntry(oldKey, oldVal),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@@ -234,7 +230,7 @@ func TestGetSecretByVersionID(t *testing.T) {
 	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
 	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
 
 
 	newKey, newVal := "newKey", "newVal"
 	newKey, newVal := "newKey", "newVal"
-	newVersionID := lockboxBackend.AddVersion(secretID,
+	newVersionID := fakeLockboxServer.AddVersion(secretID,
 		textEntry(newKey, newVal),
 		textEntry(newKey, newVal),
 	)
 	)
 
 
@@ -253,21 +249,20 @@ func TestGetSecretUnauthorized(t *testing.T) {
 	authorizedKeyA := newFakeAuthorizedKey()
 	authorizedKeyA := newFakeAuthorizedKey()
 	authorizedKeyB := newFakeAuthorizedKey()
 	authorizedKeyB := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKeyA,
 		textEntry("k1", "v1"),
 		textEntry("k1", "v1"),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKeyB))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -279,24 +274,23 @@ func TestGetSecretNotFound(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
 	tassert.EqualError(t, err, errSecretPayloadNotFound)
 	tassert.EqualError(t, err, errSecretPayloadNotFound)
 
 
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry("k1", "v1"),
 		textEntry("k1", "v1"),
 	)
 	)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
@@ -310,29 +304,28 @@ func TestGetSecretWithTwoNamespaces(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
-	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+	secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 	)
 	)
 	k2, v2 := "k2", "v2"
 	k2, v2 := "k2", "v2"
-	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
 		textEntry(k2, v2),
 		textEntry(k2, v2),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
+	err := createK8sSecret(ctx, t, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
-	err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
 	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
 	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
 	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
 	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
 	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
@@ -361,36 +354,33 @@ func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
 
-	lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer1 := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
-	secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
+	secretID1, _ := fakeLockboxServer1.CreateSecret(authorizedKey1,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 	)
 	)
-	lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
+	fakeLockboxServer2 := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k2, v2 := "k2", "v2"
 	k2, v2 := "k2", "v2"
-	secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer2.CreateSecret(authorizedKey2,
 		textEntry(k2, v2),
 		textEntry(k2, v2),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
 	store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 	store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 
 
-	provider1 := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend1,
-	})
-	provider2 := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend2,
-	})
+	provider1 := newLockboxProvider(fakeClock, fakeLockboxServer1)
+	provider2 := newLockboxProvider(fakeClock, fakeLockboxServer2)
 
 
 	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
 	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
@@ -419,23 +409,22 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
+	fakeClock := clock.NewFakeClock()
 	tokenExpirationTime := time.Hour
 	tokenExpirationTime := time.Hour
-	lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationTime)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 
 
 	var data []byte
 	var data []byte
 
 
@@ -445,7 +434,7 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
 	tassert.Equal(t, v1, string(data))
 	tassert.Equal(t, v1, string(data))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
-	lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
+	fakeClock.AddDuration(2 * tokenExpirationTime)
 
 
 	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
 	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
 	tassert.Nil(t, data)
 	tassert.Nil(t, data)
@@ -464,12 +453,13 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
 
+	fakeClock := clock.NewFakeClock()
 	tokenExpirationDuration := time.Hour
 	tokenExpirationDuration := time.Hour
-	lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
-	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationDuration)
+	secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
 		textEntry("k1", "v1"),
 		textEntry("k1", "v1"),
 	)
 	)
-	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
 		textEntry("k2", "v2"),
 		textEntry("k2", "v2"),
 	)
 	)
 
 
@@ -478,22 +468,20 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
 	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 
 
 	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
 	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
 	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
@@ -501,10 +489,10 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID1})
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID1})
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
+	fakeClock.AddDuration(tokenExpirationDuration * 2)
 
 
 	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
 	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
 	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
 	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
@@ -512,28 +500,28 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID2})
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID2})
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+	fakeClock.AddDuration(tokenExpirationDuration)
 
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
 
-	provider.cleanUpIamTokenMap()
+	provider.CleanUpIamTokenMap()
 
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+	fakeClock.AddDuration(tokenExpirationDuration)
 
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
 
-	provider.cleanUpIamTokenMap()
+	provider.CleanUpIamTokenMap()
 
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 }
 }
 
 
 func TestGetSecretMap(t *testing.T) {
 func TestGetSecretMap(t *testing.T) {
@@ -541,10 +529,11 @@ func TestGetSecretMap(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 		binaryEntry(k2, v2),
 	)
 	)
@@ -552,13 +541,11 @@ func TestGetSecretMap(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -579,22 +566,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 	namespace := uuid.NewString()
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 	authorizedKey := newFakeAuthorizedKey()
 
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
 	oldKey, oldVal := "oldKey", "oldVal"
-	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
 		textEntry(oldKey, oldVal),
 	)
 	)
 
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@@ -603,7 +589,7 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
 	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
 
 
 	newKey, newVal := "newKey", "newVal"
 	newKey, newVal := "newKey", "newVal"
-	newVersionID := lockboxBackend.AddVersion(secretID,
+	newVersionID := fakeLockboxServer.AddVersion(secretID,
 		textEntry(newKey, newVal),
 		textEntry(newKey, newVal),
 	)
 	)
 
 
@@ -618,6 +604,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 
 
 // helper functions
 // helper functions
 
 
+func newLockboxProvider(clock clock.Clock, fakeLockboxServer *client.FakeLockboxServer) *common.YandexCloudProvider {
+	return common.InitYandexCloudProvider(
+		ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox"),
+		clock,
+		adaptInput,
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+			return newLockboxSecretGetter(client.NewFakeLockboxClient(fakeLockboxServer))
+		},
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*common.IamToken, error) {
+			return fakeLockboxServer.NewIamToken(authorizedKey), nil
+		},
+		0,
+	)
+}
+
 func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
 func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
 	return &esv1beta1.SecretStore{
 	return &esv1beta1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
 		ObjectMeta: metav1.ObjectMeta{
@@ -639,23 +640,21 @@ func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName
 	}
 	}
 }
 }
 
 
-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
-	}
+func toJSON(t *testing.T, v interface{}) []byte {
+	jsonBytes, err := json.Marshal(v)
+	tassert.Nil(t, err)
+	return jsonBytes
+}
 
 
-	err = k8sClient.Create(ctx, &corev1.Secret{
+func createK8sSecret(ctx context.Context, t *testing.T, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
+	err := k8sClient.Create(ctx, &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
 			Namespace: namespace,
 			Name:      secretName,
 			Name:      secretName,
 		},
 		},
-		Data: map[string][]byte{secretKey: data},
+		Data: map[string][]byte{secretKey: secretValue},
 	})
 	})
-	if err != nil {
-		return err
-	}
-
+	tassert.Nil(t, err)
 	return nil
 	return nil
 }
 }
 
 
@@ -670,26 +669,6 @@ func newFakeAuthorizedKey() *iamkey.Key {
 	}
 	}
 }
 }
 
 
-func newFakeCACertificate() []byte {
-	cert := x509.Certificate{
-		SerialNumber: big.NewInt(2019),
-		Subject: pkix.Name{
-			Organization:  []string{"Company, INC."},
-			Country:       []string{"US"},
-			Locality:      []string{"San Francisco"},
-			StreetAddress: []string{"Golden Gate Bridge"},
-			PostalCode:    []string{"94016"},
-		},
-		NotBefore:             time.Now(),
-		NotAfter:              time.Now().AddDate(10, 0, 0),
-		IsCA:                  true,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
-		BasicConstraintsValid: true,
-	}
-	return cert.Raw
-}
-
 func textEntry(key, value string) *lockbox.Payload_Entry {
 func textEntry(key, value string) *lockbox.Payload_Entry {
 	return &lockbox.Payload_Entry{
 	return &lockbox.Payload_Entry{
 		Key: key,
 		Key: key,

+ 113 - 0
pkg/provider/yandex/lockbox/lockboxsecretgetter.go

@@ -0,0 +1,113 @@
+/*
+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"
+	"encoding/json"
+	"fmt"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Implementation of common.SecretGetter.
+type lockboxSecretGetter struct {
+	lockboxClient client.LockboxClient
+}
+
+func newLockboxSecretGetter(lockboxClient client.LockboxClient) (common.SecretGetter, error) {
+	return &lockboxSecretGetter{
+		lockboxClient: lockboxClient,
+	}, nil
+}
+
+func (g *lockboxSecretGetter) GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error) {
+	entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
+	}
+
+	if property == "" {
+		keyToValue := make(map[string]interface{}, len(entries))
+		for _, entry := range entries {
+			value, err := getValueAsIs(entry)
+			if err != nil {
+				return nil, err
+			}
+			keyToValue[entry.Key] = value
+		}
+		out, err := json.Marshal(keyToValue)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshal secret: %w", err)
+		}
+		return out, nil
+	}
+
+	entry, err := findEntryByKey(entries, property)
+	if err != nil {
+		return nil, err
+	}
+	return getValueAsBinary(entry)
+}
+
+func (g *lockboxSecretGetter) GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error) {
+	entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
+	}
+
+	secretMap := make(map[string][]byte, len(entries))
+	for _, entry := range entries {
+		value, err := getValueAsBinary(entry)
+		if err != nil {
+			return nil, err
+		}
+		secretMap[entry.Key] = value
+	}
+	return secretMap, nil
+}
+
+func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return entry.GetTextValue(), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return []byte(entry.GetTextValue()), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
+	for i := range entries {
+		if entries[i].Key == key {
+			return entries[i], nil
+		}
+	}
+	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
+}