Browse Source

:broom: refactor vault provider (#3072)

* chore: split monolith into separate files

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* chore: add tests

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* chore: rename vault/auth_iam vars

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* fixup: remove string duplication

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

---------

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 years ago
parent
commit
d246c2e082

+ 2 - 2
pkg/generator/vault/vault.go

@@ -42,7 +42,7 @@ const (
 )
 )
 
 
 func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, error) {
 func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, error) {
-	c := &provider.Connector{NewVaultClient: provider.NewVaultClient}
+	c := &provider.Provider{NewVaultClient: provider.NewVaultClient}
 
 
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// controller-runtime/client does not support TokenRequest or other subresource APIs
 	// so we need to construct our own client and use it to fetch tokens
 	// so we need to construct our own client and use it to fetch tokens
@@ -59,7 +59,7 @@ func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON,
 	return g.generate(ctx, c, jsonSpec, kube, clientset.CoreV1(), namespace)
 	return g.generate(ctx, c, jsonSpec, kube, clientset.CoreV1(), namespace)
 }
 }
 
 
-func (g *Generator) generate(ctx context.Context, c *provider.Connector, jsonSpec *apiextensions.JSON, kube client.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (map[string][]byte, error) {
+func (g *Generator) generate(ctx context.Context, c *provider.Provider, jsonSpec *apiextensions.JSON, kube client.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (map[string][]byte, error) {
 	if jsonSpec == nil {
 	if jsonSpec == nil {
 		return nil, fmt.Errorf(errNoSpec)
 		return nil, fmt.Errorf(errNoSpec)
 	}
 	}

+ 1 - 1
pkg/generator/vault/vault_test.go

@@ -166,7 +166,7 @@ spec:
 
 
 	for name, tc := range cases {
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
-			c := &provider.Connector{NewVaultClient: fake.ClientWithLoginMock}
+			c := &provider.Provider{NewVaultClient: fake.ClientWithLoginMock}
 			gen := &Generator{}
 			gen := &Generator{}
 			val, err := gen.generate(context.Background(), c, tc.args.jsonSpec, tc.args.kube, tc.args.corev1, "testing")
 			val, err := gen.generate(context.Background(), c, tc.args.jsonSpec, tc.args.kube, tc.args.corev1, "testing")
 			if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
 			if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {

+ 171 - 0
pkg/provider/vault/auth.go

@@ -0,0 +1,171 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	vault "github.com/hashicorp/vault/api"
+	authv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+
+	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/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	vaultiamauth "github.com/external-secrets/external-secrets/pkg/provider/vault/iamauth"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+)
+
+const (
+	errAuthFormat            = "cannot initialize Vault client: no valid auth method specified"
+	errVaultToken            = "cannot parse Vault authentication token: %w"
+	errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
+	errVaultRevokeToken      = "error while revoking token: %w"
+)
+
+// setAuth gets a new token using the configured mechanism.
+// If there's already a valid token, does nothing.
+func (c *client) setAuth(ctx context.Context, cfg *vault.Config) error {
+	tokenExists := false
+	var err error
+	if c.client.Token() != "" {
+		tokenExists, err = checkToken(ctx, c.token)
+	}
+	if tokenExists {
+		c.log.V(1).Info("Re-using existing token")
+		return err
+	}
+
+	tokenExists, err = setSecretKeyToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Set token from secret")
+		return err
+	}
+
+	tokenExists, err = setAppRoleToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using AppRole auth")
+		return err
+	}
+
+	tokenExists, err = setKubernetesAuthToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using Kubernetes auth")
+		return err
+	}
+
+	tokenExists, err = setLdapAuthToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using LDAP auth")
+		return err
+	}
+
+	tokenExists, err = setUserPassAuthToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using userPass auth")
+		return err
+	}
+	tokenExists, err = setJwtAuthToken(ctx, c)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using JWT auth")
+		return err
+	}
+
+	tokenExists, err = setCertAuthToken(ctx, c, cfg)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using certificate auth")
+		return err
+	}
+
+	tokenExists, err = setIamAuthToken(ctx, c, vaultiamauth.DefaultJWTProvider, vaultiamauth.DefaultSTSProvider)
+	if tokenExists {
+		c.log.V(1).Info("Retrieved new token using IAM auth")
+		return err
+	}
+
+	return errors.New(errAuthFormat)
+}
+
+func createServiceAccountToken(
+	ctx context.Context,
+	corev1Client typedcorev1.CoreV1Interface,
+	storeKind string,
+	namespace string,
+	serviceAccountRef esmeta.ServiceAccountSelector,
+	additionalAud []string,
+	expirationSeconds int64) (string, error) {
+	audiences := serviceAccountRef.Audiences
+	if len(additionalAud) > 0 {
+		audiences = append(audiences, additionalAud...)
+	}
+	tokenRequest := &authv1.TokenRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: authv1.TokenRequestSpec{
+			Audiences:         audiences,
+			ExpirationSeconds: &expirationSeconds,
+		},
+	}
+	if (storeKind == esv1beta1.ClusterSecretStoreKind) &&
+		(serviceAccountRef.Namespace != nil) {
+		tokenRequest.Namespace = *serviceAccountRef.Namespace
+	}
+	tokenResponse, err := corev1Client.ServiceAccounts(tokenRequest.Namespace).
+		CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
+	}
+	return tokenResponse.Status.Token, nil
+}
+
+// checkToken does a lookup and checks if the provided token exists.
+func checkToken(ctx context.Context, token util.Token) (bool, error) {
+	// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token-self
+	resp, err := token.LookupSelfWithContext(ctx)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLookupSelf, err)
+	if err != nil {
+		return false, err
+	}
+	t, ok := resp.Data["type"]
+	if !ok {
+		return false, fmt.Errorf("could not assert token type")
+	}
+	tokenType := t.(string)
+	if tokenType == "batch" {
+		return false, nil
+	}
+	return true, nil
+}
+
+func revokeTokenIfValid(ctx context.Context, client util.Client) error {
+	valid, err := checkToken(ctx, client.AuthToken())
+	if err != nil {
+		return fmt.Errorf(errVaultRevokeToken, err)
+	}
+	if valid {
+		err = client.AuthToken().RevokeSelfWithContext(ctx, client.Token())
+		metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultRevokeSelf, err)
+		if err != nil {
+			return fmt.Errorf(errVaultRevokeToken, err)
+		}
+		client.ClearToken()
+	}
+	return nil
+}

+ 77 - 0
pkg/provider/vault/auth_approle.go

