Browse Source

Add Conjur Support for FindByName, FindByTag (#3364)

Shlomo Zalman Heigh 1 year ago
parent
commit
02c6f625bd

+ 2 - 2
docs/introduction/stability-support.md

@@ -51,7 +51,7 @@ The following table describes the stability level of each provider and who's res
 | [Doppler SecretOps Platform](https://external-secrets.io/latest/provider/doppler)                          |   alpha   |                                                                                         [@ryan-blunden](https://github.com/ryan-blunden/) [@nmanoogian](https://github.com/nmanoogian/) |
 | [Keeper Security](https://www.keepersecurity.com/)                                                         |   alpha   |                                                                                                                                              [@ppodevlab](https://github.com/ppodevlab) |
 | [Scaleway](https://external-secrets.io/latest/provider/scaleway)                                           |   alpha   |                                                                                                                                                   [@azert9](https://github.com/azert9/) |
-| [Conjur](https://external-secrets.io/latest/provider/conjur)                                               |   alpha   |                                                                                                                                 [@davidh-cyberark](https://github.com/davidh-cyberark/) |
+| [Conjur](https://external-secrets.io/latest/provider/conjur)                                               |   stable   |                                                                                                                                 [@davidh-cyberark](https://github.com/davidh-cyberark/) [@szh](https://github.com/szh) |
 | [Delinea](https://external-secrets.io/latest/provider/delinea)                                             |   alpha   |                                                                                                                                     [@michaelsauter](https://github.com/michaelsauter/) |
 | [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi)                                           |   alpha   |                                                                                                                                                  [@dirien](https://github.com/dirien) |
 | [Passbolt](https://external-secrets.io/latest/provider/passbolt)                                           |   alpha   |                                                                                                                                                   |
@@ -80,7 +80,7 @@ The following table show the support for features across different providers.
 | Doppler                   |      x       |              |                      |                         |        x         |             |                             |
 | Keeper Security           |      x       |              |                      |                         |        x         |      x      |                             |
 | Scaleway                  |      x       |      x       |                      |                         |        x         |      x      |              x              |
-| Conjur                    |              |              |                      |                         |        x         |             |                             |
+| Conjur                    |      x       |      x       |                      |                         |        x         |             |                             |
 | Delinea                   |      x       |              |                      |                         |        x         |             |                             |
 | Pulumi ESC                |      x       |              |                      |                         |        x         |             |                             |
 | Passbolt                  |      x       |              |                      |                         |        x         |             |                             |

+ 69 - 32
docs/provider/conjur.md

@@ -6,12 +6,12 @@ This section describes how to set up the Conjur provider for External Secrets Op
 
 Before installing the Conjur provider, you need:
 
-*   A running Conjur Server, with:
-    *   An accessible Conjur endpoint (for example: `https://myapi.example.com`).
-    *   Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
-    *   Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
-    *   **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
-*   A Kubernetes cluster with ESO installed.
+* A running Conjur Server, with:
+  * An accessible Conjur endpoint (for example: `https://myapi.example.com`).
+  * Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
+  * Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
+  * **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
+* A Kubernetes cluster with ESO installed.
 
 ### Conjur server certificate
 
@@ -21,11 +21,18 @@ If you set up your Conjur server with a self-signed certificate, we recommend th
 {% include 'conjur-ca-bundle.yaml' %}
 ```
 
-### External secret store with apiKey authentication
+### External secret store
+
+The Conjur provider is configured as an external secret store in ESO. The Conjur provider supports these two methods to authenticate to Conjur:
+
+* [`apikey`](#option-1-external-secret-store-with-apikey-authentication): uses a Conjur `hostid` and `apikey` to authenticate with Conjur
+* [`jwt`](#option-2-external-secret-store-with-jwt-authentication): uses a JWT to authenticate with Conjur
+
+#### Option 1: External secret store with apiKey authentication
 
 This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It is the simplest method to set up and use because your Conjur instance requires no additional configuration.
 
-#### Step 1: Create an external secret store
+##### Step 1: Define an external secret store
 
 !!! Tip
     Save as the file as: `conjur-secret-store.yaml`
@@ -34,7 +41,7 @@ This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It
 {% include 'conjur-secret-store-apikey.yaml' %}
 ```
 
-#### Step 2: Create Kubernetes secrets
+##### Step 2: Create Kubernetes secrets for Conjur credentials
 
 To connect to the Conjur server, the **ESO Conjur provider** needs to retrieve the `apikey` credentials from K8s secrets.
 
@@ -54,19 +61,36 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
 !!! Note
     `conjur-creds` is the `name` defined in the `userRef` and `apikeyRef` fields of the `conjur-secret-store.yml` file.
 
-### External secret store with JWT authentication
+
+##### Step 3: Create the external secrets store
+
+!!! Important
+    Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
+
+```shell
+# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
+#
+kubectl apply -n external-secrets -f conjur-secret-store.yaml
+
+# WARNING: running the delete command will delete the secret store configuration
+#
+# If there is a need to delete the external secretstore
+# kubectl delete secretstore -n external-secrets conjur
+```
+
+#### Option 2: External secret store with JWT authentication
 
 This method uses JWT tokens to authenticate with Conjur. You can use the following methods to retrieve a JWT token for authentication:
 
--  JWT token from a referenced Kubernetes service account
--  JWT token stored in a Kubernetes secret
+* JWT token from a referenced Kubernetes service account
+* JWT token stored in a Kubernetes secret
 
-#### Step 1: Define an external secret store
+##### Step 1: Define an external secret store
 
 When you use JWT authentication, the following must be specified in the `SecretStore`:
 
-- `account` -  The name of the Conjur account
-- `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that is used to authenticate the JWT token
+* `account` -  The name of the Conjur account
+* `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that is used to authenticate the JWT token
 
 You can retrieve the JWT token from either a referenced service account or a Kubernetes secret.
 
@@ -93,20 +117,9 @@ You can use an external JWT issuer or the Kubernetes API server to create the to
 kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
 ```
 
-Save the secret store file as `conjur-secret-store.yaml` (the filename used in subsequent steps).
-
-#### Step 2: Define an external secret
-
-Save the external secret file as: `conjur-external-secret.yaml`
-
-```yaml
-{% include 'conjur-external-secret.yaml' %}
-```
-
-!!!Important
-    Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
+Save the secret store file as `conjur-secret-store.yaml`.
 
-#### Step 3: Create the external secrets store
+##### Step 2: Create the external secrets store
 
 ```shell
 # WARNING: creates the store in the "external-secrets" namespace, update the value as needed
@@ -119,7 +132,32 @@ kubectl apply -n external-secrets -f conjur-secret-store.yaml
 # kubectl delete secretstore -n external-secrets conjur
 ```
 
-#### Step 4: Create the external secret
+### Define an external secret
+
+After you have configured the Conjur provider secret store, you can fetch secrets from Conjur.
+
+Here is an example of how to fetch a single secret from Conjur:
+
+```yaml
+{% include 'conjur-external-secret.yaml' %}
+```
+
+Save the external secret file as `conjur-external-secret.yaml`.
+
+#### Find by Name and Find by Tag
+
+The Conjur provider also supports the Find by Name and Find by Tag ESO features. This means that
+you can use a regular expression or tags to dynamically fetch multiple secrets from Conjur.
+
+```yaml
+{% include 'conjur-external-secret-find.yaml' %}
+```
+
+If you use these features, we strongly recommend that you limit the permissions of the Conjur host
+to only the secrets that it needs to access. This is more secure and it reduces the load on
+both the Conjur server and ESO.
+
+### Create the external secret
 
 ```shell
 # WARNING: creates the external-secret in the "external-secrets" namespace, update the value as needed
@@ -132,7 +170,7 @@ kubectl apply -n external-secrets -f conjur-external-secret.yaml
 # kubectl delete externalsecret -n external-secrets conjur
 ```
 
-#### Step 5: Get the K8s secret
+### Get the K8s secret
 
 * Log in to your Conjur server and verify that your secret exists
 * Review the value of your Kubernetes secret to verify that it contains the same value as the Conjur server
@@ -149,10 +187,9 @@ kubectl get secret -n external-secrets conjur -o jsonpath="{.data.secret00}"  |
 * [Accelerator-K8s-External-Secrets repo](https://github.com/conjurdemos/Accelerator-K8s-External-Secrets)
 * [Configure Conjur JWT authentication](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm)
 
-
 ### License
 
-Copyright (c) 2023 CyberArk Software Ltd. All rights reserved.
+Copyright (c) 2023-2024 CyberArk Software Ltd. All rights reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

+ 22 - 0
docs/snippets/conjur-external-secret-find.yaml

@@ -0,0 +1,22 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: conjur-find-by-name
+spec:
+  refreshInterval: 10s
+  secretStoreRef:
+    # This name must match the metadata.name in the `SecretStore`
+    name: conjur
+    kind: SecretStore
+  target:
+    name: k8s-secret-to-be-created
+  dataFrom:
+    - find:
+        # You can use *either* `name` or `tags` to filter the secrets. Here are basic examples of both:
+        name:
+          # Match all secrets in the app1 namespace (e.g., `app1/secret00`, `app1/secret01`, etc.)
+          regexp: "^app1\/.+$"
+        tags:
+          # Only fetch Conjur secrets with the following annotations
+          environment: "prod"
+          application: "app1"

+ 1 - 3
e2e/go.mod

@@ -1,8 +1,6 @@
 module github.com/external-secrets/external-secrets-e2e
 
-go 1.22
-
-toolchain go1.22.1
+go 1.22.1
 
 replace github.com/external-secrets/external-secrets => ../
 

+ 21 - 1
e2e/suites/provider/cases/conjur/conjur.go

@@ -32,20 +32,40 @@ var _ = Describe("[conjur]", Label("conjur"), func() {
 
 	DescribeTable("sync secrets",
 		framework.TableFuncWithExternalSecret(f, prov),
-		// uses token auth
+		// use api key auth
+		framework.Compose(withTokenAuth, f, common.FindByName, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.FindByNameAndRewrite, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.FindByTag, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.SimpleDataSync, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.SyncWithoutTargetName, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplateFromLiteral, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.TemplateFromConfigmaps, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.SSHKeySync, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.SSHKeySyncDataProperty, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.DockerJSONConfig, useApiKeyAuth),
+		framework.Compose(withTokenAuth, f, common.NestedJSONWithGJSON, useApiKeyAuth),
 		framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useApiKeyAuth),
 
 		// use jwt k8s provider
+		framework.Compose(withJWTK8s, f, common.FindByName, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.FindByNameAndRewrite, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.FindByTag, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.SimpleDataSync, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.SyncWithoutTargetName, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
 
 		// use jwt k8s hostid provider
+		framework.Compose(withJWTK8sHostID, f, common.FindByName, useJWTK8sHostIDProvider),
+		framework.Compose(withJWTK8sHostID, f, common.FindByNameAndRewrite, useJWTK8sHostIDProvider),
+		framework.Compose(withJWTK8sHostID, f, common.FindByTag, useJWTK8sHostIDProvider),
 		framework.Compose(withJWTK8sHostID, f, common.SimpleDataSync, useJWTK8sHostIDProvider),
 		framework.Compose(withJWTK8sHostID, f, common.SyncWithoutTargetName, useJWTK8sHostIDProvider),
 		framework.Compose(withJWTK8sHostID, f, common.JSONDataFromSync, useJWTK8sHostIDProvider),

+ 12 - 5
e2e/suites/provider/cases/conjur/policy.go

@@ -20,6 +20,12 @@ import (
 
 const createVariablePolicyTemplate = `- !variable
   id: {{ .Key }}
+  {{ if .Tags }}
+  annotations:
+    {{- range $key, $value := .Tags }}
+    {{ $key }}: "{{ $value }}"
+    {{- end }}
+  {{ end }}
 
 - !permit
   role: !host system:serviceaccount:{{ .Namespace }}:test-app-sa
@@ -44,27 +50,28 @@ const jwtHostPolicyTemplate = `- !host
   privilege: [ read, authenticate ]
   resource: !webservice conjur/authn-jwt/{{ .ServiceID }}`
 
-func createVariablePolicy(key, namespace string) string {
-	return renderTemplate(createVariablePolicyTemplate, map[string]string{
+func createVariablePolicy(key, namespace string, tags map[string]string) string {
+	return renderTemplate(createVariablePolicyTemplate, map[string]interface{}{
 		"Key":       key,
 		"Namespace": namespace,
+		"Tags":      tags,
 	})
 }
 
 func deleteVariablePolicy(key string) string {
-	return renderTemplate(deleteVariablePolicyTemplate, map[string]string{
+	return renderTemplate(deleteVariablePolicyTemplate, map[string]interface{}{
 		"Key": key,
 	})
 }
 
 func createJwtHostPolicy(hostID, serviceID string) string {
-	return renderTemplate(jwtHostPolicyTemplate, map[string]string{
+	return renderTemplate(jwtHostPolicyTemplate, map[string]interface{}{
 		"HostID":    hostID,
 		"ServiceID": serviceID,
 	})
 }
 
-func renderTemplate(templateText string, data map[string]string) string {
+func renderTemplate(templateText string, data map[string]interface{}) string {
 	// Use golang templates to render the policy
 	tmpl, err := template.New("policy").Parse(templateText)
 	if err != nil {

+ 1 - 27
e2e/suites/provider/cases/conjur/provider.go

@@ -49,13 +49,12 @@ func newConjurProvider(f *framework.Framework) *conjurProvider {
 		framework: f,
 	}
 	BeforeEach(prov.BeforeEach)
-	AfterEach(prov.AfterEach)
 	return prov
 }
 
 func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
 	// Generate a policy file for the secret key
-	policy := createVariablePolicy(key, s.framework.Namespace.Name)
+	policy := createVariablePolicy(key, s.framework.Namespace.Name, val.Tags)
 
 	_, err := s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
 	Expect(err).ToNot(HaveOccurred())
@@ -84,31 +83,6 @@ func (s *conjurProvider) BeforeEach() {
 	s.CreateJWTK8sHostIDStore(c, ns)
 }
 
-func (s *conjurProvider) AfterEach() {
-	// Print Conjur logs if the test failed
-	if !CurrentGinkgoTestDescription().Failed {
-		return
-	}
-
-	// Get logs from Conjur pod
-	ns := s.framework.Namespace.Name
-	pods, err := s.framework.KubeClientSet.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{})
-	if err != nil {
-		GinkgoWriter.Printf("Error getting pods: %s\n", err)
-		return
-	}
-
-	for _, pod := range pods.Items {
-		if strings.Contains(pod.Name, "conjur-oss") {
-			logs, err := s.framework.KubeClientSet.CoreV1().Pods(ns).GetLogs(pod.Name, &v1.PodLogOptions{Container: "conjur-oss"}).DoRaw(context.Background())
-			if err != nil {
-				GinkgoWriter.Printf("Error getting logs from Conjur pod: %s\n", err)
-			}
-			GinkgoWriter.Printf("Conjur logs:\n%s\n", logs)
-		}
-	}
-}
-
 func makeStore(name, ns string, c *addon.Conjur) *esv1beta1.SecretStore {
 	return &esv1beta1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{

+ 1 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/external-secrets/external-secrets
 
-go 1.22
+go 1.22.1
 
 require (
 	cloud.google.com/go/iam v1.1.7

+ 12 - 12
pkg/provider/conjur/auth_jwt.go

@@ -34,10 +34,10 @@ import (
 const JwtLifespan = 600 // 10 minutes
 
 // getJWTToken retrieves a JWT token either using the TokenRequest API for a specified service account, or from a jwt stored in a k8s secret.
-func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
+func (c *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
 	if conjurJWTConfig.ServiceAccountRef != nil {
 		// Should work for Kubernetes >=v1.22: fetch token via TokenRequest API
-		jwtToken, err := p.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
+		jwtToken, err := c.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
 		if err != nil {
 			return "", err
 		}
@@ -50,9 +50,9 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
 		}
 		jwtToken, err := resolvers.SecretKeyRef(
 			ctx,
-			p.kube,
-			p.StoreKind,
-			p.namespace,
+			c.kube,
+			c.StoreKind,
+			c.namespace,
 			tokenRef)
 		if err != nil {
 			return "", err
@@ -63,25 +63,25 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
 }
 
 // getJwtFromServiceAccountTokenRequest uses the TokenRequest API to get a JWT token for the given service account.
-func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
+func (c *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
 	audiences := serviceAccountRef.Audiences
 	if len(additionalAud) > 0 {
 		audiences = append(audiences, additionalAud...)
 	}
 	tokenRequest := &authenticationv1.TokenRequest{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: p.namespace,
+			Namespace: c.namespace,
 		},
 		Spec: authenticationv1.TokenRequestSpec{
 			Audiences:         audiences,
 			ExpirationSeconds: &expirationSeconds,
 		},
 	}
-	if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+	if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
 		(serviceAccountRef.Namespace != nil) {
 		tokenRequest.Namespace = *serviceAccountRef.Namespace
 	}
-	tokenResponse, err := p.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
+	tokenResponse, err := c.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
 	if err != nil {
 		return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
 	}
@@ -89,13 +89,13 @@ func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, servi
 }
 
 // newClientFromJwt creates a new Conjur client using the given JWT Auth Config.
-func (p *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
-	jwtToken, getJWTError := p.getJWTToken(ctx, jwtAuth)
+func (c *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
+	jwtToken, getJWTError := c.getJWTToken(ctx, jwtAuth)
 	if getJWTError != nil {
 		return nil, getJWTError
 	}
 
-	client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
+	client, clientError := c.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
 	if clientError != nil {
 		return nil, clientError
 	}

+ 219 - 0
pkg/provider/conjur/client.go

@@ -0,0 +1,219 @@
+/*
+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 conjur
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
+	"github.com/cyberark/conjur-api-go/conjurapi/authn"
+	corev1 "k8s.io/api/core/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"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"
+	"github.com/external-secrets/external-secrets/pkg/provider/conjur/util"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+var (
+	errConjurClient     = "cannot setup new Conjur client: %w"
+	errBadCertBundle    = "caBundle failed to base64 decode: %w"
+	errBadServiceUser   = "could not get Auth.Apikey.UserRef: %w"
+	errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
+
+	errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
+
+	errUnableToFetchCAProviderCM     = "unable to fetch Server.CAProvider ConfigMap: %w"
+	errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
+
+	errSecretKeyFmt = "cannot find secret data for key: %q"
+)
+
+// Client is a provider for Conjur.
+type Client struct {
+	StoreKind string
+	kube      client.Client
+	store     esv1beta1.GenericStore
+	namespace string
+	corev1    typedcorev1.CoreV1Interface
+	clientAPI SecretsClientFactory
+	client    SecretsClient
+}
+
+func (c *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
+	// if the client is initialized already, return it
+	if c.client != nil {
+		return c.client, nil
+	}
+
+	prov, err := util.GetConjurProvider(c.store)
+	if err != nil {
+		return nil, err
+	}
+
+	cert, getCertErr := c.getCA(ctx, prov)
+	if getCertErr != nil {
+		return nil, getCertErr
+	}
+
+	config := conjurapi.Config{
+		ApplianceURL: prov.URL,
+		SSLCert:      cert,
+	}
+
+	if prov.Auth.APIKey != nil {
+		config.Account = prov.Auth.APIKey.Account
+		conjUser, secErr := resolvers.SecretKeyRef(
+			ctx,
+			c.kube,
+			c.StoreKind,
+			c.namespace, prov.Auth.APIKey.UserRef)
+		if secErr != nil {
+			return nil, fmt.Errorf(errBadServiceUser, secErr)
+		}
+		conjAPIKey, secErr := resolvers.SecretKeyRef(
+			ctx,
+			c.kube,
+			c.StoreKind,
+			c.namespace,
+			prov.Auth.APIKey.APIKeyRef)
+		if secErr != nil {
+			return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
+		}
+
+		conjur, newClientFromKeyError := c.clientAPI.NewClientFromKey(config,
+			authn.LoginPair{
+				Login:  conjUser,
+				APIKey: conjAPIKey,
+			},
+		)
+
+		if newClientFromKeyError != nil {
+			return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
+		}
+		c.client = conjur
+		return conjur, nil
+	} else if prov.Auth.Jwt != nil {
+		config.Account = prov.Auth.Jwt.Account
+
+		conjur, clientFromJwtError := c.newClientFromJwt(ctx, config, prov.Auth.Jwt)
+		if clientFromJwtError != nil {
+			return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
+		}
+
+		c.client = conjur
+
+		return conjur, nil
+	} else {
+		// Should not happen because validate func should catch this
+		return nil, fmt.Errorf("no authentication method provided")
+	}
+}
+
+// PushSecret will write a single secret into the provider.
+func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
+	// NOT IMPLEMENTED
+	return nil
+}
+
+func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
+	// NOT IMPLEMENTED
+	return nil
+}
+
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
+// Validate validates the provider.
+func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultReady, nil
+}
+
+// Close closes the provider.
+func (c *Client) Close(_ context.Context) error {
+	return nil
+}
+
+// configMapKeyRef returns the value of a key in a ConfigMap.
+func (c *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
+	configMap := &corev1.ConfigMap{}
+	ref := client.ObjectKey{
+		Namespace: c.namespace,
+		Name:      cmRef.Name,
+	}
+	if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
+		(cmRef.Namespace != nil) {
+		ref.Namespace = *cmRef.Namespace
+	}
+	err := c.kube.Get(ctx, ref, configMap)
+	if err != nil {
+		return "", err
+	}
+
+	keyBytes, ok := configMap.Data[cmRef.Key]
+	if !ok {
+		return "", err
+	}
+
+	valueStr := strings.TrimSpace(keyBytes)
+	return valueStr, nil
+}
+
+// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
+func (c *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
+	if provider.CAProvider != nil {
+		var ca string
+		var err error
+		switch provider.CAProvider.Type {
+		case esv1beta1.CAProviderTypeConfigMap:
+			keySelector := esmeta.SecretKeySelector{
+				Name:      provider.CAProvider.Name,
+				Namespace: provider.CAProvider.Namespace,
+				Key:       provider.CAProvider.Key,
+			}
+			ca, err = c.configMapKeyRef(ctx, &keySelector)
+			if err != nil {
+				return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
+			}
+		case esv1beta1.CAProviderTypeSecret:
+			keySelector := esmeta.SecretKeySelector{
+				Name:      provider.CAProvider.Name,
+				Namespace: provider.CAProvider.Namespace,
+				Key:       provider.CAProvider.Key,
+			}
+			ca, err = resolvers.SecretKeyRef(
+				ctx,
+				c.kube,
+				c.StoreKind,
+				c.namespace,
+				&keySelector)
+			if err != nil {
+				return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
+			}
+		}
+		return ca, nil
+	}
+	certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
+	if decodeErr != nil {
+		return "", fmt.Errorf(errBadCertBundle, decodeErr)
+	}
+	return string(certBytes), nil
+}

+ 223 - 0
pkg/provider/conjur/client_get.go

@@ -0,0 +1,223 @@
+/*
+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 conjur
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
+	"github.com/tidwall/gjson"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/find"
+)
+
+type conjurResource map[string]interface{}
+
+// resourceFilterFunc is a function that filters resources.
+// It takes a resource as input and returns the name of the resource if it should be included.
+// If the resource should not be included, it returns an empty string.
+// If an error occurs, it returns an empty string and the error.
+type resourceFilterFunc func(candidate conjurResource) (name string, err error)
+
+// GetSecret returns a single secret from the provider.
+func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	conjurClient, getConjurClientError := c.GetConjurClient(ctx)
+	if getConjurClientError != nil {
+		return nil, getConjurClientError
+	}
+	secretValue, err := conjurClient.RetrieveSecret(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	// If no property is specified, return the secret value as is
+	if ref.Property == "" {
+		return secretValue, nil
+	}
+
+	// If a property is specified, parse the secret value as JSON and return the property value
+	val := gjson.Get(string(secretValue), ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
+	}
+	return []byte(val.String()), nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	// Gets a secret as normal, expecting secret value to be a json object
+	data, err := c.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
+	}
+
+	// Maps the json data to a string:string map
+	kv := make(map[string]string)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
+	}
+
+	// Converts values in K:V pairs into bytes, while leaving keys as strings
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}
+
+// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
+// First load all secrets from secretStore path configuration
+// Then, gets secrets from a matching name or matching custom_metadata.
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	if ref.Name != nil {
+		return c.findSecretsFromName(ctx, *ref.Name)
+	}
+	return c.findSecretsFromTags(ctx, ref.Tags)
+}
+
+func (c *Client) findSecretsFromName(ctx context.Context, ref esv1beta1.FindName) (map[string][]byte, error) {
+	matcher, err := find.New(ref)
+	if err != nil {
+		return nil, err
+	}
+
+	var resourceFilterFunc = func(candidate conjurResource) (string, error) {
+		name := trimConjurResourceName(candidate["id"].(string))
+		isMatch := matcher.MatchName(name)
+		if !isMatch {
+			return "", nil
+		}
+		return name, nil
+	}
+
+	return c.listSecrets(ctx, resourceFilterFunc)
+}
+
+func (c *Client) findSecretsFromTags(ctx context.Context, tags map[string]string) (map[string][]byte, error) {
+	var resourceFilterFunc = func(candidate conjurResource) (string, error) {
+		name := trimConjurResourceName(candidate["id"].(string))
+		annotations, ok := candidate["annotations"].([]interface{})
+		if !ok {
+			// No annotations, skip
+			return "", nil
+		}
+
+		formattedAnnotations, err := formatAnnotations(annotations)
+		if err != nil {
+			return "", err
+		}
+
+		// Check if all tags match
+		for tk, tv := range tags {
+			p, ok := formattedAnnotations[tk]
+			if !ok || p != tv {
+				return "", nil
+			}
+		}
+
+		return name, nil
+	}
+
+	return c.listSecrets(ctx, resourceFilterFunc)
+}
+
+func (c *Client) listSecrets(ctx context.Context, filterFunc resourceFilterFunc) (map[string][]byte, error) {
+	conjurClient, getConjurClientError := c.GetConjurClient(ctx)
+	if getConjurClientError != nil {
+		return nil, getConjurClientError
+	}
+
+	filteredResourceNames := []string{}
+
+	// Loop through all secrets in the Conjur account.
+	// Ideally this will be only a small list, but we need to handle pagination in the
+	// case that there are a lot of secrets. To limit load on Conjur and memory usage
+	// in ESO, we will only load 100 secrets at a time. We will then filter these secrets,
+	// discarding any that do not match the filterFunc. We will then repeat this process
+	// until we have loaded all secrets.
+	for offset := 0; ; offset += 100 {
+		resFilter := &conjurapi.ResourceFilter{
+			Kind:   "variable",
+			Limit:  100,
+			Offset: offset,
+		}
+		resources, err := conjurClient.Resources(resFilter)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, candidate := range resources {
+			name, err := filterFunc(candidate)
+			if err != nil {
+				return nil, err
+			}
+			if name != "" {
+				filteredResourceNames = append(filteredResourceNames, name)
+			}
+		}
+
+		// If we have less than 100 resources, we reached the last page
+		if len(resources) < 100 {
+			break
+		}
+	}
+
+	filteredResources, err := c.client.RetrieveBatchSecrets(filteredResourceNames)
+	if err != nil {
+		return nil, err
+	}
+
+	// Trim the resource names to just the last part of the ID
+	return trimConjurResourceNames(filteredResources), nil
+}
+
+// trimConjurResourceNames trims the Conjur resource names to the last part of the ID.
+// It iterates over a map of secrets and returns a new map with the trimmed names.
+func trimConjurResourceNames(resources map[string][]byte) map[string][]byte {
+	trimmedResources := make(map[string][]byte)
+	for k, v := range resources {
+		trimmedResources[trimConjurResourceName(k)] = v
+	}
+	return trimmedResources
+}
+
+// trimConjurResourceName trims the Conjur resource name to the last part of the ID.
+// For example, if the ID is "account:variable:secret", the function will return
+// "secret".
+func trimConjurResourceName(id string) string {
+	tokens := strings.SplitN(id, ":", 3)
+	return tokens[len(tokens)-1]
+}
+
+// Convert annotations from objects with "name", "policy", "value" keys (as returned by the Conjur API)
+// to a key/value map for easier comparison in code.
+func formatAnnotations(annotations []interface{}) (map[string]string, error) {
+	formattedAnnotations := make(map[string]string)
+	for _, annot := range annotations {
+		annot, ok := annot.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("could not parse annotation: %v", annot)
+		}
+		name := annot["name"].(string)
+		value := annot["value"].(string)
+		formattedAnnotations[name] = value
+	}
+	return formattedAnnotations, nil
+}

+ 2 - 0
pkg/provider/conjur/conjur_api.go

@@ -29,6 +29,8 @@ import (
 // SecretsClient is an interface for the Conjur client.
 type SecretsClient interface {
 	RetrieveSecret(secret string) (result []byte, err error)
+	RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error)
+	Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error)
 }
 
 // SecretsClientFactory is an interface for creating a Conjur client.

+ 94 - 0
pkg/provider/conjur/fake/fake.go

@@ -16,6 +16,10 @@ package fake
 
 import (
 	"errors"
+	"fmt"
+	"math/rand"
+
+	"github.com/cyberark/conjur-api-go/conjurapi"
 )
 
 type ConjurMockClient struct {
@@ -26,5 +30,95 @@ func (mc *ConjurMockClient) RetrieveSecret(secret string) (result []byte, err er
 		err = errors.New("error")
 		return nil, err
 	}
+	if secret == "json_map" {
+		return []byte(`{"key1":"value1","key2":"value2"}`), nil
+	}
+	if secret == "json_nested" {
+		return []byte(`{"key1":"value1","key2":{"key3":"value3","key4":"value4"}}`), nil
+	}
 	return []byte("secret"), nil
 }
+
+func (mc *ConjurMockClient) RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error) {
+	secrets := make(map[string][]byte)
+	for _, id := range variableIDs {
+		if id == "error" {
+			return nil, errors.New("error")
+		}
+		fullID := fmt.Sprintf("conjur:variable:%s", id)
+		secrets[fullID] = []byte("secret")
+	}
+	return secrets, nil
+}
+
+func (mc *ConjurMockClient) Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error) {
+	policyID := "conjur:policy:root"
+	if filter.Offset == 0 {
+		// First "page" of secrets: 2 static ones and 98 random ones
+		secrets := []map[string]interface{}{
+			{
+				"id": "conjur:variable:secret1",
+				"annotations": []interface{}{
+					map[string]interface{}{
+						"name":  "conjur/kind",
+						"value": "dummy",
+					},
+				},
+			},
+			{
+				"id":    "conjur:variable:secret2",
+				"owner": "conjur:policy:admin1",
+				"annotations": []interface{}{
+					map[string]interface{}{
+						"name":   "Description",
+						"policy": policyID,
+						"value":  "Lorem ipsum dolor sit amet",
+					},
+					map[string]interface{}{
+						"name":   "conjur/kind",
+						"policy": policyID,
+						"value":  "password",
+					},
+				},
+				"permissions": map[string]string{
+					"policy":    policyID,
+					"privilege": "update",
+					"role":      "conjur:group:admins",
+				},
+				"policy": policyID,
+			},
+		}
+		// Add 98 random secrets so we can simulate a full "page" of 100 secrets
+		secrets = append(secrets, generateRandomSecrets(98)...)
+		return secrets, nil
+	} else if filter.Offset == 100 {
+		// Second "page" of secrets: 100 random ones
+		return generateRandomSecrets(100), nil
+	}
+
+	// Add 50 random secrets so we can simulate a partial "page" of 50 secrets
+	return generateRandomSecrets(50), nil
+}
+
+func generateRandomSecrets(count int) []map[string]interface{} {
+	var secrets []map[string]interface{}
+	for i := 0; i < count; i++ {
+		//nolint:gosec
+		randomNumber := rand.Intn(10000)
+		secrets = append(secrets, generateRandomSecret(randomNumber))
+	}
+	return secrets
+}
+
+func generateRandomSecret(num int) map[string]interface{} {
+	return map[string]interface{}{
+		"id": fmt.Sprintf("conjur:variable:random/var_%d", num),
+		"annotations": []map[string]interface{}{
+			{
+				"name":  "random_number",
+				"value": fmt.Sprintf("%d", num),
+			},
+		},
+		"policy": "conjur:policy:random",
+	}
+}

+ 7 - 304
pkg/provider/conjur/provider.go

@@ -17,55 +17,21 @@ package conjur
 
 import (
 	"context"
-	"encoding/json"
-	"fmt"
-	"strings"
 
-	"github.com/cyberark/conjur-api-go/conjurapi"
-	"github.com/cyberark/conjur-api-go/conjurapi/authn"
-	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/kubernetes"
 	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
-	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	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/conjur/util"
-	"github.com/external-secrets/external-secrets/pkg/utils"
-	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 )
 
-var (
-	errConjurClient     = "cannot setup new Conjur client: %w"
-	errBadCertBundle    = "caBundle failed to base64 decode: %w"
-	errBadServiceUser   = "could not get Auth.Apikey.UserRef: %w"
-	errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
-
-	errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
-
-	errUnableToFetchCAProviderCM     = "unable to fetch Server.CAProvider ConfigMap: %w"
-	errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
-)
-
-// Client is a provider for Conjur.
-type Client struct {
-	StoreKind string
-	kube      client.Client
-	store     esv1beta1.GenericStore
-	namespace string
-	corev1    typedcorev1.CoreV1Interface
-	clientAPI SecretsClientFactory
-	client    SecretsClient
-}
-
 type Provider struct {
 	NewConjurProvider func(context context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientApi SecretsClientFactory) (esv1beta1.SecretsClient, error)
 }
 
 // NewClient creates a new Conjur client.
-func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// so we need to construct our own client and use it to create a TokenRequest
 	restCfg, err := ctrlcfg.GetConfig()
@@ -77,7 +43,12 @@ func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore,
 		return nil, err
 	}
 
-	return c.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
+	return p.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
+}
+
+// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadOnly
 }
 
 func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientAPI SecretsClientFactory) (esv1beta1.SecretsClient, error) {
@@ -91,274 +62,6 @@ func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube cli
 	}, nil
 }
 
-func (p *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
-	// if the client is initialized already, return it
-	if p.client != nil {
-		return p.client, nil
-	}
-
-	prov, err := util.GetConjurProvider(p.store)
-	if err != nil {
-		return nil, err
-	}
-
-	cert, getCertErr := p.getCA(ctx, prov)
-	if getCertErr != nil {
-		return nil, getCertErr
-	}
-
-	config := conjurapi.Config{
-		ApplianceURL: prov.URL,
-		SSLCert:      cert,
-	}
-
-	if prov.Auth.APIKey != nil {
-		config.Account = prov.Auth.APIKey.Account
-		conjUser, secErr := resolvers.SecretKeyRef(
-			ctx,
-			p.kube,
-			p.StoreKind,
-			p.namespace, prov.Auth.APIKey.UserRef)
-		if secErr != nil {
-			return nil, fmt.Errorf(errBadServiceUser, secErr)
-		}
-		conjAPIKey, secErr := resolvers.SecretKeyRef(
-			ctx,
-			p.kube,
-			p.StoreKind,
-			p.namespace,
-			prov.Auth.APIKey.APIKeyRef)
-		if secErr != nil {
-			return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
-		}
-
-		conjur, newClientFromKeyError := p.clientAPI.NewClientFromKey(config,
-			authn.LoginPair{
-				Login:  conjUser,
-				APIKey: conjAPIKey,
-			},
-		)
-
-		if newClientFromKeyError != nil {
-			return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
-		}
-		p.client = conjur
-		return conjur, nil
-	} else if prov.Auth.Jwt != nil {
-		config.Account = prov.Auth.Jwt.Account
-
-		conjur, clientFromJwtError := p.newClientFromJwt(ctx, config, prov.Auth.Jwt)
-		if clientFromJwtError != nil {
-			return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
-		}
-
-		p.client = conjur
-
-		return conjur, nil
-	} else {
-		// Should not happen because validate func should catch this
-		return nil, fmt.Errorf("no authentication method provided")
-	}
-}
-
-// GetAllSecrets returns all secrets from the provider.
-// NOT IMPLEMENTED.
-func (p *Client) GetAllSecrets(_ context.Context, _ 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 (p *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	conjurClient, getConjurClientError := p.GetConjurClient(ctx)
-	if getConjurClientError != nil {
-		return nil, getConjurClientError
-	}
-	secretValue, err := conjurClient.RetrieveSecret(ref.Key)
-	if err != nil {
-		return nil, err
-	}
-
-	return secretValue, nil
-}
-
-// PushSecret will write a single secret into the provider.
-func (p *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	// NOT IMPLEMENTED
-	return nil
-}
-
-func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	// NOT IMPLEMENTED
-	return nil
-}
-
-func (p *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
-	return false, fmt.Errorf("not implemented")
-}
-
-// GetSecretMap returns multiple k/v pairs from the provider.
-func (p *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	// Gets a secret as normal, expecting secret value to be a json object
-	data, err := p.GetSecret(ctx, ref)
-	if err != nil {
-		return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
-	}
-
-	// Maps the json data to a string:string map
-	kv := make(map[string]string)
-	err = json.Unmarshal(data, &kv)
-	if err != nil {
-		return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
-	}
-
-	// Converts values in K:V pairs into bytes, while leaving keys as strings
-	secretData := make(map[string][]byte)
-	for k, v := range kv {
-		secretData[k] = []byte(v)
-	}
-	return secretData, nil
-}
-
-// Close closes the provider.
-func (p *Client) Close(_ context.Context) error {
-	return nil
-}
-
-// Validate validates the provider.
-func (p *Client) Validate() (esv1beta1.ValidationResult, error) {
-	return esv1beta1.ValidationResultReady, nil
-}
-
-// ValidateStore validates the store.
-func (c *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
-	prov, err := util.GetConjurProvider(store)
-	if err != nil {
-		return nil, err
-	}
-
-	if prov.URL == "" {
-		return nil, fmt.Errorf("conjur URL cannot be empty")
-	}
-	if prov.Auth.APIKey != nil {
-		if prov.Auth.APIKey.Account == "" {
-			return nil, fmt.Errorf("missing Auth.ApiKey.Account")
-		}
-		if prov.Auth.APIKey.UserRef == nil {
-			return nil, fmt.Errorf("missing Auth.Apikey.UserRef")
-		}
-		if prov.Auth.APIKey.APIKeyRef == nil {
-			return nil, fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
-		}
-		if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.UserRef); err != nil {
-			return nil, fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
-		}
-		if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.APIKeyRef); err != nil {
-			return nil, fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
-		}
-	}
-
-	if prov.Auth.Jwt != nil {
-		if prov.Auth.Jwt.Account == "" {
-			return nil, fmt.Errorf("missing Auth.Jwt.Account")
-		}
-		if prov.Auth.Jwt.ServiceID == "" {
-			return nil, fmt.Errorf("missing Auth.Jwt.ServiceID")
-		}
-		if prov.Auth.Jwt.ServiceAccountRef == nil && prov.Auth.Jwt.SecretRef == nil {
-			return nil, fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
-		}
-		if prov.Auth.Jwt.SecretRef != nil {
-			if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.Jwt.SecretRef); err != nil {
-				return nil, fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
-			}
-		}
-		if prov.Auth.Jwt.ServiceAccountRef != nil {
-			if err := utils.ValidateReferentServiceAccountSelector(store, *prov.Auth.Jwt.ServiceAccountRef); err != nil {
-				return nil, fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
-			}
-		}
-	}
-
-	// At least one auth must be configured
-	if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
-		return nil, fmt.Errorf("missing Auth.* configuration")
-	}
-
-	return nil, nil
-}
-
-// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
-func (c *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
-	return esv1beta1.SecretStoreReadOnly
-}
-
-// configMapKeyRef returns the value of a key in a ConfigMap.
-func (p *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
-	configMap := &corev1.ConfigMap{}
-	ref := client.ObjectKey{
-		Namespace: p.namespace,
-		Name:      cmRef.Name,
-	}
-	if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
-		(cmRef.Namespace != nil) {
-		ref.Namespace = *cmRef.Namespace
-	}
-	err := p.kube.Get(ctx, ref, configMap)
-	if err != nil {
-		return "", err
-	}
-
-	keyBytes, ok := configMap.Data[cmRef.Key]
-	if !ok {
-		return "", err
-	}
-
-	valueStr := strings.TrimSpace(keyBytes)
-	return valueStr, nil
-}
-
-// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
-func (p *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
-	if provider.CAProvider != nil {
-		var ca string
-		var err error
-		switch provider.CAProvider.Type {
-		case esv1beta1.CAProviderTypeConfigMap:
-			keySelector := esmeta.SecretKeySelector{
-				Name:      provider.CAProvider.Name,
-				Namespace: provider.CAProvider.Namespace,
-				Key:       provider.CAProvider.Key,
-			}
-			ca, err = p.configMapKeyRef(ctx, &keySelector)
-			if err != nil {
-				return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
-			}
-		case esv1beta1.CAProviderTypeSecret:
-			keySelector := esmeta.SecretKeySelector{
-				Name:      provider.CAProvider.Name,
-				Namespace: provider.CAProvider.Namespace,
-				Key:       provider.CAProvider.Key,
-			}
-			ca, err = resolvers.SecretKeyRef(
-				ctx,
-				p.kube,
-				p.StoreKind,
-				p.namespace,
-				&keySelector)
-			if err != nil {
-				return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
-			}
-		}
-		return ca, nil
-	}
-	certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
-	if decodeErr != nil {
-		return "", fmt.Errorf(errBadCertBundle, decodeErr)
-	}
-	return string(certBytes), nil
-}
-
 func init() {
 	esv1beta1.Register(&Provider{
 		NewConjurProvider: newConjurProvider,

+ 242 - 67
pkg/provider/conjur/provider_test.go

@@ -55,75 +55,17 @@ func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
 	}
 }
 
-type ValidateStoreTestCase struct {
-	store *esv1beta1.SecretStore
-	err   error
-}
-
-func TestValidateStore(t *testing.T) {
-	testCases := []ValidateStoreTestCase{
-		{
-			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
-			err:   nil,
-		},
-		{
-			store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
-			err:   fmt.Errorf("conjur URL cannot be empty"),
-		},
-		{
-			store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
-			err:   fmt.Errorf("missing Auth.Apikey.UserRef"),
-		},
-		{
-			store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
-			err:   fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
-		},
-		{
-			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
-			err:   fmt.Errorf("missing Auth.ApiKey.Account"),
-		},
-
-		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
-			err:   nil,
-		},
-		{
-			store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
-			err:   nil,
-		},
-		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
-			err:   fmt.Errorf("missing Auth.Jwt.Account"),
-		},
-		{
-			store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
-			err:   fmt.Errorf("missing Auth.Jwt.ServiceID"),
-		},
-		{
-			store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
-			err:   fmt.Errorf("conjur URL cannot be empty"),
-		},
-		{
-			store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
-			err:   fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
-		},
-
-		{
-			store: makeNoAuthSecretStore(svcURL),
-			err:   fmt.Errorf("missing Auth.* configuration"),
-		},
-	}
-	c := Provider{}
-	for _, tc := range testCases {
-		_, err := c.ValidateStore(tc.store)
-		if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
-			t.Errorf("test failed! want %v, got %v", tc.err, err)
-		} else if tc.err == nil && err != nil {
-			t.Errorf("want nil got err %v", err)
-		} else if tc.err != nil && err == nil {
-			t.Errorf("want err %v got nil", tc.err)
+func makeValidFindRef(search string, tags map[string]string) *esv1beta1.ExternalSecretFind {
+	var name *esv1beta1.FindName
+	if search != "" {
+		name = &esv1beta1.FindName{
+			RegExp: search,
 		}
 	}
+	return &esv1beta1.ExternalSecretFind{
+		Name: name,
+		Tags: tags,
+	}
 }
 
 func TestGetSecret(t *testing.T) {
@@ -264,6 +206,239 @@ func TestGetSecret(t *testing.T) {
 	}
 }
 
+func TestGetAllSecrets(t *testing.T) {
+	type args struct {
+		store     esv1beta1.GenericStore
+		kube      kclient.Client
+		corev1    typedcorev1.CoreV1Interface
+		namespace string
+		search    string
+		tags      map[string]string
+	}
+
+	type want struct {
+		err    error
+		values map[string][]byte
+	}
+
+	type testCase struct {
+		reason string
+		args   args
+		want   want
+	}
+
+	cases := map[string]testCase{
+		"SimpleSearchSingleResultSuccess": {
+			reason: "Should search for secrets successfully using a simple string.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				search:    "secret1",
+			},
+			want: want{
+				err: nil,
+				values: map[string][]byte{
+					"secret1": []byte("secret"),
+				},
+			},
+		},
+		"RegexSearchMultipleResultsSuccess": {
+			reason: "Should search for secrets successfully using a regex and return multiple results.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				search:    "^secret[1,2]$",
+			},
+			want: want{
+				err: nil,
+				values: map[string][]byte{
+					"secret1": []byte("secret"),
+					"secret2": []byte("secret"),
+				},
+			},
+		},
+		"RegexSearchInvalidRegexFailure": {
+			reason: "Should fail to search for secrets using an invalid regex.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				search:    "^secret[1,2", // Missing `]`
+			},
+			want: want{
+				err:    fmt.Errorf("could not compile find.name.regexp [%s]: %w", "^secret[1,2", fmt.Errorf("error parsing regexp: missing closing ]: `[1,2`")),
+				values: nil,
+			},
+		},
+		"SimpleSearchNoResultsSuccess": {
+			reason: "Should search for secrets successfully using a simple string and return no results.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				search:    "nonexistent",
+			},
+			want: want{
+				err:    nil,
+				values: map[string][]byte{},
+			},
+		},
+		"TagSearchSingleResultSuccess": {
+			reason: "Should search for secrets successfully using a tag.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				tags: map[string]string{
+					"conjur/kind": "password",
+				},
+			},
+			want: want{
+				err: nil,
+				values: map[string][]byte{
+					"secret2": []byte("secret"),
+				},
+			},
+		},
+	}
+
+	runTest := func(t *testing.T, _ string, tc testCase) {
+		provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+		ref := makeValidFindRef(tc.args.search, tc.args.tags)
+		secrets, err := provider.GetAllSecrets(context.Background(), *ref)
+		if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+			t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want error, +got error:\n%s", tc.reason, diff)
+		}
+		if diff := cmp.Diff(tc.want.values, secrets); diff != "" {
+			t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want, +got:\n%s", tc.reason, diff)
+		}
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			runTest(t, name, tc)
+		})
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	type args struct {
+		store     esv1beta1.GenericStore
+		kube      kclient.Client
+		corev1    typedcorev1.CoreV1Interface
+		namespace string
+		ref       *esv1beta1.ExternalSecretDataRemoteRef
+	}
+
+	type want struct {
+		err error
+		val map[string][]byte
+	}
+
+	type testCase struct {
+		reason string
+		args   args
+		want   want
+	}
+
+	cases := map[string]testCase{
+		"ReadJsonSecret": {
+			reason: "Should read a JSON key value secret.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				ref:       makeValidRef("json_map"),
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"key1": []byte("value1"),
+					"key2": []byte("value2"),
+				},
+			},
+		},
+		"ReadJsonSecretFailure": {
+			reason: "Should fail to read a non JSON secret",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				ref:       makeValidRef("secret1"),
+			},
+			want: want{
+				err: fmt.Errorf("%w", fmt.Errorf("unable to unmarshal secret secret1: invalid character 's' looking for beginning of value")),
+				val: nil,
+			},
+		},
+		"ReadJsonSecretSpecificKey": {
+			reason: "Should read a specific key from a JSON secret.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				ref: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      "json_nested",
+					Version:  "default",
+					Property: "key2",
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"key3": []byte("value3"),
+					"key4": []byte("value4"),
+				},
+			},
+		},
+		"ReadJsonSecretSpecificKeyNotFound": {
+			reason: "Should fail to read a nonexistent key from a JSON secret.",
+			args: args{
+				store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
+				kube: clientfake.NewClientBuilder().
+					WithObjects(makeFakeAPIKeySecrets()...).Build(),
+				namespace: "default",
+				ref: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      "json_map",
+					Version:  "default",
+					Property: "key3",
+				},
+			},
+			want: want{
+				err: fmt.Errorf("%w", fmt.Errorf("error getting secret json_map: cannot find secret data for key: \"key3\"")),
+				val: nil,
+			},
+		},
+	}
+
+	runTest := func(t *testing.T, _ string, tc testCase) {
+		provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
+		val, err := provider.GetSecretMap(context.Background(), *tc.args.ref)
+		if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+			t.Errorf("\n%s\nconjur.GetSecretMap(...): -want error, +got error:\n%s", tc.reason, diff)
+		}
+		if diff := cmp.Diff(tc.want.val, val); diff != "" {
+			t.Errorf("\n%s\nconjur.GetSecretMap(...): -want val, +got val:\n%s", tc.reason, diff)
+		}
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			runTest(t, name, tc)
+		})
+	}
+}
+
 func TestGetCA(t *testing.T) {
 	type args struct {
 		store     esv1beta1.GenericStore

+ 100 - 0
pkg/provider/conjur/validate.go

@@ -0,0 +1,100 @@
+/*
+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 conjur provides a Conjur provider for External Secrets.
+package conjur
+
+import (
+	"fmt"
+
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/conjur/util"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+// ValidateStore validates the store.
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	prov, err := util.GetConjurProvider(store)
+	if err != nil {
+		return nil, err
+	}
+
+	if prov.URL == "" {
+		return nil, fmt.Errorf("conjur URL cannot be empty")
+	}
+	if prov.Auth.APIKey != nil {
+		err := validateAPIKeyStore(store, *prov.Auth.APIKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if prov.Auth.Jwt != nil {
+		err := validateJWTStore(store, *prov.Auth.Jwt)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// At least one auth must be configured
+	if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
+		return nil, fmt.Errorf("missing Auth.* configuration")
+	}
+
+	return nil, nil
+}
+
+func validateAPIKeyStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurAPIKey) error {
+	if auth.Account == "" {
+		return fmt.Errorf("missing Auth.ApiKey.Account")
+	}
+	if auth.UserRef == nil {
+		return fmt.Errorf("missing Auth.Apikey.UserRef")
+	}
+	if auth.APIKeyRef == nil {
+		return fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
+	}
+	if err := utils.ValidateReferentSecretSelector(store, *auth.UserRef); err != nil {
+		return fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
+	}
+	if err := utils.ValidateReferentSecretSelector(store, *auth.APIKeyRef); err != nil {
+		return fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
+	}
+	return nil
+}
+
+func validateJWTStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurJWT) error {
+	if auth.Account == "" {
+		return fmt.Errorf("missing Auth.Jwt.Account")
+	}
+	if auth.ServiceID == "" {
+		return fmt.Errorf("missing Auth.Jwt.ServiceID")
+	}
+	if auth.ServiceAccountRef == nil && auth.SecretRef == nil {
+		return fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
+	}
+	if auth.SecretRef != nil {
+		if err := utils.ValidateReferentSecretSelector(store, *auth.SecretRef); err != nil {
+			return fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
+		}
+	}
+	if auth.ServiceAccountRef != nil {
+		if err := utils.ValidateReferentServiceAccountSelector(store, *auth.ServiceAccountRef); err != nil {
+			return fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
+		}
+	}
+	return nil
+}

+ 93 - 0
pkg/provider/conjur/validate_test.go

@@ -0,0 +1,93 @@
+/*
+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 conjur
+
+import (
+	"fmt"
+	"testing"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+type ValidateStoreTestCase struct {
+	store *esv1beta1.SecretStore
+	err   error
+}
+
+func TestValidateStore(t *testing.T) {
+	testCases := []ValidateStoreTestCase{
+		{
+			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
+			err:   nil,
+		},
+		{
+			store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
+			err:   fmt.Errorf("conjur URL cannot be empty"),
+		},
+		{
+			store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
+			err:   fmt.Errorf("missing Auth.Apikey.UserRef"),
+		},
+		{
+			store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
+			err:   fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
+		},
+		{
+			store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
+			err:   fmt.Errorf("missing Auth.ApiKey.Account"),
+		},
+
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
+			err:   nil,
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
+			err:   nil,
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
+			err:   fmt.Errorf("missing Auth.Jwt.Account"),
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
+			err:   fmt.Errorf("missing Auth.Jwt.ServiceID"),
+		},
+		{
+			store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
+			err:   fmt.Errorf("conjur URL cannot be empty"),
+		},
+		{
+			store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
+			err:   fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
+		},
+
+		{
+			store: makeNoAuthSecretStore(svcURL),
+			err:   fmt.Errorf("missing Auth.* configuration"),
+		},
+	}
+	p := Provider{}
+	for _, tc := range testCases {
+		_, err := p.ValidateStore(tc.store)
+		if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
+			t.Errorf("test failed! want %v, got %v", tc.err, err)
+		} else if tc.err == nil && err != nil {
+			t.Errorf("want nil got err %v", err)
+		} else if tc.err != nil && err == nil {
+			t.Errorf("want err %v got nil", tc.err)
+		}
+	}
+}