Repository: external-secrets/external-secrets (go)

Issue:
Support pushing the whole secret for the Vault provider
**Is your feature request related to a problem? Please describe.**
Even though pushing the whole secret is generically documented as supported for the `PushSecret` resource [here](https://external-secrets.io/latest/guides/pushsecrets/#pushing-the-whole-secret), it is still up to the provider to support and proper implement it, talking specifically about the Vault provider, that provider is not yet supporting this feature and if you try to create a PushSecret object by leaving off the secret key and remote property options, the controller will fail with a generic json marshal error.

**Describe the solution you'd like**
I would like to use the PushSecret object, as described in the "Pushing the whole secret" documentation, when pushing the whole secret to Hashicorp Vault.

**Describe alternatives you've considered**
For now it only works if I whitelist all the secret keys I want to push, but for larger secrets, that is too much work and won't scale.



Pull request:
fix: allow pushing the whole secret to Vault
## Problem Statement

This PR is adding support to pushing the whole secret to the Hashicorp Vault provider.

## Related Issue

Fixes #3287

## Proposed Changes

Proper treating when the `secretKey` is missing and therefore making the whole secret available for pushing.

## Checklist

- [x] I have read the [contribution guidelines](https://external-secrets.io/latest/contributing/process/#submitting-a-pull-request)
- [x] All commits are signed with `git commit --signoff`
- [x] My changes have reasonable test coverage
- [x] All tests pass with `make test`
- [x] I ensured my PR is ready for review with `make reviewable`


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3e9d87550..7de3c58fe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -116,7 +116,7 @@ jobs:
         run: go mod download
 
       - name: Cache envtest binaries
-        uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
+        uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
         with:
           path: bin/k8s
           key: ${{ runner.os }}-envtest-${{env.KUBERNETES_VERSION}}
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 86f49d314..490fdb083 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -33,6 +33,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7
+        uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
         with:
           sarif_file: results.sarif
diff --git a/apis/externalsecrets/v1beta1/secretstore_vault_types.go b/apis/externalsecrets/v1beta1/secretstore_vault_types.go
index 11a623cf5..d221001d0 100644
--- a/apis/externalsecrets/v1beta1/secretstore_vault_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_vault_types.go
@@ -104,8 +104,16 @@ type VaultClientTLS struct {
 
 // VaultAuth is the configuration used to authenticate with a Vault server.
 // Only one of `tokenSecretRef`, `appRole`,  `kubernetes`, `ldap`, `userPass`, `jwt` or `cert`
-// can be specified.
+// can be specified. A namespace to authenticate against can optionally be specified.
 type VaultAuth struct {
+	// Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+	// Namespaces is a set of features within Vault Enterprise that allows
+	// Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+	// More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+	// This will default to Vault.Namespace field if set, or empty otherwise
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+
 	// TokenSecretRef authenticates with Vault by presenting a token.
 	// +optional
 	TokenSecretRef *esmeta.SecretKeySelector `json:"tokenSecretRef,omitempty"`
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index f5714a3d1..7e52edd11 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -2569,6 +2569,11 @@ func (in *VaultAppRole) DeepCopy() *VaultAppRole {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VaultAuth) DeepCopyInto(out *VaultAuth) {
 	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
 	if in.TokenSecretRef != nil {
 		in, out := &in.TokenSecretRef, &out.TokenSecretRef
 		*out = new(metav1.SecretKeySelector)
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index 6fa7625a4..60941eeb5 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -3918,6 +3918,14 @@ spec:
                             - path
                             - username
                             type: object
+                          namespace:
+                            description: |-
+                              Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                              Namespaces is a set of features within Vault Enterprise that allows
+                              Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                              More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                              This will default to Vault.Namespace field if set, or empty otherwise
+                            type: string
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with Vault by
                               presenting a token.
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index 51e019fe8..dafc738f2 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -3918,6 +3918,14 @@ spec:
                             - path
                             - username
                             type: object
+                          namespace:
+                            description: |-
+                              Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                              Namespaces is a set of features within Vault Enterprise that allows
+                              Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                              More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                              This will default to Vault.Namespace field if set, or empty otherwise
+                            type: string
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with Vault by
                               presenting a token.
diff --git a/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml b/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml
index 6d2965607..6fccb81eb 100644
--- a/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml
+++ b/config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml
@@ -491,6 +491,14 @@ spec:
                         - path
                         - username
                         type: object
+                      namespace:
+                        description: |-
+                          Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                          Namespaces is a set of features within Vault Enterprise that allows
+                          Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                          More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                          This will default to Vault.Namespace field if set, or empty otherwise
+                        type: string
                       tokenSecretRef:
                         description: TokenSecretRef authenticates with Vault by presenting
                           a token.
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index 083b3a4fc..06c63ab8b 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -4247,6 +4247,14 @@ spec:
                                 - path
                                 - username
                               type: object
+                            namespace:
+                              description: |-
+                                Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                                Namespaces is a set of features within Vault Enterprise that allows
+                                Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                                More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                                This will default to Vault.Namespace field if set, or empty otherwise
+                              type: string
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with Vault by presenting a token.
                               properties:
@@ -9495,6 +9503,14 @@ spec:
                                 - path
                                 - username
                               type: object
+                            namespace:
+                              description: |-
+                                Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                                Namespaces is a set of features within Vault Enterprise that allows
+                                Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                                More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                                This will default to Vault.Namespace field if set, or empty otherwise
+                              type: string
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with Vault by presenting a token.
                               properties:
@@ -11062,6 +11078,14 @@ spec:
                             - path
                             - username
                           type: object
+                        namespace:
+                          description: |-
+                            Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                            Namespaces is a set of features within Vault Enterprise that allows
+                            Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                            More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                            This will default to Vault.Namespace field if set, or empty otherwise
+                          type: string
                         tokenSecretRef:
                           description: TokenSecretRef authenticates with Vault by presenting a token.
                           properties:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 87870fdeb..8d5e28ca0 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -6876,7 +6876,7 @@ resource is used as the app role secret.</p>
 <p>
 <p>VaultAuth is the configuration used to authenticate with a Vault server.
 Only one of <code>tokenSecretRef</code>, <code>appRole</code>,  <code>kubernetes</code>, <code>ldap</code>, <code>userPass</code>, <code>jwt</code> or <code>cert</code>
-can be specified.</p>
+can be specified. A namespace to authenticate against can optionally be specified.</p>
 </p>
 <table>
 <thead>
@@ -6888,6 +6888,22 @@ can be specified.</p>
 <tbody>
 <tr>
 <td>
+<code>namespace</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+Namespaces is a set of features within Vault Enterprise that allows
+Vault environments to support Secure Multi-tenancy. e.g: &ldquo;ns1&rdquo;.
+More about namespaces can be found here <a href="https://www.vaultproject.io/docs/enterprise/namespaces">https://www.vaultproject.io/docs/enterprise/namespaces</a>
+This will default to Vault.Namespace field if set, or empty otherwise</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>tokenSecretRef</code></br>
 <em>
 <a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
diff --git a/docs/provider/hashicorp-vault.md b/docs/provider/hashicorp-vault.md
index a48a2a188..27c271c32 100644
--- a/docs/provider/hashicorp-vault.md
+++ b/docs/provider/hashicorp-vault.md
@@ -278,6 +278,8 @@ We support five different modes for authentication:
 [tlsCert](https://developer.hashicorp.com/vault/docs/auth/cert), each one comes with it's own
 trade-offs. Depending on the authentication method you need to adapt your environment.
 
+If you're using Vault namespaces, you can authenticate into one namespace and use the vault token against a different namespace, if desired.
+
 #### Token-based authentication
 
 A static token is stored in a `Kind=Secret` and is used to authenticate with vault.
@@ -461,6 +463,28 @@ spec:
         # ...
 ```
 
+##### Authenticating into a different namespace
+
+In some situations your authentication backend may be in one namespace, and your secrets in another. You can authenticate into one namespace, and use that token against another, by setting `provider.vault.namespace` and `provider.vault.auth.namespace` to different values. If `provider.vault.auth.namespace` is unset but `provider.vault.namespace` is, it will default to the `provider.vault.namespace` value.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: vault-backend
+spec:
+  provider:
+    vault:
+      server: "http://my.vault.server:8200"
+      # See https://www.vaultproject.io/docs/enterprise/namespaces
+      namespace: "app-team"
+      path: "secret"
+      version: "v2"
+      auth:
+        namespace: "kubernetes-team"
+        # ...
+```
+
 #### Read Your Writes
 
 Vault 1.10.0 and later encodes information in the token to detect the case
diff --git a/hack/api-docs/requirements.txt b/hack/api-docs/requirements.txt
index 7af09d0ee..0fe35a943 100644
--- a/hack/api-docs/requirements.txt
+++ b/hack/api-docs/requirements.txt
@@ -7,8 +7,8 @@ csscompressor==0.9.5
 ghp-import==2.1.0
 htmlmin==0.1.12
 idna==3.6
-importlib-metadata==7.0.2
-importlib-resources==6.3.1
+importlib-metadata==7.1.0
+importlib-resources==6.4.0
 Jinja2==3.1.3
 jsmin==3.0.1
 livereload==2.6.3
@@ -18,7 +18,7 @@ mergedeep==1.3.4
 mike @ git+https://github.com/jimporter/mike@300593c338b18f61f604d18457c351e166318020
 mkdocs==1.5.3
 mkdocs-macros-plugin==1.0.5
-mkdocs-material==9.5.14
+mkdocs-material==9.5.15
 mkdocs-material-extensions==1.3.1
 mkdocs-minify-plugin==0.8.0
 packaging==24.0
diff --git a/pkg/provider/vault/auth.go b/pkg/provider/vault/auth.go
index ec1f0529e..6f06e17fa 100644
--- a/pkg/provider/vault/auth.go
+++ b/pkg/provider/vault/auth.go
@@ -42,6 +42,10 @@ const (
 // 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 {
+	// Switch to auth namespace if different from the provider namespace
+	restoreNamespace := c.useAuthNamespace(ctx)
+	defer restoreNamespace()
+
 	tokenExists := false
 	var err error
 	if c.client.Token() != "" {
@@ -169,3 +173,27 @@ func revokeTokenIfValid(ctx context.Context, client util.Client) error {
 	}
 	return nil
 }
+
+func (c *client) useAuthNamespace(_ context.Context) func() {
+	ns := ""
+	if c.store.Namespace != nil {
+		ns = *c.store.Namespace
+	}
+
+	if c.store.Auth.Namespace != nil {
+		// Different Auth Vault Namespace than Secret Vault Namespace
+		// Switch namespaces then switch back at the end
+		if c.store.Auth.Namespace != nil && *c.store.Auth.Namespace != ns {
+			c.log.V(1).Info("Using namespace=%s for the vault login", *c.store.Auth.Namespace)
+			c.client.SetNamespace(*c.store.Auth.Namespace)
+			// use this as a defer to reset the namespace
+			return func() {
+				c.log.V(1).Info("Restoring client namespace to namespace=%s", ns)
+				c.client.SetNamespace(ns)
+			}
+		}
+	}
+
+	// no-op
+	return func() {}
+}
diff --git a/pkg/provider/vault/client_push.go b/pkg/provider/vault/client_push.go
index 960571228..b4bb60f0d 100644
--- a/pkg/provider/vault/client_push.go
+++ b/pkg/provider/vault/client_push.go
@@ -26,10 +26,28 @@ import (
 	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"
 )
 
 func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
-	value := secret.Data[data.GetSecretKey()]
+	var (
+		value []byte
+		err   error
+	)
+	key := data.GetSecretKey()
+	if key == "" {
+		// Must convert secret values to string, otherwise data will be sent as base64 to Vault
+		secretStringVal := make(map[string]string)
+		for k, v := range secret.Data {
+			secretStringVal[k] = string(v)
+		}
+		value, err = utils.JSONMarshal(secretStringVal)
+		if err != nil {
+			return fmt.Errorf("failed to serialize secret content as JSON: %w", err)
+		}
+	} else {
+		value = secret.Data[key]
+	}
 	label := map[string]interface{}{
 		"custom_metadata": map[string]string{
 			"managed-by": "external-secrets",
diff --git a/pkg/provider/vault/fake/vault.go b/pkg/provider/vault/fake/vault.go
index 74b506901..96fabf97c 100644
--- a/pkg/provider/vault/fake/vault.go
+++ b/pkg/provider/vault/fake/vault.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"reflect"
 	"strings"
+	"sync"
 
 	vault "github.com/hashicorp/vault/api"
 
@@ -155,6 +156,8 @@ type MockTokenFn func() string
 
 type MockClearTokenFn func()
 
+type MockNamespaceFn func() string
+
 type MockSetNamespaceFn func(namespace string)
 
 type MockAddHeaderFn func(key, value string)
@@ -188,10 +191,6 @@ func NewClearTokenFn() MockClearTokenFn {
 	return func() {}
 }
 
-func NewSetNamespaceFn() MockSetNamespaceFn {
-	return func(namespace string) {}
-}
-
 func NewAddHeaderFn() MockAddHeaderFn {
 	return func(key, value string) {}
 }
@@ -203,8 +202,12 @@ type VaultClient struct {
 	MockSetToken     MockSetTokenFn
 	MockToken        MockTokenFn
 	MockClearToken   MockClearTokenFn
+	MockNamespace    MockNamespaceFn
 	MockSetNamespace MockSetNamespaceFn
 	MockAddHeader    MockAddHeaderFn
+
+	namespace string
+	lock      sync.RWMutex
 }
 
 func (c *VaultClient) Logical() Logical {
@@ -253,8 +256,17 @@ func (c *VaultClient) ClearToken() {
 	c.MockClearToken()
 }
 
+func (c *VaultClient) Namespace() string {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
+	ns := c.namespace
+	return ns
+}
+
 func (c *VaultClient) SetNamespace(namespace string) {
-	c.MockSetNamespace(namespace)
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	c.namespace = namespace
 }
 
 func (c *VaultClient) AddHeader(key, value string) {
@@ -277,6 +289,7 @@ func ClientWithLoginMock(_ *vault.Config) (util.Client, error) {
 		AuthField:        cl.Auth(),
 		AuthTokenField:   cl.AuthToken(),
 		LogicalField:     cl.Logical(),
+		NamespaceFunc:    cl.Namespace,
 		SetNamespaceFunc: cl.SetNamespace,
 		AddHeaderFunc:    cl.AddHeader,
 	}, nil
diff --git a/pkg/provider/vault/provider.go b/pkg/provider/vault/provider.go
index fa9182bdd..df97f6e06 100644
--- a/pkg/provider/vault/provider.go
+++ b/pkg/provider/vault/provider.go
@@ -71,6 +71,7 @@ func NewVaultClient(config *vault.Config) (util.Client, error) {
 		AuthField:        vaultClient.Auth(),
 		AuthTokenField:   vaultClient.Auth().Token(),
 		LogicalField:     vaultClient.Logical(),
+		NamespaceFunc:    vaultClient.Namespace,
 		SetNamespaceFunc: vaultClient.SetNamespace,
 		AddHeaderFunc:    vaultClient.AddHeader,
 	}, nil
diff --git a/pkg/provider/vault/util/vault.go b/pkg/provider/vault/util/vault.go
index a82016ff1..a53b211d4 100644
--- a/pkg/provider/vault/util/vault.go
+++ b/pkg/provider/vault/util/vault.go
@@ -46,6 +46,7 @@ type Client interface {
 	Auth() Auth
 	Logical() Logical
 	AuthToken() Token
+	Namespace() string
 	SetNamespace(namespace string)
 	AddHeader(key, value string)
 }
@@ -57,6 +58,7 @@ type VaultClient struct {
 	AuthField        Auth
 	LogicalField     Logical
 	AuthTokenField   Token
+	NamespaceFunc    func() string
 	SetNamespaceFunc func(namespace string)
 	AddHeaderFunc    func(key, value string)
 }
@@ -65,6 +67,10 @@ func (v VaultClient) AddHeader(key, value string) {
 	v.AddHeaderFunc(key, value)
 }
 
+func (v VaultClient) Namespace() string {
+	return v.NamespaceFunc()
+}
+
 func (v VaultClient) SetNamespace(namespace string) {
 	v.SetNamespaceFunc(namespace)
 }


Test patch:
diff --git a/pkg/provider/vault/auth_test.go b/pkg/provider/vault/auth_test.go
new file mode 100644
index 000000000..823d260df
--- /dev/null
+++ b/pkg/provider/vault/auth_test.go
@@ -0,0 +1,167 @@
+/*
+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"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+)
+
+// Test Vault Namespace logic.
+func TestSetAuthNamespace(t *testing.T) {
+	store := makeValidSecretStore()
+
+	kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "vault-secret",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"key": []byte("token"),
+		},
+	}).Build()
+
+	store.Spec.Provider.Vault.Auth.Kubernetes.ServiceAccountRef = nil
+	store.Spec.Provider.Vault.Auth.Kubernetes.SecretRef = &esmeta.SecretKeySelector{
+		Name:      "vault-secret",
+		Namespace: ptr.To("default"),
+		Key:       "key",
+	}
+
+	adminNS := "admin"
+	teamNS := "admin/team-a"
+
+	type result struct {
+		Before string
+		During string
+		After  string
+	}
+
+	type args struct {
+		store    *esv1beta1.SecretStore
+		expected result
+	}
+	cases := map[string]struct {
+		reason string
+		args   args
+	}{
+		"StoreNoNamespace": {
+			reason: "no namespace should ever be set",
+			args: args{
+				store:    store,
+				expected: result{Before: "", During: "", After: ""},
+			},
+		},
+		"StoreWithNamespace": {
+			reason: "use the team namespace throughout",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
+					return s
+				}(store),
+				expected: result{Before: teamNS, During: teamNS, After: teamNS},
+			},
+		},
+		"StoreWithAuthNamespace": {
+			reason: "switch to the auth namespace during login then revert",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: "", During: adminNS, After: ""},
+			},
+		},
+		"StoreWithSameNamespace": {
+			reason: "the admin namespace throughout",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(adminNS)
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: adminNS, During: adminNS, After: adminNS},
+			},
+		},
+		"StoreWithDistinctNamespace": {
+			reason: "switch from team namespace, to admin, then back",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: teamNS, During: adminNS, After: teamNS},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			prov := &Provider{
+				NewVaultClient: fake.ClientWithLoginMock,
+			}
+
+			c, cfg, err := prov.prepareConfig(context.Background(), kube, nil, tc.args.store.Spec.Provider.Vault, nil, "default", store.GetObjectKind().GroupVersionKind().Kind)
+			if err != nil {
+				t.Errorf(err.Error())
+			}
+
+			client, err := getVaultClient(prov, tc.args.store, cfg)
+			if err != nil {
+				t.Errorf("vault.useAuthNamespace: failed to create client: %s", err.Error())
+			}
+
+			_, err = prov.initClient(context.Background(), c, client, cfg, tc.args.store.Spec.Provider.Vault)
+			if err != nil {
+				t.Errorf("vault.useAuthNamespace: failed to init client: %s", err.Error())
+			}
+
+			c.client = client
+
+			// before auth
+			actual := result{
+				Before: c.client.Namespace(),
+			}
+
+			// during authentication (getting a token)
+			resetNS := c.useAuthNamespace(context.Background())
+			actual.During = c.client.Namespace()
+			resetNS()
+
+			// after getting the token
+			actual.After = c.client.Namespace()
+
+			if diff := cmp.Diff(tc.args.expected, actual, cmpopts.EquateComparable()); diff != "" {
+				t.Errorf("\n%s\nvault.useAuthNamepsace(...): -want namespace, +got namespace:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
diff --git a/pkg/provider/vault/client_push_test.go b/pkg/provider/vault/client_push_test.go
index f4ad9eae0..da8e0acba 100644
--- a/pkg/provider/vault/client_push_test.go
+++ b/pkg/provider/vault/client_push_test.go
@@ -362,6 +362,7 @@ func TestPushSecret(t *testing.T) {
 		want   want
 		data   *testingfake.PushSecretData
 		value  []byte
+		secret *corev1.Secret
 	}{
 		"SetSecretKV1": {
 			reason: "secret is successfully set, with no existing vault secret",
@@ -648,6 +649,21 @@ func TestPushSecret(t *testing.T) {
 				err: errors.New("secret not managed by external-secrets"),
 			},
 		},
+		"WholeSecretKV2": {
+			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.ExpectWriteWithContextValue(map[string]interface{}{"data": map[string]interface{}{"key1": "value1", "key2": "value2"}}),
+				},
+			},
+			data:   &testingfake.PushSecretData{SecretKey: "", RemoteKey: "secret", Property: ""},
+			secret: &corev1.Secret{Data: map[string][]byte{"key1": []byte(`value1`), "key2": []byte(`value2`)}},
+			want: want{
+				err: nil,
+			},
+		},
 	}
 
 	for name, tc := range tests {
@@ -660,11 +676,14 @@ func TestPushSecret(t *testing.T) {
 				logical: tc.args.vLogical,
 				store:   tc.args.store,
 			}
-			val := tc.value
-			if val == nil {
-				val = []byte(`{"fake-key":"fake-value"}`)
+			s := tc.secret
+			if s == nil {
+				val := tc.value
+				if val == nil {
+					val = []byte(`{"fake-key":"fake-value"}`)
+				}
+				s = &corev1.Secret{Data: map[string][]byte{secretKey: val}}
 			}
-			s := &corev1.Secret{Data: map[string][]byte{secretKey: val}}
 			err := client.PushSecret(context.Background(), s, data)
 
 			// Error nil XOR tc.want.err nil