@@ -0,0 +1,77 @@
+/*
+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 vault
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	approle "github.com/hashicorp/vault/api/auth/approle"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+const (
+	errInvalidAppRoleID = "invalid Auth.AppRole: neither `roleId` nor `roleRef` was supplied"
+)
+
+func setAppRoleToken(ctx context.Context, v *client) (bool, error) {
+	appRole := v.store.Auth.AppRole
+	if appRole != nil {
+		err := v.requestTokenWithAppRoleRef(ctx, appRole)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithAppRoleRef(ctx context.Context, appRole *esv1beta1.VaultAppRole) error {
+	var err error
+	var roleID string // becomes the RoleID used to authenticate with HashiCorp Vault
+
+	// prefer .auth.appRole.roleId, fallback to .auth.appRole.roleRef, give up after that.
+	if appRole.RoleID != "" { // use roleId from CRD, if configured
+		roleID = strings.TrimSpace(appRole.RoleID)
+	} else if appRole.RoleRef != nil { // use RoleID from Secret, if configured
+		roleID, err = resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, appRole.RoleRef)
+		if err != nil {
+			return err
+		}
+	} else { // we ran out of ways to get RoleID. return an appropriate error
+		return fmt.Errorf(errInvalidAppRoleID)
+	}
+
+	secretID, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &appRole.SecretRef)
+	if err != nil {
+		return err
+	}
+	secret := approle.SecretID{FromString: secretID}
+	appRoleClient, err := approle.NewAppRoleAuth(roleID, &secret, approle.WithMountPath(appRole.Path))
+	if err != nil {
+		return err
+	}
+	_, err = c.auth.Login(ctx, appRoleClient)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 80 - 0
pkg/provider/vault/auth_cert.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 vault
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"strings"
+
+	vault "github.com/hashicorp/vault/api"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+const (
+	errVaultRequest = "error from Vault request: %w"
+)
+
+func setCertAuthToken(ctx context.Context, v *client, cfg *vault.Config) (bool, error) {
+	certAuth := v.store.Auth.Cert
+	if certAuth != nil {
+		err := v.requestTokenWithCertAuth(ctx, certAuth, cfg)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithCertAuth(ctx context.Context, certAuth *esv1beta1.VaultCertAuth, cfg *vault.Config) error {
+	clientKey, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &certAuth.SecretRef)
+	if err != nil {
+		return err
+	}
+
+	clientCert, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &certAuth.ClientCert)
+	if err != nil {
+		return err
+	}
+
+	cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
+	if err != nil {
+		return fmt.Errorf(errClientTLSAuth, err)
+	}
+
+	if transport, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
+		transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+	}
+
+	url := strings.Join([]string{"auth", "cert", "login"}, "/")
+	vaultResult, err := c.logical.WriteWithContext(ctx, url, nil)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
+	if err != nil {
+		return fmt.Errorf(errVaultRequest, err)
+	}
+	token, err := vaultResult.TokenID()
+	if err != nil {
+		return fmt.Errorf(errVaultToken, err)
+	}
+	c.client.SetToken(token)
+	return nil
+}

+ 184 - 0
pkg/provider/vault/auth_iam.go

@@ -0,0 +1,184 @@
+/*
+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 vault
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
+	"github.com/golang-jwt/jwt/v5"
+	authaws "github.com/hashicorp/vault/api/auth/aws"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	vaultiamauth "github.com/external-secrets/external-secrets/pkg/provider/vault/iamauth"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+)
+
+const (
+	defaultAWSRegion                = "us-east-1"
+	defaultAWSAuthMountPath         = "aws"
+	errIrsaTokenEnvVarNotFoundOnPod = "expected env variable: %s not found on controller's pod"
+	errIrsaTokenFileNotFoundOnPod   = "web ddentity token file not found at %s location: %w"
+	errIrsaTokenFileNotReadable     = "could not read the web identity token from the file %s: %w"
+	errIrsaTokenNotValidJWT         = "could not parse web identity token available at %s. not a valid jwt?: %w"
+	errPodInfoNotFoundOnToken       = "could not find pod identity info on token %s: %w"
+)
+
+func setIamAuthToken(ctx context.Context, v *client, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) (bool, error) {
+	iamAuth := v.store.Auth.Iam
+	isClusterKind := v.storeKind == esv1beta1.ClusterSecretStoreKind
+	if iamAuth != nil {
+		err := v.requestTokenWithIamAuth(ctx, iamAuth, isClusterKind, v.kube, v.namespace, jwtProvider, assumeRoler)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithIamAuth(ctx context.Context, iamAuth *esv1beta1.VaultIamAuth, isClusterKind bool, k kclient.Client, n string, jwtProvider util.JwtProviderFactory, assumeRoler vaultiamauth.STSProvider) error {
+	jwtAuth := iamAuth.JWTAuth
+	secretRefAuth := iamAuth.SecretRef
+	regionAWS := defaultAWSRegion
+	awsAuthMountPath := defaultAWSAuthMountPath
+	if iamAuth.Region != "" {
+		regionAWS = iamAuth.Region
+	}
+	if iamAuth.Path != "" {
+		awsAuthMountPath = iamAuth.Path
+	}
+	var creds *credentials.Credentials
+	var err error
+	if jwtAuth != nil { // use credentials from a sa explicitly defined and referenced. Highest preference is given to this method/configuration.
+		creds, err = vaultiamauth.CredsFromServiceAccount(ctx, *iamAuth, regionAWS, isClusterKind, k, n, jwtProvider)
+		if err != nil {
+			return err
+		}
+	} else if secretRefAuth != nil { // if jwtAuth is not defined, check if secretRef is defined. Second preference.
+		logger.V(1).Info("using credentials from secretRef")
+		creds, err = vaultiamauth.CredsFromSecretRef(ctx, *iamAuth, c.storeKind, k, n)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Neither of jwtAuth or secretRefAuth defined. Last preference.
+	// Default to controller pod's identity
+	if jwtAuth == nil && secretRefAuth == nil {
+		// Checking if controller pod's service account is IRSA enabled and Web Identity token is available on pod
+		tokenFile, ok := os.LookupEnv(vaultiamauth.AWSWebIdentityTokenFileEnvVar)
+		if !ok {
+			return fmt.Errorf(errIrsaTokenEnvVarNotFoundOnPod, vaultiamauth.AWSWebIdentityTokenFileEnvVar) // No Web Identity(IRSA) token found on pod
+		}
+
+		// IRSA enabled service account, let's check that the jwt token filemount and file exists
+		if _, err := os.Stat(tokenFile); err != nil {
+			return fmt.Errorf(errIrsaTokenFileNotFoundOnPod, tokenFile, err)
+		}
+
+		// everything looks good so far, let's fetch the jwt token from AWS_WEB_IDENTITY_TOKEN_FILE
+		jwtByte, err := os.ReadFile(tokenFile)
+		if err != nil {
+			return fmt.Errorf(errIrsaTokenFileNotReadable, tokenFile, err)
+		}
+
+		// let's parse the jwt token
+		parser := jwt.NewParser(jwt.WithoutClaimsValidation())
+
+		token, _, err := parser.ParseUnverified(string(jwtByte), jwt.MapClaims{})
+		if err != nil {
+			return fmt.Errorf(errIrsaTokenNotValidJWT, tokenFile, err) // JWT token parser error
+		}
+
+		var ns string
+		var sa string
+
+		// let's fetch the namespace and serviceaccount from parsed jwt token
+		if claims, ok := token.Claims.(jwt.MapClaims); ok {
+			ns = claims["kubernetes.io"].(map[string]interface{})["namespace"].(string)
+			sa = claims["kubernetes.io"].(map[string]interface{})["serviceaccount"].(map[string]interface{})["name"].(string)
+		} else {
+			return fmt.Errorf(errPodInfoNotFoundOnToken, tokenFile, err)
+		}
+
+		creds, err = vaultiamauth.CredsFromControllerServiceAccount(ctx, sa, ns, regionAWS, k, jwtProvider)
+		if err != nil {
+			return err
+		}
+	}
+
+	config := aws.NewConfig().WithEndpointResolver(vaultiamauth.ResolveEndpoint())
+	if creds != nil {
+		config.WithCredentials(creds)
+	}
+
+	if regionAWS != "" {
+		config.WithRegion(regionAWS)
+	}
+
+	sess, err := vaultiamauth.GetAWSSession(config)
+	if err != nil {
+		return err
+	}
+	if iamAuth.AWSIAMRole != "" {
+		stsclient := assumeRoler(sess)
+		if iamAuth.ExternalID != "" {
+			var setExternalID = func(p *stscreds.AssumeRoleProvider) {
+				p.ExternalID = aws.String(iamAuth.ExternalID)
+			}
+			sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole, setExternalID))
+		} else {
+			sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, iamAuth.AWSIAMRole))
+		}
+	}
+
+	getCreds, err := sess.Config.Credentials.Get()
+	if err != nil {
+		return err
+	}
+	// Set environment variables. These would be fetched by Login
+	os.Setenv("AWS_ACCESS_KEY_ID", getCreds.AccessKeyID)
+	os.Setenv("AWS_SECRET_ACCESS_KEY", getCreds.SecretAccessKey)
+	os.Setenv("AWS_SESSION_TOKEN", getCreds.SessionToken)
+
+	var awsAuthClient *authaws.AWSAuth
+
+	if iamAuth.VaultAWSIAMServerID != "" {
+		awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath), authaws.WithIAMServerIDHeader(iamAuth.VaultAWSIAMServerID))
+		if err != nil {
+			return err
+		}
+	} else {
+		awsAuthClient, err = authaws.NewAWSAuth(authaws.WithRegion(regionAWS), authaws.WithIAMAuth(), authaws.WithRole(iamAuth.Role), authaws.WithMountPath(awsAuthMountPath))
+		if err != nil {
+			return err
+		}
+	}
+
+	_, err = c.auth.Login(ctx, awsAuthClient)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 92 - 0
pkg/provider/vault/auth_jwt.go

@@ -0,0 +1,92 @@
+/*
+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 vault
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+const (
+	errJwtNoTokenSource = "neither `secretRef` nor `kubernetesServiceAccountToken` was supplied as token source for jwt authentication"
+)
+
+func setJwtAuthToken(ctx context.Context, v *client) (bool, error) {
+	jwtAuth := v.store.Auth.Jwt
+	if jwtAuth != nil {
+		err := v.requestTokenWithJwtAuth(ctx, jwtAuth)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithJwtAuth(ctx context.Context, jwtAuth *esv1beta1.VaultJwtAuth) error {
+	role := strings.TrimSpace(jwtAuth.Role)
+	var jwt string
+	var err error
+	if jwtAuth.SecretRef != nil {
+		jwt, err = resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, jwtAuth.SecretRef)
+	} else if k8sServiceAccountToken := jwtAuth.KubernetesServiceAccountToken; k8sServiceAccountToken != nil {
+		audiences := k8sServiceAccountToken.Audiences
+		if audiences == nil {
+			audiences = &[]string{"vault"}
+		}
+		expirationSeconds := k8sServiceAccountToken.ExpirationSeconds
+		if expirationSeconds == nil {
+			tmp := int64(600)
+			expirationSeconds = &tmp
+		}
+		jwt, err = createServiceAccountToken(
+			ctx,
+			c.corev1,
+			c.storeKind,
+			c.namespace,
+			k8sServiceAccountToken.ServiceAccountRef,
+			*audiences,
+			*expirationSeconds)
+	} else {
+		err = fmt.Errorf(errJwtNoTokenSource)
+	}
+	if err != nil {
+		return err
+	}
+
+	parameters := map[string]interface{}{
+		"role": role,
+		"jwt":  jwt,
+	}
+	url := strings.Join([]string{"auth", jwtAuth.Path, "login"}, "/")
+	vaultResult, err := c.logical.WriteWithContext(ctx, url, parameters)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
+	if err != nil {
+		return err
+	}
+
+	token, err := vaultResult.TokenID()
+	if err != nil {
+		return fmt.Errorf(errVaultToken, err)
+	}
+	c.client.SetToken(token)
+	return nil
+}

+ 151 - 0
pkg/provider/vault/auth_kubernetes.go

@@ -0,0 +1,151 @@
+/*
+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 vault
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	authkubernetes "github.com/hashicorp/vault/api/auth/kubernetes"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	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/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+const (
+	serviceAccTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+	errServiceAccount   = "cannot read Kubernetes service account token from file system: %w"
+	errGetKubeSA        = "cannot get Kubernetes service account %q: %w"
+	errGetKubeSASecrets = "cannot find secrets bound to service account: %q"
+	errGetKubeSANoToken = "cannot find token in secrets bound to service account: %q"
+)
+
+func setKubernetesAuthToken(ctx context.Context, v *client) (bool, error) {
+	kubernetesAuth := v.store.Auth.Kubernetes
+	if kubernetesAuth != nil {
+		err := v.requestTokenWithKubernetesAuth(ctx, kubernetesAuth)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithKubernetesAuth(ctx context.Context, kubernetesAuth *esv1beta1.VaultKubernetesAuth) error {
+	jwtString, err := getJwtString(ctx, c, kubernetesAuth)
+	if err != nil {
+		return err
+	}
+	k, err := authkubernetes.NewKubernetesAuth(kubernetesAuth.Role, authkubernetes.WithServiceAccountToken(jwtString), authkubernetes.WithMountPath(kubernetesAuth.Path))
+	if err != nil {
+		return err
+	}
+	_, err = c.auth.Login(ctx, k)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func getJwtString(ctx context.Context, v *client, kubernetesAuth *esv1beta1.VaultKubernetesAuth) (string, error) {
+	if kubernetesAuth.ServiceAccountRef != nil {
+		// Kubernetes <v1.24 fetch token via ServiceAccount.Secrets[]
+		// this behavior was removed in v1.24 and we must use TokenRequest API (see below)
+		jwt, err := v.secretKeyRefForServiceAccount(ctx, kubernetesAuth.ServiceAccountRef)
+		if jwt != "" {
+			return jwt, err
+		}
+		if err != nil {
+			v.log.V(1).Info("unable to fetch jwt from service account secret, trying service account token next")
+		}
+		// Kubernetes >=v1.24: fetch token via TokenRequest API
+		// note: this is a massive change from vault perspective: the `iss` claim will very likely change.
+		// Vault 1.9 deprecated issuer validation by default, and authentication with Vault clusters <1.9 will likely fail.
+		jwt, err = createServiceAccountToken(
+			ctx,
+			v.corev1,
+			v.storeKind,
+			v.namespace,
+			*kubernetesAuth.ServiceAccountRef,
+			nil,
+			600)
+		if err != nil {
+			return "", err
+		}
+		return jwt, nil
+	} else if kubernetesAuth.SecretRef != nil {
+		tokenRef := kubernetesAuth.SecretRef
+		if tokenRef.Key == "" {
+			tokenRef = kubernetesAuth.SecretRef.DeepCopy()
+			tokenRef.Key = "token"
+		}
+		jwt, err := resolvers.SecretKeyRef(ctx, v.kube, v.storeKind, v.namespace, tokenRef)
+		if err != nil {
+			return "", err
+		}
+		return jwt, nil
+	} else {
+		// Kubernetes authentication is specified, but without a referenced
+		// Kubernetes secret. We check if the file path for in-cluster service account
+		// exists and attempt to use the token for Vault Kubernetes auth.
+		if _, err := os.Stat(serviceAccTokenPath); err != nil {
+			return "", fmt.Errorf(errServiceAccount, err)
+		}
+		jwtByte, err := os.ReadFile(serviceAccTokenPath)
+		if err != nil {
+			return "", fmt.Errorf(errServiceAccount, err)
+		}
+		return string(jwtByte), nil
+	}
+}
+
+func (c *client) secretKeyRefForServiceAccount(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) (string, error) {
+	serviceAccount := &corev1.ServiceAccount{}
+	ref := types.NamespacedName{
+		Namespace: c.namespace,
+		Name:      serviceAccountRef.Name,
+	}
+	if (c.storeKind == esv1beta1.ClusterSecretStoreKind) &&
+		(serviceAccountRef.Namespace != nil) {
+		ref.Namespace = *serviceAccountRef.Namespace
+	}
+	err := c.kube.Get(ctx, ref, serviceAccount)
+	if err != nil {
+		return "", fmt.Errorf(errGetKubeSA, ref.Name, err)
+	}
+	if len(serviceAccount.Secrets) == 0 {
+		return "", fmt.Errorf(errGetKubeSASecrets, ref.Name)
+	}
+	for _, tokenRef := range serviceAccount.Secrets {
+		token, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &esmeta.SecretKeySelector{
+			Name:      tokenRef.Name,
+			Namespace: &ref.Namespace,
+			Key:       "token",
+		})
+		if err != nil {
+			continue
+		}
+		return token, nil
+	}
+	return "", fmt.Errorf(errGetKubeSANoToken, ref.Name)
+}

+ 58 - 0
pkg/provider/vault/auth_ldap.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 vault
+
+import (
+	"context"
+	"strings"
+
+	authldap "github.com/hashicorp/vault/api/auth/ldap"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+func setLdapAuthToken(ctx context.Context, v *client) (bool, error) {
+	ldapAuth := v.store.Auth.Ldap
+	if ldapAuth != nil {
+		err := v.requestTokenWithLdapAuth(ctx, ldapAuth)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithLdapAuth(ctx context.Context, ldapAuth *esv1beta1.VaultLdapAuth) error {
+	username := strings.TrimSpace(ldapAuth.Username)
+	password, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &ldapAuth.SecretRef)
+	if err != nil {
+		return err
+	}
+	pass := authldap.Password{FromString: password}
+	l, err := authldap.NewLDAPAuth(username, &pass, authldap.WithMountPath(ldapAuth.Path))
+	if err != nil {
+		return err
+	}
+	_, err = c.auth.Login(ctx, l)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 34 - 0
pkg/provider/vault/auth_token.go

@@ -0,0 +1,34 @@
+/*
+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 vault
+
+import (
+	"context"
+
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+func setSecretKeyToken(ctx context.Context, v *client) (bool, error) {
+	tokenRef := v.store.Auth.TokenSecretRef
+	if tokenRef != nil {
+		token, err := resolvers.SecretKeyRef(ctx, v.kube, v.storeKind, v.namespace, tokenRef)
+		if err != nil {
+			return true, err
+		}
+		v.client.SetToken(token)
+		return true, nil
+	}
+	return false, nil
+}

+ 58 - 0
pkg/provider/vault/auth_userpass.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 vault
+
+import (
+	"context"
+	"strings"
+
+	authuserpass "github.com/hashicorp/vault/api/auth/userpass"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+func setUserPassAuthToken(ctx context.Context, v *client) (bool, error) {
+	userPassAuth := v.store.Auth.UserPass
+	if userPassAuth != nil {
+		err := v.requestTokenWithUserPassAuth(ctx, userPassAuth)
+		if err != nil {
+			return true, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (c *client) requestTokenWithUserPassAuth(ctx context.Context, userPassAuth *esv1beta1.VaultUserPassAuth) error {
+	username := strings.TrimSpace(userPassAuth.Username)
+	password, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, &userPassAuth.SecretRef)
+	if err != nil {
+		return err
+	}
+	pass := authuserpass.Password{FromString: password}
+	l, err := authuserpass.NewUserpassAuth(username, &pass, authuserpass.WithMountPath(userPassAuth.Path))
+	if err != nil {
+		return err
+	}
+	_, err = c.auth.Login(ctx, l)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultLogin, err)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 195 - 0
pkg/provider/vault/client.go

@@ -0,0 +1,195 @@
+/*
+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 vault
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/go-logr/logr"
+	vault "github.com/hashicorp/vault/api"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	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"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+var _ esv1beta1.SecretsClient = &client{}
+
+type client struct {
+	kube      kclient.Client
+	store     *esv1beta1.VaultProvider
+	log       logr.Logger
+	corev1    typedcorev1.CoreV1Interface
+	client    util.Client
+	auth      util.Auth
+	logical   util.Logical
+	token     util.Token
+	namespace string
+	storeKind string
+}
+
+func (c *client) newConfig(ctx context.Context) (*vault.Config, error) {
+	cfg := vault.DefaultConfig()
+	cfg.Address = c.store.Server
+
+	if len(c.store.CABundle) != 0 || c.store.CAProvider != nil {
+		caCertPool := x509.NewCertPool()
+
+		if len(c.store.CABundle) > 0 {
+			ok := caCertPool.AppendCertsFromPEM(c.store.CABundle)
+			if !ok {
+				return nil, fmt.Errorf(errVaultCert, errors.New("failed to parse certificates from CertPool"))
+			}
+		}
+
+		if c.store.CAProvider != nil && c.storeKind == esv1beta1.ClusterSecretStoreKind && c.store.CAProvider.Namespace == nil {
+			return nil, errors.New(errCANamespace)
+		}
+
+		if c.store.CAProvider != nil {
+			var cert []byte
+			var err error
+
+			switch c.store.CAProvider.Type {
+			case esv1beta1.CAProviderTypeSecret:
+				cert, err = getCertFromSecret(c)
+			case esv1beta1.CAProviderTypeConfigMap:
+				cert, err = getCertFromConfigMap(c)
+			default:
+				return nil, errors.New(errUnknownCAProvider)
+			}
+
+			if err != nil {
+				return nil, err
+			}
+
+			ok := caCertPool.AppendCertsFromPEM(cert)
+			if !ok {
+				return nil, fmt.Errorf(errVaultCert, errors.New("failed to parse certificates from CertPool"))
+			}
+		}
+
+		if transport, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
+			transport.TLSClientConfig.RootCAs = caCertPool
+		}
+	}
+
+	err := c.configureClientTLS(ctx, cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	// If either read-after-write consistency feature is enabled, enable ReadYourWrites
+	cfg.ReadYourWrites = c.store.ReadYourWrites || c.store.ForwardInconsistent
+
+	return cfg, nil
+}
+
+func (c *client) configureClientTLS(ctx context.Context, cfg *vault.Config) error {
+	clientTLS := c.store.ClientTLS
+	if clientTLS.CertSecretRef != nil && clientTLS.KeySecretRef != nil {
+		if clientTLS.KeySecretRef.Key == "" {
+			clientTLS.KeySecretRef.Key = corev1.TLSPrivateKeyKey
+		}
+		clientKey, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, clientTLS.KeySecretRef)
+		if err != nil {
+			return err
+		}
+
+		if clientTLS.CertSecretRef.Key == "" {
+			clientTLS.CertSecretRef.Key = corev1.TLSCertKey
+		}
+		clientCert, err := resolvers.SecretKeyRef(ctx, c.kube, c.storeKind, c.namespace, clientTLS.CertSecretRef)
+		if err != nil {
+			return err
+		}
+
+		cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
+		if err != nil {
+			return fmt.Errorf(errClientTLSAuth, err)
+		}
+
+		if transport, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
+			transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+		}
+	}
+	return nil
+}
+
+func getCertFromSecret(v *client) ([]byte, error) {
+	secretRef := esmeta.SecretKeySelector{
+		Name:      v.store.CAProvider.Name,
+		Namespace: &v.namespace,
+		Key:       v.store.CAProvider.Key,
+	}
+
+	if v.store.CAProvider.Namespace != nil {
+		secretRef.Namespace = v.store.CAProvider.Namespace
+	}
+
+	ctx := context.Background()
+	res, err := resolvers.SecretKeyRef(ctx, v.kube, v.storeKind, v.namespace, &secretRef)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultCert, err)
+	}
+
+	return []byte(res), nil
+}
+
+func getCertFromConfigMap(v *client) ([]byte, error) {
+	objKey := types.NamespacedName{
+		Name:      v.store.CAProvider.Name,
+		Namespace: v.namespace,
+	}
+
+	if v.store.CAProvider.Namespace != nil {
+		objKey.Namespace = *v.store.CAProvider.Namespace
+	}
+
+	configMapRef := &corev1.ConfigMap{}
+	ctx := context.Background()
+	err := v.kube.Get(ctx, objKey, configMapRef)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultCert, err)
+	}
+
+	val, ok := configMapRef.Data[v.store.CAProvider.Key]
+	if !ok {
+		return nil, fmt.Errorf(errConfigMapFmt, v.store.CAProvider.Key)
+	}
+	return []byte(val), nil
+}
+
+func (c *client) Close(ctx context.Context) error {
+	// Revoke the token if we have one set, it wasn't sourced from a TokenSecretRef,
+	// and token caching isn't enabled
+	if !enableCache && c.client.Token() != "" && c.store.Auth.TokenSecretRef == nil {
+		err := revokeTokenIfValid(ctx, c.client)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 283 - 0
pkg/provider/vault/client_get.go

@@ -0,0 +1,283 @@
+/*
+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 vault
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/tidwall/gjson"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errReadSecret                   = "cannot read secret data from Vault: %w"
+	errDataField                    = "failed to find data field"
+	errJSONUnmarshall               = "failed to unmarshall JSON"
+	errPathInvalid                  = "provided Path isn't a valid kv v2 path"
+	errUnsupportedMetadataKvVersion = "cannot perform metadata fetch operations with kv version v1"
+	errNotFound                     = "secret not found"
+	errSecretKeyFmt                 = "cannot find secret data for key: %q"
+)
+
+// GetSecret supports two types:
+//  1. get the full secret as json-encoded value
+//     by leaving the ref.Property empty.
+//  2. get a key from the secret.
+//     Nested values are supported by specifying a gjson expression
+func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	var data map[string]interface{}
+	var err error
+	if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
+		if c.store.Version == esv1beta1.VaultKVStoreV1 {
+			return nil, errors.New(errUnsupportedMetadataKvVersion)
+		}
+
+		metadata, err := c.readSecretMetadata(ctx, ref.Key)
+		if err != nil {
+			return nil, err
+		}
+		if len(metadata) == 0 {
+			return nil, nil
+		}
+		data = make(map[string]interface{}, len(metadata))
+		for k, v := range metadata {
+			data[k] = v
+		}
+	} else {
+		data, err = c.readSecret(ctx, ref.Key, ref.Version)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Return nil if secret value is null
+	if data == nil {
+		return nil, esv1beta1.NoSecretError{}
+	}
+	jsonStr, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+	// (1): return raw json if no property is defined
+	if ref.Property == "" {
+		return jsonStr, nil
+	}
+
+	// For backwards compatibility we want the
+	// actual keys to take precedence over gjson syntax
+	// (2): extract key from secret with property
+	if _, ok := data[ref.Property]; ok {
+		return utils.GetByteValueFromMap(data, ref.Property)
+	}
+
+	// (3): extract key from secret using gjson
+	val := gjson.Get(string(jsonStr), ref.Property)
+	if !val.Exists() {
+		return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
+	}
+	return []byte(val.String()), nil
+}
+
+// GetSecretMap supports two modes of operation:
+// 1. get the full secret from the vault data payload (by leaving .property empty).
+// 2. extract key/value pairs from a (nested) object.
+func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := c.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	var secretData map[string]interface{}
+	err = json.Unmarshal(data, &secretData)
+	if err != nil {
+		return nil, err
+	}
+	byteMap := make(map[string][]byte, len(secretData))
+	for k := range secretData {
+		byteMap[k], err = utils.GetByteValueFromMap(secretData, k)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return byteMap, nil
+}
+
+func (c *client) readSecret(ctx context.Context, path, version string) (map[string]interface{}, error) {
+	dataPath := c.buildPath(path)
+
+	// path formated according to vault docs for v1 and v2 API
+	// v1: https://www.vaultproject.io/api-docs/secret/kv/kv-v1#read-secret
+	// v2: https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
+	var params map[string][]string
+	if version != "" {
+		params = make(map[string][]string)
+		params["version"] = []string{version}
+	}
+	vaultSecret, err := c.logical.ReadWithDataWithContext(ctx, dataPath, params)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultReadSecretData, err)
+	if err != nil {
+		return nil, fmt.Errorf(errReadSecret, err)
+	}
+	if vaultSecret == nil {
+		return nil, esv1beta1.NoSecretError{}
+	}
+	secretData := vaultSecret.Data
+	if c.store.Version == esv1beta1.VaultKVStoreV2 {
+		// Vault KV2 has data embedded within sub-field
+		// reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
+		dataInt, ok := vaultSecret.Data["data"]
+		if !ok {
+			return nil, errors.New(errDataField)
+		}
+		if dataInt == nil {
+			return nil, esv1beta1.NoSecretError{}
+		}
+		secretData, ok = dataInt.(map[string]interface{})
+		if !ok {
+			return nil, errors.New(errJSONUnmarshall)
+		}
+	}
+
+	return secretData, nil
+}
+
+func (c *client) readSecretMetadata(ctx context.Context, path string) (map[string]string, error) {
+	metadata := make(map[string]string)
+	url, err := c.buildMetadataPath(path)
+	if err != nil {
+		return nil, err
+	}
+	secret, err := c.logical.ReadWithDataWithContext(ctx, url, nil)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultReadSecretData, err)
+	if err != nil {
+		return nil, fmt.Errorf(errReadSecret, err)
+	}
+	if secret == nil {
+		return nil, errors.New(errNotFound)
+	}
+	t, ok := secret.Data["custom_metadata"]
+	if !ok {
+		return nil, nil
+	}
+	d, ok := t.(map[string]interface{})
+	if !ok {
+		return metadata, nil
+	}
+	for k, v := range d {
+		metadata[k] = v.(string)
+	}
+	return metadata, nil
+}
+
+func (c *client) buildMetadataPath(path string) (string, error) {
+	var url string
+	if c.store.Version == esv1beta1.VaultKVStoreV1 {
+		url = fmt.Sprintf("%s/%s", *c.store.Path, path)
+	} else { // KV v2 is used
+		if c.store.Path == nil && !strings.Contains(path, "data") {
+			return "", fmt.Errorf(errPathInvalid)
+		}
+		if c.store.Path == nil {
+			path = strings.Replace(path, "data", "metadata", 1)
+			url = path
+		} else {
+			url = fmt.Sprintf("%s/metadata/%s", *c.store.Path, path)
+		}
+	}
+	return url, nil
+}
+
+/*
+	 buildPath is a helper method to build the vault equivalent path
+		 from ExternalSecrets and SecretStore manifests. the path build logic
+		 varies depending on the SecretStore KV version:
+		 Example inputs/outputs:
+		 # simple build:
+		 kv version == "v2":
+			provider_path: "secret/path"
+			input: "foo"
+			output: "secret/path/data/foo" # provider_path and data are prepended
+		 kv version == "v1":
+			provider_path: "secret/path"
+			input: "foo"
+			output: "secret/path/foo" # provider_path is prepended
+		 # inheriting paths:
+		 kv version == "v2":
+			provider_path: "secret/path"
+			input: "secret/path/foo"
+			output: "secret/path/data/foo" #data is prepended
+		 kv version == "v2":
+			provider_path: "secret/path"
+			input: "secret/path/data/foo"
+			output: "secret/path/data/foo" #noop
+		 kv version == "v1":
+			provider_path: "secret/path"
+			input: "secret/path/foo"
+			output: "secret/path/foo" #noop
+		 # provider path not defined:
+		 kv version == "v2":
+			provider_path: nil
+			input: "secret/path/foo"
+			output: "secret/data/path/foo" # data is prepended to secret/
+		 kv version == "v2":
+			provider_path: nil
+			input: "secret/path/data/foo"
+			output: "secret/path/data/foo" #noop
+		 kv version == "v1":
+			provider_path: nil
+			input: "secret/path/foo"
+			output: "secret/path/foo" #noop
+*/
+func (c *client) buildPath(path string) string {
+	optionalMount := c.store.Path
+	out := path
+	// if optionalMount is Set, remove it from path if its there
+	if optionalMount != nil {
+		cut := *optionalMount + "/"
+		if strings.HasPrefix(out, cut) {
+			// This current logic induces a bug when the actual secret resides on same path names as the mount path.
+			_, out, _ = strings.Cut(out, cut)
+			// if data succeeds optionalMount on v2 store, we should remove it as well
+			if strings.HasPrefix(out, "data/") && c.store.Version == esv1beta1.VaultKVStoreV2 {
+				_, out, _ = strings.Cut(out, "data/")
+			}
+		}
+		buildPath := strings.Split(out, "/")
+		buildMount := strings.Split(*optionalMount, "/")
+		if c.store.Version == esv1beta1.VaultKVStoreV2 {
+			buildMount = append(buildMount, "data")
+		}
+		buildMount = append(buildMount, buildPath...)
+		out = strings.Join(buildMount, "/")
+		return out
+	}
+	if !strings.Contains(out, "/data/") && c.store.Version == esv1beta1.VaultKVStoreV2 {
+		buildPath := strings.Split(out, "/")
+		buildMount := []string{buildPath[0], "data"}
+		buildMount = append(buildMount, buildPath[1:]...)
+		out = strings.Join(buildMount, "/")
+		return out
+	}
+	return out
+}

+ 146 - 0
pkg/provider/vault/client_get_all_secrets.go

@@ -0,0 +1,146 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/find"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+)
+
+const (
+	errUnsupportedKvVersion = "cannot perform find operations with kv version v1"
+)
+
+// 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 c.store.Version == esv1beta1.VaultKVStoreV1 {
+		return nil, errors.New(errUnsupportedKvVersion)
+	}
+	searchPath := ""
+	if ref.Path != nil {
+		searchPath = *ref.Path + "/"
+	}
+	potentialSecrets, err := c.listSecrets(ctx, searchPath)
+	if err != nil {
+		return nil, err
+	}
+	if ref.Name != nil {
+		return c.findSecretsFromName(ctx, potentialSecrets, *ref.Name)
+	}
+	return c.findSecretsFromTags(ctx, potentialSecrets, ref.Tags)
+}
+
+func (c *client) findSecretsFromTags(ctx context.Context, candidates []string, tags map[string]string) (map[string][]byte, error) {
+	secrets := make(map[string][]byte)
+	for _, name := range candidates {
+		match := true
+		metadata, err := c.readSecretMetadata(ctx, name)
+		if err != nil {
+			return nil, err
+		}
+		for tk, tv := range tags {
+			p, ok := metadata[tk]
+			if !ok || p != tv {
+				match = false
+				break
+			}
+		}
+		if match {
+			secret, err := c.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: name})
+			if errors.Is(err, esv1beta1.NoSecretError{}) {
+				continue
+			}
+			if err != nil {
+				return nil, err
+			}
+			if secret != nil {
+				secrets[name] = secret
+			}
+		}
+	}
+	return secrets, nil
+}
+
+func (c *client) findSecretsFromName(ctx context.Context, candidates []string, ref esv1beta1.FindName) (map[string][]byte, error) {
+	secrets := make(map[string][]byte)
+	matcher, err := find.New(ref)
+	if err != nil {
+		return nil, err
+	}
+	for _, name := range candidates {
+		ok := matcher.MatchName(name)
+		if ok {
+			secret, err := c.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: name})
+			if errors.Is(err, esv1beta1.NoSecretError{}) {
+				continue
+			}
+			if err != nil {
+				return nil, err
+			}
+			if secret != nil {
+				secrets[name] = secret
+			}
+		}
+	}
+	return secrets, nil
+}
+
+func (c *client) listSecrets(ctx context.Context, path string) ([]string, error) {
+	secrets := make([]string, 0)
+	url, err := c.buildMetadataPath(path)
+	if err != nil {
+		return nil, err
+	}
+	secret, err := c.logical.ListWithContext(ctx, url)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultListSecrets, err)
+	if err != nil {
+		return nil, fmt.Errorf(errReadSecret, err)
+	}
+	if secret == nil {
+		return nil, fmt.Errorf("provided path %v does not contain any secrets", url)
+	}
+	t, ok := secret.Data["keys"]
+	if !ok {
+		return nil, nil
+	}
+	paths := t.([]interface{})
+	for _, p := range paths {
+		strPath := p.(string)
+		fullPath := path + strPath // because path always ends with a /
+		if path == "" {
+			fullPath = strPath
+		}
+		// Recurrently find secrets
+		if !strings.HasSuffix(p.(string), "/") {
+			secrets = append(secrets, fullPath)
+		} else {
+			partial, err := c.listSecrets(ctx, fullPath)
+			if err != nil {
+				return nil, err
+			}
+			secrets = append(secrets, partial...)
+		}
+	}
+	return secrets, nil
+}

+ 306 - 0
pkg/provider/vault/client_get_all_secrets_test.go

@@ -0,0 +1,306 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	vault "github.com/hashicorp/vault/api"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+)
+
+func TestGetAllSecrets(t *testing.T) {
+	secret1Bytes := []byte("{\"access_key\":\"access_key\",\"access_secret\":\"access_secret\"}")
+	secret2Bytes := []byte("{\"access_key\":\"access_key2\",\"access_secret\":\"access_secret2\"}")
+	path1Bytes := []byte("{\"access_key\":\"path1\",\"access_secret\":\"path1\"}")
+	path2Bytes := []byte("{\"access_key\":\"path2\",\"access_secret\":\"path2\"}")
+	tagBytes := []byte("{\"access_key\":\"unfetched\",\"access_secret\":\"unfetched\"}")
+	path := "path"
+	secret := map[string]interface{}{
+		"secret1": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "bar",
+				},
+			},
+			"data": map[string]interface{}{
+				"access_key":    "access_key",
+				"access_secret": "access_secret",
+			},
+		},
+		"secret2": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "baz",
+				},
+			},
+			"data": map[string]interface{}{
+				"access_key":    "access_key2",
+				"access_secret": "access_secret2",
+			},
+		},
+		"secret3": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "baz",
+				},
+			},
+			"data": nil,
+		},
+		"tag": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "baz",
+				},
+			},
+			"data": map[string]interface{}{
+				"access_key":    "unfetched",
+				"access_secret": "unfetched",
+			},
+		},
+		"path/1": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "path",
+				},
+			},
+			"data": map[string]interface{}{
+				"access_key":    "path1",
+				"access_secret": "path1",
+			},
+		},
+		"path/2": map[string]interface{}{
+			"metadata": map[string]interface{}{
+				"custom_metadata": map[string]interface{}{
+					"foo": "path",
+				},
+			},
+			"data": map[string]interface{}{
+				"access_key":    "path2",
+				"access_secret": "path2",
+			},
+		},
+		"default": map[string]interface{}{
+			"data": map[string]interface{}{
+				"empty": "true",
+			},
+			"metadata": map[string]interface{}{
+				"keys": []interface{}{"secret1", "secret2", "secret3", "tag", "path/"},
+			},
+		},
+		"path/": map[string]interface{}{
+			"data": map[string]interface{}{
+				"empty": "true",
+			},
+			"metadata": map[string]interface{}{
+				"keys": []interface{}{"1", "2"},
+			},
+		},
+	}
+	type args struct {
+		store    *esv1beta1.VaultProvider
+		kube     kclient.Client
+		vLogical util.Logical
+		ns       string
+		data     esv1beta1.ExternalSecretFind
+	}
+
+	type want struct {
+		err error
+		val map[string][]byte
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"FindByName": {
+			reason: "should map multiple secrets matching name",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ListWithContextFn:         newListWithContextFn(secret),
+					ReadWithDataWithContextFn: newReadtWithContextFn(secret),
+				},
+				data: esv1beta1.ExternalSecretFind{
+					Name: &esv1beta1.FindName{
+						RegExp: "secret.*",
+					},
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"secret1": secret1Bytes,
+					"secret2": secret2Bytes,
+				},
+			},
+		},
+		"FindByTag": {
+			reason: "should map multiple secrets matching tags",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ListWithContextFn:         newListWithContextFn(secret),
+					ReadWithDataWithContextFn: newReadtWithContextFn(secret),
+				},
+				data: esv1beta1.ExternalSecretFind{
+					Tags: map[string]string{
+						"foo": "baz",
+					},
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"tag":     tagBytes,
+					"secret2": secret2Bytes,
+				},
+			},
+		},
+		"FilterByPath": {
+			reason: "should filter secrets based on path",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ListWithContextFn:         newListWithContextFn(secret),
+					ReadWithDataWithContextFn: newReadtWithContextFn(secret),
+				},
+				data: esv1beta1.ExternalSecretFind{
+					Path: &path,
+					Tags: map[string]string{
+						"foo": "path",
+					},
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"path/1": path1Bytes,
+					"path/2": path2Bytes,
+				},
+			},
+		},
+		"FailIfKv1": {
+			reason: "should not work if using kv1 store",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ListWithContextFn:         newListWithContextFn(secret),
+					ReadWithDataWithContextFn: newReadtWithContextFn(secret),
+				},
+				data: esv1beta1.ExternalSecretFind{
+					Tags: map[string]string{
+						"foo": "baz",
+					},
+				},
+			},
+			want: want{
+				err: errors.New(errUnsupportedKvVersion),
+			},
+		},
+		"MetadataNotFound": {
+			reason: "metadata secret not found",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ListWithContextFn: newListWithContextFn(secret),
+					ReadWithDataWithContextFn: func(ctx context.Context, path string, d map[string][]string) (*vault.Secret, error) {
+						return nil, nil
+					},
+				},
+				data: esv1beta1.ExternalSecretFind{
+					Tags: map[string]string{
+						"foo": "baz",
+					},
+				},
+			},
+			want: want{
+				err: errors.New(errNotFound),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			vStore := &client{
+				kube:      tc.args.kube,
+				logical:   tc.args.vLogical,
+				store:     tc.args.store,
+				namespace: tc.args.ns,
+			}
+			val, err := vStore.GetAllSecrets(context.Background(), tc.args.data)
+			if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecretMap(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.val, val); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecretMap(...): -want val, +got val:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func newListWithContextFn(secrets map[string]interface{}) func(ctx context.Context, path string) (*vault.Secret, error) {
+	return func(ctx context.Context, path string) (*vault.Secret, error) {
+		path = strings.TrimPrefix(path, "secret/metadata/")
+		if path == "" {
+			path = "default"
+		}
+		data, ok := secrets[path]
+		if !ok {
+			return nil, errors.New("Secret not found")
+		}
+		meta := data.(map[string]interface{})
+		ans := meta["metadata"].(map[string]interface{})
+		secret := &vault.Secret{
+			Data: map[string]interface{}{
+				"keys": ans["keys"],
+			},
+		}
+		return secret, nil
+	}
+}
+
+func newReadtWithContextFn(secrets map[string]interface{}) func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
+	return func(ctx context.Context, path string, d map[string][]string) (*vault.Secret, error) {
+		path = strings.TrimPrefix(path, "secret/data/")
+		path = strings.TrimPrefix(path, "secret/metadata/")
+		if path == "" {
+			path = "default"
+		}
+		data, ok := secrets[path]
+		if !ok {
+			return nil, errors.New("Secret not found")
+		}
+		meta := data.(map[string]interface{})
+		metadata := meta["metadata"].(map[string]interface{})
+		content := map[string]interface{}{
+			"data":            meta["data"],
+			"custom_metadata": metadata["custom_metadata"],
+		}
+		secret := &vault.Secret{
+			Data: content,
+		}
+		return secret, nil
+	}
+}

+ 720 - 0
pkg/provider/vault/client_get_test.go

@@ -0,0 +1,720 @@
+/*
+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 vault
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	vault "github.com/hashicorp/vault/api"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+)
+
+func TestGetSecret(t *testing.T) {
+	errBoom := errors.New("boom")
+	secret := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+	}
+	secretWithNilVal := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+		"token":         nil,
+	}
+	secretWithNestedVal := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+		"nested.bar":    "something different",
+		"nested": map[string]string{
+			"foo": "oke",
+			"bar": "also ok?",
+		},
+		"list_of_values": []string{
+			"first_value",
+			"second_value",
+			"third_value",
+		},
+		"json_number": json.Number("42"),
+	}
+
+	type args struct {
+		store    *esv1beta1.VaultProvider
+		kube     kclient.Client
+		vLogical util.Logical
+		ns       string
+		data     esv1beta1.ExternalSecretDataRemoteRef
+	}
+
+	type want struct {
+		err error
+		val []byte
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"ReadSecret": {
+			reason: "Should return the secret with property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "access_key",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("access_key"),
+			},
+		},
+		"ReadSecretWithNil": {
+			reason: "Should return the secret with property if it has a nil val",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "access_key",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNilVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("access_key"),
+			},
+		},
+		"ReadSecretWithoutProperty": {
+			reason: "Should return the json encoded secret without property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data:  esv1beta1.ExternalSecretDataRemoteRef{},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte(`{"access_key":"access_key","access_secret":"access_secret"}`),
+			},
+		},
+		"ReadSecretWithNestedValue": {
+			reason: "Should return a nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "nested.foo",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("oke"),
+			},
+		},
+		"ReadSecretWithNestedValueFromData": {
+			reason: "Should return a nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					//
+					Property: "nested.bar",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("something different"),
+			},
+		},
+		"ReadSecretWithMissingValueFromData": {
+			reason: "Should return a NoSecretErr",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "not-relevant",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: esv1beta1.NoSecretErr,
+				val: nil,
+			},
+		},
+		"ReadSecretWithSliceValue": {
+			reason: "Should return property as a joined slice",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "list_of_values",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("first_value\nsecond_value\nthird_value"),
+			},
+		},
+		"ReadSecretWithJsonNumber": {
+			reason: "Should return parsed json.Number property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "json_number",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("42"),
+			},
+		},
+		"NonexistentProperty": {
+			reason: "Should return error property does not exist.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "nop.doesnt.exist",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNestedVal, nil),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errSecretKeyFmt, "nop.doesnt.exist"),
+			},
+		},
+		"ReadSecretError": {
+			reason: "Should return error if vault client fails to read secret.",
+			args: args{
+				store: makeSecretStore().Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, errBoom),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errReadSecret, errBoom),
+			},
+		},
+		"ReadSecretNotFound": {
+			reason: "Secret doesn't exist",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "access_key",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
+						return nil, nil
+					},
+				},
+			},
+			want: want{
+				err: esv1beta1.NoSecretError{},
+			},
+		},
+		"ReadSecretMetadataWithoutProperty": {
+			reason: "Should return the json encoded metadata",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					MetadataPolicy: "Fetch",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadMetadataWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte(`{"access_key":"access_key","access_secret":"access_secret"}`),
+			},
+		},
+		"ReadSecretMetadataWithProperty": {
+			reason: "Should return the access_key value from the metadata",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					MetadataPolicy: "Fetch",
+					Property:       "access_key",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadMetadataWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("access_key"),
+			},
+		},
+		"FailReadSecretMetadataInvalidProperty": {
+			reason: "Should return error of non existent key inmetadata",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					MetadataPolicy: "Fetch",
+					Property:       "does_not_exist",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadMetadataWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errSecretKeyFmt, "does_not_exist"),
+			},
+		},
+		"FailReadSecretMetadataNoMetadata": {
+			reason: "Should return the access_key value from the metadata",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					MetadataPolicy: "Fetch",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadMetadataWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errNotFound),
+			},
+		},
+		"FailReadSecretMetadataWrongVersion": {
+			reason: "Should return the access_key value from the metadata",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					MetadataPolicy: "Fetch",
+				},
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadMetadataWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errUnsupportedMetadataKvVersion),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			vStore := &client{
+				kube:      tc.args.kube,
+				logical:   tc.args.vLogical,
+				store:     tc.args.store,
+				namespace: tc.args.ns,
+			}
+			val, err := vStore.GetSecret(context.Background(), tc.args.data)
+			if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(string(tc.want.val), string(val)); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecret(...): -want val, +got val:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	errBoom := errors.New("boom")
+	secret := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+	}
+	secretWithSpecialCharacter := map[string]interface{}{
+		"access_key":    "acc<ess_&ke.,y",
+		"access_secret": "acce&?ss_s>ecret",
+	}
+	secretWithNilVal := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+		"token":         nil,
+	}
+	secretWithNestedVal := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+		"nested": map[string]interface{}{
+			"foo": map[string]string{
+				"oke":    "yup",
+				"mhkeih": "yada yada",
+			},
+		},
+	}
+	secretWithTypes := map[string]interface{}{
+		"access_secret": "access_secret",
+		"f32":           float32(2.12),
+		"f64":           float64(2.1234534153423423),
+		"int":           42,
+		"bool":          true,
+		"bt":            []byte("foobar"),
+	}
+
+	type args struct {
+		store   *esv1beta1.VaultProvider
+		kube    kclient.Client
+		vClient util.Logical
+		ns      string
+		data    esv1beta1.ExternalSecretDataRemoteRef
+	}
+
+	type want struct {
+		err error
+		val map[string][]byte
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"ReadSecretKV1": {
+			reason: "Should read a v1 secret",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secret, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("access_key"),
+					"access_secret": []byte("access_secret"),
+				},
+			},
+		},
+		"ReadSecretKV2": {
+			reason: "Should read a v2 secret",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secret,
+					}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("access_key"),
+					"access_secret": []byte("access_secret"),
+				},
+			},
+		},
+		"ReadSecretWithSpecialCharactersKV1": {
+			reason: "Should read a v1 secret with special characters",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithSpecialCharacter, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("acc<ess_&ke.,y"),
+					"access_secret": []byte("acce&?ss_s>ecret"),
+				},
+			},
+		},
+		"ReadSecretWithSpecialCharactersKV2": {
+			reason: "Should read a v2 secret with special characters",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithSpecialCharacter,
+					}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("acc<ess_&ke.,y"),
+					"access_secret": []byte("acce&?ss_s>ecret"),
+				},
+			},
+		},
+		"ReadSecretWithNilValueKV1": {
+			reason: "Should read v1 secret with a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNilVal, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("access_key"),
+					"access_secret": []byte("access_secret"),
+					"token":         []byte(nil),
+				},
+			},
+		},
+		"ReadSecretWithNilValueKV2": {
+			reason: "Should read v2 secret with a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithNilVal}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_key":    []byte("access_key"),
+					"access_secret": []byte("access_secret"),
+					"token":         []byte(nil),
+				},
+			},
+		},
+		"ReadSecretWithTypesKV2": {
+			reason: "Should read v2 secret with different types",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithTypes}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"access_secret": []byte("access_secret"),
+					"f32":           []byte("2.12"),
+					"f64":           []byte("2.1234534153423423"),
+					"int":           []byte("42"),
+					"bool":          []byte("true"),
+					"bt":            []byte("Zm9vYmFy"), // base64
+				},
+			},
+		},
+		"ReadNestedSecret": {
+			reason: "Should read the secret with nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "nested",
+				},
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithNestedVal}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"foo": []byte(`{"mhkeih":"yada yada","oke":"yup"}`),
+				},
+			},
+		},
+		"ReadDeeplyNestedSecret": {
+			reason: "Should read the secret for deeply nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					Property: "nested.foo",
+				},
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithNestedVal}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"oke":    []byte("yup"),
+					"mhkeih": []byte("yada yada"),
+				},
+			},
+		},
+		"ReadSecretError": {
+			reason: "Should return error if vault client fails to read secret.",
+			args: args{
+				store: makeSecretStore().Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, errBoom),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errReadSecret, errBoom),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			vStore := &client{
+				kube:      tc.args.kube,
+				logical:   tc.args.vClient,
+				store:     tc.args.store,
+				namespace: tc.args.ns,
+			}
+			val, err := vStore.GetSecretMap(context.Background(), tc.args.data)
+			if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecretMap(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.val, val); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecretMap(...): -want val, +got val:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestGetSecretPath(t *testing.T) {
+	storeV2 := makeValidSecretStore()
+	storeV2NoPath := storeV2.DeepCopy()
+	multiPath := "secret/path"
+	storeV2.Spec.Provider.Vault.Path = &multiPath
+	storeV2NoPath.Spec.Provider.Vault.Path = nil
+
+	storeV1 := makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1)
+	storeV1NoPath := storeV1.DeepCopy()
+	storeV1.Spec.Provider.Vault.Path = &multiPath
+	storeV1NoPath.Spec.Provider.Vault.Path = nil
+
+	type args struct {
+		store    *esv1beta1.VaultProvider
+		path     string
+		expected string
+	}
+	cases := map[string]struct {
+		reason string
+		args   args
+	}{
+		"PathWithoutFormatV2": {
+			reason: "path should compose with mount point if set",
+			args: args{
+				store:    storeV2.Spec.Provider.Vault,
+				path:     "secret/path/data/test",
+				expected: "secret/path/data/test",
+			},
+		},
+		"PathWithoutFormatV2_NoData": {
+			reason: "path should compose with mount point if set without data",
+			args: args{
+				store:    storeV2.Spec.Provider.Vault,
+				path:     "secret/path/test",
+				expected: "secret/path/data/test",
+			},
+		},
+		"PathWithoutFormatV2_NoPath": {
+			reason: "if no mountpoint and no data available, needs to be set in second element",
+			args: args{
+				store:    storeV2NoPath.Spec.Provider.Vault,
+				path:     "secret/test/big/path",
+				expected: "secret/data/test/big/path",
+			},
+		},
+		"PathWithoutFormatV2_NoPathWithData": {
+			reason: "if data is available, should respect order",
+			args: args{
+				store:    storeV2NoPath.Spec.Provider.Vault,
+				path:     "secret/test/data/not/the/first/and/data/twice",
+				expected: "secret/test/data/not/the/first/and/data/twice",
+			},
+		},
+		"PathWithoutFormatV1": {
+			reason: "v1 mountpoint should be added but not enforce 'data'",
+			args: args{
+				store:    storeV1.Spec.Provider.Vault,
+				path:     "secret/path/test",
+				expected: "secret/path/test",
+			},
+		},
+		"PathWithoutFormatV1_NoPath": {
+			reason: "Should not append any path information if v1 with no mountpoint",
+			args: args{
+				store:    storeV1NoPath.Spec.Provider.Vault,
+				path:     "secret/test",
+				expected: "secret/test",
+			},
+		},
+		"WithoutPathButMountpointV2": {
+			reason: "Mountpoint needs to be set in addition to data",
+			args: args{
+				store:    storeV2.Spec.Provider.Vault,
+				path:     "test",
+				expected: "secret/path/data/test",
+			},
+		},
+		"WithoutPathButMountpointV1": {
+			reason: "Mountpoint needs to be set in addition to data",
+			args: args{
+				store:    storeV1.Spec.Provider.Vault,
+				path:     "test",
+				expected: "secret/path/test",
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			vStore := &client{
+				store: tc.args.store,
+			}
+			want := vStore.buildPath(tc.args.path)
+			if diff := cmp.Diff(want, tc.args.expected); diff != "" {
+				t.Errorf("\n%s\nvault.buildPath(...): -want expected, +got error:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+// EquateErrors returns true if the supplied errors are of the same type and
+// produce identical strings. This mirrors the error comparison behavior of
+// https://github.com/go-test/deep, which most Crossplane tests targeted before
+// we switched to go-cmp.
+//
+// This differs from cmpopts.EquateErrors, which does not test for error strings
+// and instead returns whether one error 'is' (in the errors.Is sense) the
+// other.
+func EquateErrors() cmp.Option {
+	return cmp.Comparer(func(a, b error) bool {
+		if a == nil || b == nil {
+			return a == nil && b == nil
+		}
+
+		av := reflect.ValueOf(a)
+		bv := reflect.ValueOf(b)
+		if av.Type() != bv.Type() {
+			return false
+		}
+
+		return a.Error() == b.Error()
+	})
+}

+ 185 - 0
pkg/provider/vault/client_push.go

@@ -0,0 +1,185 @@
+/*
+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 vault
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/constants"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+)
+
+func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	value := secret.Data[data.GetSecretKey()]
+	label := map[string]interface{}{
+		"custom_metadata": map[string]string{
+			"managed-by": "external-secrets",
+		},
+	}
+	secretVal := make(map[string]interface{})
+	path := c.buildPath(data.GetRemoteKey())
+	metaPath, err := c.buildMetadataPath(data.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+
+	// Retrieve the secret map from vault and convert the secret value in string form.
+	vaultSecret, err := c.readSecret(ctx, path, "")
+	// If error is not of type secret not found, we should error
+	if err != nil && !errors.Is(err, esv1beta1.NoSecretError{}) {
+		return err
+	}
+	// If the secret exists (err == nil), we should check if it is managed by external-secrets
+	if err == nil {
+		metadata, err := c.readSecretMetadata(ctx, data.GetRemoteKey())
+		if err != nil {
+			return err
+		}
+		manager, ok := metadata["managed-by"]
+		if !ok || manager != "external-secrets" {
+			return fmt.Errorf("secret not managed by external-secrets")
+		}
+	}
+	// Remove the metadata map to check the reconcile difference
+	if c.store.Version == esv1beta1.VaultKVStoreV1 {
+		delete(vaultSecret, "custom_metadata")
+	}
+	buf := &bytes.Buffer{}
+	enc := json.NewEncoder(buf)
+	enc.SetEscapeHTML(false)
+	err = enc.Encode(vaultSecret)
+	if err != nil {
+		return fmt.Errorf("error encoding vault secret: %w", err)
+	}
+	vaultSecretValue := bytes.TrimSpace(buf.Bytes())
+	if err != nil {
+		return fmt.Errorf("error marshaling vault secret: %w", err)
+	}
+	if bytes.Equal(vaultSecretValue, value) {
+		return nil
+	}
+	// If a Push of a property only, we should merge and add/update the property
+	if data.GetProperty() != "" {
+		if _, ok := vaultSecret[data.GetProperty()]; ok {
+			d := vaultSecret[data.GetProperty()].(string)
+			if err != nil {
+				return fmt.Errorf("error marshaling vault secret: %w", err)
+			}
+			// If the property has the same value, don't update the secret
+			if bytes.Equal([]byte(d), value) {
+				return nil
+			}
+		}
+		for k, v := range vaultSecret {
+			secretVal[k] = v
+		}
+		// Secret got from vault is already on map[string]string format
+		secretVal[data.GetProperty()] = string(value)
+	} else {
+		err = json.Unmarshal(value, &secretVal)
+		if err != nil {
+			return fmt.Errorf("error unmarshalling vault secret: %w", err)
+		}
+	}
+	secretToPush := secretVal
+	// Adding custom_metadata to the secret for KV v1
+	if c.store.Version == esv1beta1.VaultKVStoreV1 {
+		secretToPush["custom_metadata"] = label["custom_metadata"]
+	}
+	if c.store.Version == esv1beta1.VaultKVStoreV2 {
+		secretToPush = map[string]interface{}{
+			"data": secretVal,
+		}
+	}
+	if err != nil {
+		return fmt.Errorf("failed to convert value to a valid JSON: %w", err)
+	}
+	// Secret metadata should be pushed separately only for KV2
+	if c.store.Version == esv1beta1.VaultKVStoreV2 {
+		_, err = c.logical.WriteWithContext(ctx, metaPath, label)
+		metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
+		if err != nil {
+			return err
+		}
+	}
+	// Otherwise, create or update the version.
+	_, err = c.logical.WriteWithContext(ctx, path, secretToPush)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultWriteSecretData, err)
+	return err
+}
+
+func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
+	path := c.buildPath(remoteRef.GetRemoteKey())
+	metaPath, err := c.buildMetadataPath(remoteRef.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+	// Retrieve the secret map from vault and convert the secret value in string form.
+	secretVal, err := c.readSecret(ctx, path, "")
+	// If error is not of type secret not found, we should error
+	if err != nil && errors.Is(err, esv1beta1.NoSecretError{}) {
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+	metadata, err := c.readSecretMetadata(ctx, remoteRef.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+	manager, ok := metadata["managed-by"]
+	if !ok || manager != "external-secrets" {
+		return nil
+	}
+	// If Push for a Property, we need to delete the property and update the secret
+	if remoteRef.GetProperty() != "" {
+		delete(secretVal, remoteRef.GetProperty())
+		// If the only key left in the remote secret is the reference of the metadata.
+		if c.store.Version == esv1beta1.VaultKVStoreV1 && len(secretVal) == 1 {
+			delete(secretVal, "custom_metadata")
+		}
+		if len(secretVal) > 0 {
+			secretToPush := secretVal
+			if c.store.Version == esv1beta1.VaultKVStoreV2 {
+				secretToPush = map[string]interface{}{
+					"data": secretVal,
+				}
+			}
+			_, err = c.logical.WriteWithContext(ctx, path, secretToPush)
+			metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
+			return err
+		}
+	}
+	_, err = c.logical.DeleteWithContext(ctx, path)
+	metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
+	if err != nil {
+		return fmt.Errorf("could not delete secret %v: %w", remoteRef.GetRemoteKey(), err)
+	}
+	if c.store.Version == esv1beta1.VaultKVStoreV2 {
+		_, err = c.logical.DeleteWithContext(ctx, metaPath)
+		metrics.ObserveAPICall(constants.ProviderHCVault, constants.CallHCVaultDeleteSecret, err)
+		if err != nil {
+			return fmt.Errorf("could not delete secret metadata %v: %w", remoteRef.GetRemoteKey(), err)
+		}
+	}
+	return nil
+}

+ 683 - 0
pkg/provider/vault/client_push_test.go

@@ -0,0 +1,683 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+)
+
+const (
+	fakeKey      = "fake-key"
+	fakeValue    = "fake-value"
+	managedBy    = "managed-by"
+	managedByESO = "external-secrets"
+)
+
+func TestDeleteSecret(t *testing.T) {
+	type args struct {
+		store    *esv1beta1.VaultProvider
+		vLogical util.Logical
+	}
+
+	type want struct {
+		err error
+	}
+	tests := map[string]struct {
+		reason string
+		args   args
+		ref    *testingfake.PushSecretData
+		want   want
+		value  []byte
+	}{
+		"DeleteSecretNoOpKV1": {
+			reason: "delete secret is a no-op if v1 secret does not exist",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn:       fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretNoOpKV2": {
+			reason: "delete secret is a no-op if v2 secret does not exist",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn:       fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretFailIfErrorKV1": {
+			reason: "delete v1 secret fails if error occurs",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, fmt.Errorf("failed to read")),
+					WriteWithContextFn:        fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn:       fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: fmt.Errorf("failed to read"),
+			},
+		},
+		"DeleteSecretFailIfErrorKV2": {
+			reason: "delete v2 secret fails if error occurs",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, fmt.Errorf("failed to read")),
+					WriteWithContextFn:        fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn:       fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: fmt.Errorf("failed to read"),
+			},
+		},
+		"DeleteSecretNotManagedKV1": {
+			reason: "delete v1 secret when not managed by ESO",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: "another-secret-tool",
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretNotManagedKV2": {
+			reason: "delete v2 secret when not managed by eso",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: "another-secret-tool",
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretSuccessKV1": {
+			reason: "delete secret succeeds if secret is managed by ESO and exists in vault v1",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretSuccessKV2": {
+			reason: "delete secret succeeds if secret is managed by ESO and exists in vault v2",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretErrorKV1": {
+			reason: "delete secret fails if error occurs v1",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, fmt.Errorf("failed to delete")),
+				},
+			},
+			want: want{
+				err: fmt.Errorf("failed to delete"),
+			},
+		},
+		"DeleteSecretErrorKV2": {
+			reason: "delete secret fails if error occurs v2",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, fmt.Errorf("failed to delete")),
+				},
+			},
+			want: want{
+				err: fmt.Errorf("failed to delete"),
+			},
+		},
+		"DeleteSecretUpdatePropertyKV1": {
+			reason: "Secret should only be updated if Property is set v1",
+			ref:    &testingfake.PushSecretData{RemoteKey: "secret", Property: fakeKey},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"foo":   "bar",
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]interface{}{
+						"foo": "bar",
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						}}),
+					DeleteWithContextFn: fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretUpdatePropertyKV2": {
+			reason: "Secret should only be updated if Property is set v2",
+			ref:    &testingfake.PushSecretData{RemoteKey: "secret", Property: fakeKey},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+							"foo":   "bar",
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextValue(map[string]interface{}{"data": map[string]interface{}{"foo": "bar"}}),
+					DeleteWithContextFn: fake.ExpectDeleteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretIfNoOtherPropertiesKV1": {
+			reason: "Secret should only be deleted if no other properties are set v1",
+			ref:    &testingfake.PushSecretData{RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"foo": "bar",
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteSecretIfNoOtherPropertiesKV2": {
+			reason: "Secret should only be deleted if no other properties are set v2",
+			ref:    &testingfake.PushSecretData{RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							"foo": "bar",
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn:  fake.ExpectWriteWithContextNoCall(),
+					DeleteWithContextFn: fake.NewDeleteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			ref := testingfake.PushSecretData{RemoteKey: "secret", Property: ""}
+			if tc.ref != nil {
+				ref = *tc.ref
+			}
+			client := &client{
+				logical: tc.args.vLogical,
+				store:   tc.args.store,
+			}
+			err := client.DeleteSecret(context.Background(), ref)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+				}
+			}
+		})
+	}
+}
+func TestPushSecret(t *testing.T) {
+	secretKey := "secret-key"
+	noPermission := errors.New("no permission")
+	type args struct {
+		store    *esv1beta1.VaultProvider
+		vLogical util.Logical
+	}
+
+	type want struct {
+		err error
+	}
+	tests := map[string]struct {
+		reason string
+		args   args
+		want   want
+		data   *testingfake.PushSecretData
+		value  []byte
+	}{
+		"SetSecretKV1": {
+			reason: "secret is successfully set, with no existing vault secret",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.NewWriteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretKV2": {
+			reason: "secret is successfully set, with no existing vault secret",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.NewWriteWithContextFn(nil, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretWithWriteErrorKV1": {
+			reason: "secret cannot be pushed if write fails",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.NewWriteWithContextFn(nil, noPermission),
+				},
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretWithWriteErrorKV2": {
+			reason: "secret cannot be pushed if write fails",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn:        fake.NewWriteWithContextFn(nil, noPermission),
+				},
+			},
+			want: want{
+				err: noPermission,
+			},
+		},
+		"SetSecretEqualsPushSecretV1": {
+			reason: "vault secret kv equals secret to push kv",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretEqualsPushSecretV2": {
+			reason: "vault secret kv equals secret to push kv",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretPropertyKV1": {
+			reason: "push secret with property adds the property",
+			value:  []byte(fakeValue),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]interface{}{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]string{
+							managedBy: managedByESO,
+						},
+						"foo": fakeValue,
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretPropertyKV2": {
+			reason: "push secret with property adds the property",
+			value:  []byte(fakeValue),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]interface{}{"data": map[string]interface{}{fakeKey: fakeValue, "foo": fakeValue}}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretUpdatePropertyKV1": {
+			reason: "push secret with property only updates the property",
+			value:  []byte("new-value"),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"foo": fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]interface{}{
+						"foo": "new-value",
+						"custom_metadata": map[string]string{
+							managedBy: managedByESO,
+						},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretUpdatePropertyKV2": {
+			reason: "push secret with property only updates the property",
+			value:  []byte("new-value"),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							"foo": fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]interface{}{"data": map[string]interface{}{"foo": "new-value"}}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretPropertyNoUpdateKV1": {
+			reason: "push secret with property only updates the property",
+			value:  []byte(fakeValue),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"foo": fakeValue,
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"PushSecretPropertyNoUpdateKV2": {
+			reason: "push secret with property only updates the property",
+			value:  []byte(fakeValue),
+			data:   &testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: "foo"},
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							"foo": fakeValue,
+						},
+						"custom_metadata": map[string]interface{}{
+							managedBy: managedByESO,
+						},
+					}, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextNoCall(),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SetSecretErrorReadingSecretKV1": {
+			reason: "error occurs if secret cannot be read",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, noPermission),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errReadSecret, noPermission),
+			},
+		},
+		"SetSecretErrorReadingSecretKV2": {
+			reason: "error occurs if secret cannot be read",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, noPermission),
+				},
+			},
+			want: want{
+				err: fmt.Errorf(errReadSecret, noPermission),
+			},
+		},
+		"SetSecretNotManagedByESOV1": {
+			reason: "a secret not managed by ESO cannot be updated",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						fakeKey: "fake-value2",
+						"custom_metadata": map[string]interface{}{
+							managedBy: "not-external-secrets",
+						},
+					}, nil),
+				},
+			},
+			want: want{
+				err: errors.New("secret not managed by external-secrets"),
+			},
+		},
+		"SetSecretNotManagedByESOV2": {
+			reason: "a secret not managed by ESO cannot be updated",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": map[string]interface{}{
+							fakeKey: "fake-value2",
+							"custom_metadata": map[string]interface{}{
+								managedBy: "not-external-secrets",
+							},
+						},
+					}, nil),
+				},
+			},
+			want: want{
+				err: errors.New("secret not managed by external-secrets"),
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			data := testingfake.PushSecretData{SecretKey: secretKey, RemoteKey: "secret", Property: ""}
+			if tc.data != nil {
+				data = *tc.data
+			}
+			client := &client{
+				logical: tc.args.vLogical,
+				store:   tc.args.store,
+			}
+			val := tc.value
+			if val == nil {
+				val = []byte(`{"fake-key":"fake-value"}`)
+			}
+			s := &corev1.Secret{Data: map[string][]byte{secretKey: val}}
+			err := client.PushSecret(context.Background(), s, data)
+
+			// Error nil XOR tc.want.err nil
+			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
+				t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+			}
+
+			// if errors are the same type but their contents do not match
+			if err != nil && tc.want.err != nil {
+				if !strings.Contains(err.Error(), tc.want.err.Error()) {
+					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+				}
+			}
+		})
+	}
+}

+ 6 - 9
pkg/provider/vault/fake/vault.go

@@ -269,18 +269,15 @@ func ClientWithLoginMock(_ *vault.Config) (util.Client, error) {
 		MockAuth:      NewVaultAuth(),
 		MockAuth:      NewVaultAuth(),
 		MockLogical:   NewVaultLogical(),
 		MockLogical:   NewVaultLogical(),
 	}
 	}
-	auth := cl.Auth()
-	token := cl.AuthToken()
-	logical := cl.Logical()
-	out := util.VClient{
+
+	return &util.VaultClient{
 		SetTokenFunc:     cl.SetToken,
 		SetTokenFunc:     cl.SetToken,
 		TokenFunc:        cl.Token,
 		TokenFunc:        cl.Token,
 		ClearTokenFunc:   cl.ClearToken,
 		ClearTokenFunc:   cl.ClearToken,
-		AuthField:        auth,
-		AuthTokenField:   token,
-		LogicalField:     logical,
+		AuthField:        cl.Auth(),
+		AuthTokenField:   cl.AuthToken(),
+		LogicalField:     cl.Logical(),
 		SetNamespaceFunc: cl.SetNamespace,
 		SetNamespaceFunc: cl.SetNamespace,
 		AddHeaderFunc:    cl.AddHeader,
 		AddHeaderFunc:    cl.AddHeader,
-	}
-	return out, nil
+	}, nil
 }
 }

+ 2 - 1
pkg/provider/vault/iamauth/iamauth.go

@@ -40,6 +40,7 @@ import (
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	awsutil "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 )
 )
@@ -81,7 +82,7 @@ func DefaultJWTProvider(name, namespace, roleArn string, aud []string, region st
 		Handlers:          handlers,
 		Handlers:          handlers,
 	})
 	})
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, awsutil.SanitizeErr(err)
 	}
 	}
 	tokenFetcher := &authTokenFetcher{
 	tokenFetcher := &authTokenFetcher{
 		Namespace:      namespace,
 		Namespace:      namespace,

+ 300 - 0
pkg/provider/vault/provider.go

@@ -0,0 +1,300 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	vault "github.com/hashicorp/vault/api"
+	"github.com/spf13/pflag"
+	"k8s.io/client-go/kubernetes"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/cache"
+	"github.com/external-secrets/external-secrets/pkg/feature"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+var (
+	_           esv1beta1.Provider = &Provider{}
+	enableCache bool
+	logger      = ctrl.Log.WithName("provider").WithName("vault")
+	clientCache *cache.Cache[util.Client]
+)
+
+const (
+	errVaultStore        = "received invalid Vault SecretStore resource: %w"
+	errVaultClient       = "cannot setup new vault client: %w"
+	errVaultCert         = "cannot set Vault CA certificate: %w"
+	errConfigMapFmt      = "cannot find config map data for key: %q"
+	errClientTLSAuth     = "error from Client TLS Auth: %q"
+	errUnknownCAProvider = "unknown caProvider type given"
+	errCANamespace       = "cannot read secret for CAProvider due to missing namespace on kind ClusterSecretStore"
+)
+
+type Provider struct {
+	// NewVaultClient is a function that returns a new Vault client.
+	// This is used for testing to inject a fake client.
+	NewVaultClient func(config *vault.Config) (util.Client, error)
+}
+
+// NewVaultClient returns a new Vault client.
+func NewVaultClient(config *vault.Config) (util.Client, error) {
+	vaultClient, err := vault.NewClient(config)
+	if err != nil {
+		return nil, err
+	}
+	return &util.VaultClient{
+		SetTokenFunc:     vaultClient.SetToken,
+		TokenFunc:        vaultClient.Token,
+		ClearTokenFunc:   vaultClient.ClearToken,
+		AuthField:        vaultClient.Auth(),
+		AuthTokenField:   vaultClient.Auth().Token(),
+		LogicalField:     vaultClient.Logical(),
+		SetNamespaceFunc: vaultClient.SetNamespace,
+		AddHeaderFunc:    vaultClient.AddHeader,
+	}, nil
+}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadWrite
+}
+
+// NewClient implements the Client interface.
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.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 fetch tokens
+	// (for Kubernetes service account token auth)
+	restCfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(restCfg)
+	if err != nil {
+		return nil, err
+	}
+	return p.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
+}
+
+func (p *Provider) NewGeneratorClient(ctx context.Context, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1beta1.VaultProvider, namespace string) (util.Client, error) {
+	vStore, cfg, err := p.prepareConfig(ctx, kube, corev1, vaultSpec, nil, namespace, resolvers.EmptyStoreKind)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := p.NewVaultClient(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = p.initClient(ctx, vStore, client, cfg, vaultSpec)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
+func (p *Provider) newClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, namespace string) (esv1beta1.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
+		return nil, errors.New(errVaultStore)
+	}
+	vaultSpec := storeSpec.Provider.Vault
+
+	vStore, cfg, err := p.prepareConfig(
+		ctx,
+		kube,
+		corev1,
+		vaultSpec,
+		storeSpec.RetrySettings,
+		namespace,
+		store.GetObjectKind().GroupVersionKind().Kind)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := getVaultClient(p, store, cfg)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultClient, err)
+	}
+
+	return p.initClient(ctx, vStore, client, cfg, vaultSpec)
+}
+
+func (p *Provider) initClient(ctx context.Context, c *client, client util.Client, cfg *vault.Config, vaultSpec *esv1beta1.VaultProvider) (esv1beta1.SecretsClient, error) {
+	if vaultSpec.Namespace != nil {
+		client.SetNamespace(*vaultSpec.Namespace)
+	}
+
+	if vaultSpec.ReadYourWrites && vaultSpec.ForwardInconsistent {
+		client.AddHeader("X-Vault-Inconsistent", "forward-active-node")
+	}
+	c.client = client
+	c.auth = client.Auth()
+	c.logical = client.Logical()
+	c.token = client.AuthToken()
+
+	// allow SecretStore controller validation to pass
+	// when using referent namespace.
+	if c.storeKind == esv1beta1.ClusterSecretStoreKind && c.namespace == "" && isReferentSpec(vaultSpec) {
+		return c, nil
+	}
+	if err := c.setAuth(ctx, cfg); err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+func (p *Provider) prepareConfig(ctx context.Context, kube kclient.Client, corev1 typedcorev1.CoreV1Interface, vaultSpec *esv1beta1.VaultProvider, retrySettings *esv1beta1.SecretStoreRetrySettings, namespace, storeKind string) (*client, *vault.Config, error) {
+	c := &client{
+		kube:      kube,
+		corev1:    corev1,
+		store:     vaultSpec,
+		log:       logger,
+		namespace: namespace,
+		storeKind: storeKind,
+	}
+
+	cfg, err := c.newConfig(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Setup retry options if present
+	if retrySettings != nil {
+		if retrySettings.MaxRetries != nil {
+			cfg.MaxRetries = int(*retrySettings.MaxRetries)
+		} else {
+			// By default we rely only on the reconciliation process for retrying
+			cfg.MaxRetries = 0
+		}
+
+		if retrySettings.RetryInterval != nil {
+			retryWait, err := time.ParseDuration(*retrySettings.RetryInterval)
+			if err != nil {
+				return nil, nil, err
+			}
+			cfg.MinRetryWait = retryWait
+			cfg.MaxRetryWait = retryWait
+		}
+	}
+
+	return c, cfg, nil
+}
+
+func getVaultClient(p *Provider, store esv1beta1.GenericStore, cfg *vault.Config) (util.Client, error) {
+	isStaticToken := store.GetSpec().Provider.Vault.Auth.TokenSecretRef != nil
+	useCache := enableCache && !isStaticToken
+
+	key := cache.Key{
+		Name:      store.GetObjectMeta().Name,
+		Namespace: store.GetObjectMeta().Namespace,
+		Kind:      store.GetTypeMeta().Kind,
+	}
+	if useCache {
+		client, ok := clientCache.Get(store.GetObjectMeta().ResourceVersion, key)
+		if ok {
+			return client, nil
+		}
+	}
+
+	client, err := p.NewVaultClient(cfg)
+	if err != nil {
+		return nil, fmt.Errorf(errVaultClient, err)
+	}
+
+	if useCache && !clientCache.Contains(key) {
+		clientCache.Add(store.GetObjectMeta().ResourceVersion, key, client)
+	}
+	return client, nil
+}
+
+func isReferentSpec(prov *esv1beta1.VaultProvider) bool {
+	if prov.Auth.TokenSecretRef != nil && prov.Auth.TokenSecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.AppRole != nil && prov.Auth.AppRole.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.SecretRef != nil && prov.Auth.Kubernetes.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Kubernetes != nil && prov.Auth.Kubernetes.ServiceAccountRef != nil && prov.Auth.Kubernetes.ServiceAccountRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Ldap != nil && prov.Auth.Ldap.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.UserPass != nil && prov.Auth.UserPass.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Jwt != nil && prov.Auth.Jwt.SecretRef != nil && prov.Auth.Jwt.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Jwt != nil && prov.Auth.Jwt.KubernetesServiceAccountToken != nil && prov.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Cert != nil && prov.Auth.Cert.SecretRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Iam != nil && prov.Auth.Iam.JWTAuth != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef != nil && prov.Auth.Iam.JWTAuth.ServiceAccountRef.Namespace == nil {
+		return true
+	}
+	if prov.Auth.Iam != nil && prov.Auth.Iam.SecretRef != nil &&
+		(prov.Auth.Iam.SecretRef.AccessKeyID.Namespace == nil ||
+			prov.Auth.Iam.SecretRef.SecretAccessKey.Namespace == nil ||
+			(prov.Auth.Iam.SecretRef.SessionToken != nil && prov.Auth.Iam.SecretRef.SessionToken.Namespace == nil)) {
+		return true
+	}
+	return false
+}
+
+func init() {
+	var vaultTokenCacheSize int
+	fs := pflag.NewFlagSet("vault", pflag.ExitOnError)
+	fs.BoolVar(&enableCache, "experimental-enable-vault-token-cache", false, "Enable experimental Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.")
+	// max. 265k vault leases with 30bytes each ~= 7MB
+	fs.IntVar(&vaultTokenCacheSize, "experimental-vault-token-cache-size", 2<<17, "Maximum size of Vault token cache. When more tokens than Only used if --experimental-enable-vault-token-cache is set.")
+	lateInit := func() {
+		logger.Info("initializing vault cache", "size", vaultTokenCacheSize)
+		clientCache = cache.Must(vaultTokenCacheSize, func(client util.Client) {
+			err := revokeTokenIfValid(context.Background(), client)
+			if err != nil {
+				logger.Error(err, "unable to revoke cached token on eviction")
+			}
+		})
+	}
+	feature.Register(feature.Feature{
+		Flags:      fs,
+		Initialize: lateInit,
+	})
+
+	esv1beta1.Register(&Provider{
+		NewVaultClient: NewVaultClient,
+	}, &esv1beta1.SecretStoreProvider{
+		Vault: &esv1beta1.VaultProvider{},
+	})
+}

File diff suppressed because it is too large
+ 709 - 0
pkg/provider/vault/provider_test.go


+ 9 - 9
pkg/provider/vault/util/vault.go

@@ -50,7 +50,7 @@ type Client interface {
 	AddHeader(key, value string)
 	AddHeader(key, value string)
 }
 }
 
 
-type VClient struct {
+type VaultClient struct {
 	SetTokenFunc     func(v string)
 	SetTokenFunc     func(v string)
 	TokenFunc        func() string
 	TokenFunc        func() string
 	ClearTokenFunc   func()
 	ClearTokenFunc   func()
@@ -61,34 +61,34 @@ type VClient struct {
 	AddHeaderFunc    func(key, value string)
 	AddHeaderFunc    func(key, value string)
 }
 }
 
 
-func (v VClient) AddHeader(key, value string) {
+func (v VaultClient) AddHeader(key, value string) {
 	v.AddHeaderFunc(key, value)
 	v.AddHeaderFunc(key, value)
 }
 }
 
 
-func (v VClient) SetNamespace(namespace string) {
+func (v VaultClient) SetNamespace(namespace string) {
 	v.SetNamespaceFunc(namespace)
 	v.SetNamespaceFunc(namespace)
 }
 }
 
 
-func (v VClient) ClearToken() {
+func (v VaultClient) ClearToken() {
 	v.ClearTokenFunc()
 	v.ClearTokenFunc()
 }
 }
 
 
-func (v VClient) Token() string {
+func (v VaultClient) Token() string {
 	return v.TokenFunc()
 	return v.TokenFunc()
 }
 }
 
 
-func (v VClient) SetToken(token string) {
+func (v VaultClient) SetToken(token string) {
 	v.SetTokenFunc(token)
 	v.SetTokenFunc(token)
 }
 }
 
 
-func (v VClient) Auth() Auth {
+func (v VaultClient) Auth() Auth {
 	return v.AuthField
 	return v.AuthField
 }
 }
 
 
-func (v VClient) AuthToken() Token {
+func (v VaultClient) AuthToken() Token {
 	return v.AuthTokenField
 	return v.AuthTokenField
 }
 }
 
 
-func (v VClient) Logical() Logical {
+func (v VaultClient) Logical() Logical {
 	return v.LogicalField
 	return v.LogicalField
 }
 }

+ 178 - 0
pkg/provider/vault/validate.go

@@ -0,0 +1,178 @@
+/*
+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 vault
+
+import (
+	"context"
+	"errors"
+	"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/utils"
+)
+
+const (
+	errInvalidCredentials     = "invalid vault credentials: %w"
+	errInvalidStore           = "invalid store"
+	errInvalidStoreSpec       = "invalid store spec"
+	errInvalidStoreProv       = "invalid store provider"
+	errInvalidVaultProv       = "invalid vault provider"
+	errInvalidAppRoleRef      = "invalid Auth.AppRole.RoleRef: %w"
+	errInvalidAppRoleSec      = "invalid Auth.AppRole.SecretRef: %w"
+	errInvalidClientCert      = "invalid Auth.Cert.ClientCert: %w"
+	errInvalidCertSec         = "invalid Auth.Cert.SecretRef: %w"
+	errInvalidJwtSec          = "invalid Auth.Jwt.SecretRef: %w"
+	errInvalidJwtK8sSA        = "invalid Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef: %w"
+	errInvalidKubeSA          = "invalid Auth.Kubernetes.ServiceAccountRef: %w"
+	errInvalidKubeSec         = "invalid Auth.Kubernetes.SecretRef: %w"
+	errInvalidLdapSec         = "invalid Auth.Ldap.SecretRef: %w"
+	errInvalidTokenRef        = "invalid Auth.TokenSecretRef: %w"
+	errInvalidUserPassSec     = "invalid Auth.UserPass.SecretRef: %w"
+	errInvalidClientTLSCert   = "invalid ClientTLS.ClientCert: %w"
+	errInvalidClientTLSSecret = "invalid ClientTLS.SecretRef: %w"
+	errInvalidClientTLS       = "when provided, both ClientTLS.ClientCert and ClientTLS.SecretRef should be provided"
+)
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	if store == nil {
+		return nil, fmt.Errorf(errInvalidStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return nil, fmt.Errorf(errInvalidStoreSpec)
+	}
+	if spc.Provider == nil {
+		return nil, fmt.Errorf(errInvalidStoreProv)
+	}
+	vaultProvider := spc.Provider.Vault
+	if vaultProvider == nil {
+		return nil, fmt.Errorf(errInvalidVaultProv)
+	}
+	if vaultProvider.Auth.AppRole != nil {
+		// check SecretRef for valid configuration
+		if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.AppRole.SecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidAppRoleSec, err)
+		}
+
+		// prefer .auth.appRole.roleId, fallback to .auth.appRole.roleRef, give up after that.
+		if vaultProvider.Auth.AppRole.RoleID == "" { // prevents further RoleID tests if .auth.appRole.roleId is given
+			if vaultProvider.Auth.AppRole.RoleRef != nil { // check RoleRef for valid configuration
+				if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.Auth.AppRole.RoleRef); err != nil {
+					return nil, fmt.Errorf(errInvalidAppRoleRef, err)
+				}
+			} else { // we ran out of ways to get RoleID. return an appropriate error
+				return nil, fmt.Errorf(errInvalidAppRoleID)
+			}
+		}
+	}
+	if vaultProvider.Auth.Cert != nil {
+		if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.Cert.ClientCert); err != nil {
+			return nil, fmt.Errorf(errInvalidClientCert, err)
+		}
+		if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.Cert.SecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidCertSec, err)
+		}
+	}
+	if vaultProvider.Auth.Jwt != nil {
+		if vaultProvider.Auth.Jwt.SecretRef != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.Auth.Jwt.SecretRef); err != nil {
+				return nil, fmt.Errorf(errInvalidJwtSec, err)
+			}
+		} else if vaultProvider.Auth.Jwt.KubernetesServiceAccountToken != nil {
+			if err := utils.ValidateReferentServiceAccountSelector(store, vaultProvider.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef); err != nil {
+				return nil, fmt.Errorf(errInvalidJwtK8sSA, err)
+			}
+		} else {
+			return nil, fmt.Errorf(errJwtNoTokenSource)
+		}
+	}
+	if vaultProvider.Auth.Kubernetes != nil {
+		if vaultProvider.Auth.Kubernetes.ServiceAccountRef != nil {
+			if err := utils.ValidateReferentServiceAccountSelector(store, *vaultProvider.Auth.Kubernetes.ServiceAccountRef); err != nil {
+				return nil, fmt.Errorf(errInvalidKubeSA, err)
+			}
+		}
+		if vaultProvider.Auth.Kubernetes.SecretRef != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.Auth.Kubernetes.SecretRef); err != nil {
+				return nil, fmt.Errorf(errInvalidKubeSec, err)
+			}
+		}
+	}
+	if vaultProvider.Auth.Ldap != nil {
+		if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.Ldap.SecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidLdapSec, err)
+		}
+	}
+	if vaultProvider.Auth.UserPass != nil {
+		if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.UserPass.SecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidUserPassSec, err)
+		}
+	}
+	if vaultProvider.Auth.TokenSecretRef != nil {
+		if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.Auth.TokenSecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidTokenRef, err)
+		}
+	}
+	if vaultProvider.Auth.Iam != nil {
+		if vaultProvider.Auth.Iam.JWTAuth != nil {
+			if vaultProvider.Auth.Iam.JWTAuth.ServiceAccountRef != nil {
+				if err := utils.ValidateReferentServiceAccountSelector(store, *vaultProvider.Auth.Iam.JWTAuth.ServiceAccountRef); err != nil {
+					return nil, fmt.Errorf(errInvalidTokenRef, err)
+				}
+			}
+		}
+
+		if vaultProvider.Auth.Iam.SecretRef != nil {
+			if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.Iam.SecretRef.AccessKeyID); err != nil {
+				return nil, fmt.Errorf(errInvalidTokenRef, err)
+			}
+			if err := utils.ValidateReferentSecretSelector(store, vaultProvider.Auth.Iam.SecretRef.SecretAccessKey); err != nil {
+				return nil, fmt.Errorf(errInvalidTokenRef, err)
+			}
+			if vaultProvider.Auth.Iam.SecretRef.SessionToken != nil {
+				if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.Auth.Iam.SecretRef.SessionToken); err != nil {
+					return nil, fmt.Errorf(errInvalidTokenRef, err)
+				}
+			}
+		}
+	}
+	if vaultProvider.ClientTLS.CertSecretRef != nil && vaultProvider.ClientTLS.KeySecretRef != nil {
+		if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.ClientTLS.CertSecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidClientTLSCert, err)
+		}
+		if err := utils.ValidateReferentSecretSelector(store, *vaultProvider.ClientTLS.KeySecretRef); err != nil {
+			return nil, fmt.Errorf(errInvalidClientTLSSecret, err)
+		}
+	} else if vaultProvider.ClientTLS.CertSecretRef != nil || vaultProvider.ClientTLS.KeySecretRef != nil {
+		return nil, errors.New(errInvalidClientTLS)
+	}
+	return nil, nil
+}
+
+func (c *client) Validate() (esv1beta1.ValidationResult, error) {
+	// when using referent namespace we can not validate the token
+	// because the namespace is not known yet when Validate() is called
+	// from the SecretStore controller.
+	if c.storeKind == esv1beta1.ClusterSecretStoreKind && isReferentSpec(c.store) {
+		return esv1beta1.ValidationResultUnknown, nil
+	}
+	_, err := checkToken(context.Background(), c.token)
+	if err != nil {
+		return esv1beta1.ValidationResultError, fmt.Errorf(errInvalidCredentials, err)
+	}
+	return esv1beta1.ValidationResultReady, nil
+}

+ 272 - 0
pkg/provider/vault/validate_test.go

@@ -0,0 +1,272 @@
+/*
+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 vault
+
+import (
+	"testing"
+
+	pointer "k8s.io/utils/ptr"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const fakeValidationValue = "fake-value"
+
+func TestValidateStore(t *testing.T) {
+	type args struct {
+		auth      esv1beta1.VaultAuth
+		clientTLS esv1beta1.VaultClientTLS
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "empty auth",
+			args: args{},
+		},
+
+		{
+			name: "invalid approle with namespace",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid approle with roleId and no roleRef",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleID:  "",
+						RoleRef: nil,
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid approle with roleId and no roleRef",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleID: fakeValidationValue,
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid approle with roleId and no roleRef",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalid clientcert",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Cert: &esv1beta1.VaultCertAuth{
+						ClientCert: esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid cert secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Cert: &esv1beta1.VaultCertAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid jwt secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Jwt: &esv1beta1.VaultJwtAuth{
+						SecretRef: &esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid kubernetes sa",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Kubernetes: &esv1beta1.VaultKubernetesAuth{
+						ServiceAccountRef: &esmeta.ServiceAccountSelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid kubernetes secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Kubernetes: &esv1beta1.VaultKubernetesAuth{
+						SecretRef: &esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid ldap secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					Ldap: &esv1beta1.VaultLdapAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid userpass secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					UserPass: &esv1beta1.VaultUserPassAuth{
+						SecretRef: esmeta.SecretKeySelector{
+							Namespace: pointer.To("invalid"),
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid token secret",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					TokenSecretRef: &esmeta.SecretKeySelector{
+						Namespace: pointer.To("invalid"),
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid clientTls config",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				clientTLS: esv1beta1.VaultClientTLS{
+					CertSecretRef: &esmeta.SecretKeySelector{
+						Name: "tls-auth-certs",
+					},
+					KeySecretRef: &esmeta.SecretKeySelector{
+						Name: "tls-auth-certs",
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalid clientTls config, missing SecretRef",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				clientTLS: esv1beta1.VaultClientTLS{
+					CertSecretRef: &esmeta.SecretKeySelector{
+						Name: "tls-auth-certs",
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid clientTls config, missing ClientCert",
+			args: args{
+				auth: esv1beta1.VaultAuth{
+					AppRole: &esv1beta1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				clientTLS: esv1beta1.VaultClientTLS{
+					KeySecretRef: &esmeta.SecretKeySelector{
+						Name: "tls-auth-certs",
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Provider{
+				NewVaultClient: nil,
+			}
+			store := &esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Vault: &esv1beta1.VaultProvider{
+							Auth:      tt.args.auth,
+							ClientTLS: tt.args.clientTLS,
+						},
+					},
+				},
+			}
+			if _, err := c.ValidateStore(store); (err != nil) != tt.wantErr {
+				t.Errorf("connector.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}

File diff suppressed because it is too large
+ 0 - 1801
pkg/provider/vault/vault.go


File diff suppressed because it is too large
+ 0 - 2463
pkg/provider/vault/vault_test.go