Browse Source

feat(vault): marshal nested value as json, add docs

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
Moritz Johner 4 years ago
parent
commit
5b8ab034ec

+ 85 - 2
docs/provider-hashicorp-vault.md

@@ -71,9 +71,92 @@ data:
   foobar: czNjcjN0
   foobar: czNjcjN0
 ```
 ```
 
 
-#### Limitations
+#### Fetching Raw Values
 
 
-Vault supports only simple key/value pairs - nested objects are not supported. Hence specifying `gjson` properties like other providers support it is not supported.
+You can fetch all key/value pairs for a given path If you leave the `remoteRef.property` empty. This returns the json-encoded secret value for that path.
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: vault-example
+spec:
+  # ...
+  data:
+  - secretKey: foobar
+    remoteRef:
+      key: /dev/package.json
+```
+
+#### Nested Values
+
+Vault supports nested key/value pairs. You can specify a [gjson](https://github.com/tidwall/gjson) expression at `remoteRef.property` to get a nested value.
+
+Given the following secret - assume its path is `/dev/config`:
+```json
+{
+  "foo": {
+    "nested": {
+      "bar": "mysecret"
+    }
+  }
+}
+```
+
+You can set the `remoteRef.property` to point to the nested key using a [gjson](https://github.com/tidwall/gjson) expression.
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: vault-example
+spec:
+  # ...
+  data:
+  - secretKey: foobar
+    remoteRef:
+      key: /dev/config
+      property: foo.nested.bar
+---
+# creates a secret with:
+# foobar=mysecret
+```
+
+If you would set the `remoteRef.property` to just `foo` then you would get the json-encoded value of that property: `{"nested":{"bar":"mysecret"}}`.
+
+#### Multiple nested Values
+
+You can extract multiple keys from a nested secret using `dataFrom`.
+
+Given the following secret - assume its path is `/dev/config`:
+```json
+{
+  "foo": {
+    "nested": {
+      "bar": "mysecret",
+      "baz": "bang"
+    }
+  }
+}
+```
+
+You can set the `remoteRef.property` to point to the nested key using a [gjson](https://github.com/tidwall/gjson) expression.
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: vault-example
+spec:
+  # ...
+  dataFrom:
+  - key: /dev/config
+    property: foo.nested
+```
+
+That results in a secret with these values:
+```
+bar=mysecret
+baz=bang
+```
 
 
 ### Authentication
 ### Authentication
 
 

+ 97 - 0
e2e/suite/vault/vault.go

@@ -13,10 +13,13 @@ limitations under the License.
 package vault
 package vault
 
 
 import (
 import (
+	"fmt"
 
 
 	// nolint
 	// nolint
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/ginkgo/v2"
+	v1 "k8s.io/api/core/v1"
 
 
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/e2e/framework"
 	"github.com/external-secrets/external-secrets/e2e/framework"
 	"github.com/external-secrets/external-secrets/e2e/suite/common"
 	"github.com/external-secrets/external-secrets/e2e/suite/common"
 )
 )
@@ -72,6 +75,11 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithoutTargetName, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithoutTargetName, useKubernetesProvider),
+		// vault-specific test cases
+		Entry("secret value via data without property should return json-encoded string", Label("json"), testJSONWithoutProperty),
+		Entry("secret value via data with property should return json-encoded string", Label("json"), testJSONWithProperty),
+		Entry("dataFrom without property should extract key/value pairs", Label("json"), testDataFromJSONWithoutProperty),
+		Entry("dataFrom with property should extract key/value pairs", Label("json"), testDataFromJSONWithProperty),
 	)
 	)
 })
 })
 
 
@@ -98,3 +106,92 @@ func useJWTProvider(tc *framework.TestCase) {
 func useKubernetesProvider(tc *framework.TestCase) {
 func useKubernetesProvider(tc *framework.TestCase) {
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = kubernetesProviderName
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = kubernetesProviderName
 }
 }
+
+const jsonVal = `{"foo":{"nested":{"bar":"mysecret","baz":"bang"}}}`
+
+// when no property is set it should return the json-encoded at path.
+func testJSONWithoutProperty(tc *framework.TestCase) {
+	secretKey := fmt.Sprintf("%s-%s", tc.Framework.Namespace.Name, "json")
+	tc.Secrets = map[string]string{
+		secretKey: jsonVal,
+	}
+	tc.ExpectedSecret = &v1.Secret{
+		Type: v1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			secretKey: []byte(jsonVal),
+		},
+	}
+	tc.ExternalSecret.Spec.Data = []esapi.ExternalSecretData{
+		{
+			SecretKey: secretKey,
+			RemoteRef: esapi.ExternalSecretDataRemoteRef{
+				Key: secretKey,
+			},
+		},
+	}
+}
+
+// when property is set it should return the json-encoded at path.
+func testJSONWithProperty(tc *framework.TestCase) {
+	secretKey := fmt.Sprintf("%s-%s", tc.Framework.Namespace.Name, "json")
+	expectedVal := `{"bar":"mysecret","baz":"bang"}`
+	tc.Secrets = map[string]string{
+		secretKey: jsonVal,
+	}
+	tc.ExpectedSecret = &v1.Secret{
+		Type: v1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			secretKey: []byte(expectedVal),
+		},
+	}
+	tc.ExternalSecret.Spec.Data = []esapi.ExternalSecretData{
+		{
+			SecretKey: secretKey,
+			RemoteRef: esapi.ExternalSecretDataRemoteRef{
+				Key:      secretKey,
+				Property: "foo.nested",
+			},
+		},
+	}
+}
+
+// when no property is set it should extract the key/value pairs at the given path
+// note: it should json-encode if a value contains nested data
+func testDataFromJSONWithoutProperty(tc *framework.TestCase) {
+	secretKey := fmt.Sprintf("%s-%s", tc.Framework.Namespace.Name, "json")
+	tc.Secrets = map[string]string{
+		secretKey: jsonVal,
+	}
+	tc.ExpectedSecret = &v1.Secret{
+		Type: v1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"foo": []byte(`{"nested":{"bar":"mysecret","baz":"bang"}}`),
+		},
+	}
+	tc.ExternalSecret.Spec.DataFrom = []esapi.ExternalSecretDataRemoteRef{
+		{
+			Key: secretKey,
+		},
+	}
+}
+
+// when property is set it should extract values with dataFrom at the given path.
+func testDataFromJSONWithProperty(tc *framework.TestCase) {
+	secretKey := fmt.Sprintf("%s-%s", tc.Framework.Namespace.Name, "json")
+	tc.Secrets = map[string]string{
+		secretKey: jsonVal,
+	}
+	tc.ExpectedSecret = &v1.Secret{
+		Type: v1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"bar": []byte(`mysecret`),
+			"baz": []byte(`bang`),
+		},
+	}
+	tc.ExternalSecret.Spec.DataFrom = []esapi.ExternalSecretDataRemoteRef{
+		{
+			Key:      secretKey,
+			Property: "foo.nested",
+		},
+	}
+}

+ 7 - 1
pkg/provider/vault/vault.go

@@ -174,7 +174,7 @@ func (v *client) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDat
 }
 }
 
 
 func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	data, err := v.readSecret(ctx, ref.Key, ref.Version)
+	data, err := v.GetSecret(ctx, ref)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -189,6 +189,12 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
 		switch t := v.(type) {
 		switch t := v.(type) {
 		case string:
 		case string:
 			byteMap[k] = []byte(t)
 			byteMap[k] = []byte(t)
+		case map[string]interface{}:
+			jsonData, err := json.Marshal(t)
+			if err != nil {
+				return nil, err
+			}
+			byteMap[k] = jsonData
 		case []byte:
 		case []byte:
 			byteMap[k] = t
 			byteMap[k] = t
 		// also covers int and float32 due to json.Marshal
 		// also covers int and float32 due to json.Marshal

+ 61 - 0
pkg/provider/vault/vault_test.go

@@ -725,6 +725,16 @@ func TestGetSecretMap(t *testing.T) {
 		"access_secret": "access_secret",
 		"access_secret": "access_secret",
 		"token":         nil,
 		"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{}{
 	secretWithTypes := map[string]interface{}{
 		"access_secret": "access_secret",
 		"access_secret": "access_secret",
 		"f32":           float32(2.12),
 		"f32":           float32(2.12),
@@ -865,6 +875,57 @@ func TestGetSecretMap(t *testing.T) {
 				},
 				},
 			},
 			},
 		},
 		},
+		"ReadNestedSecret": {
+			reason: "Should map the secret for deeply nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1alpha1.ExternalSecretDataRemoteRef{
+					Property: "nested",
+				},
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(
+							map[string]interface{}{
+								"data": secretWithNestedVal,
+							},
+						), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"foo": []byte(`{"mhkeih":"yada yada","oke":"yup"}`),
+				},
+			},
+		},
+		"ReadDeeplyNestedSecret": {
+			reason: "Should map the secret for deeply nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault,
+				data: esv1alpha1.ExternalSecretDataRemoteRef{
+					Property: "nested.foo",
+				},
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(
+							map[string]interface{}{
+								"data": secretWithNestedVal,
+							},
+						), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+				val: map[string][]byte{
+					"oke":    []byte("yup"),
+					"mhkeih": []byte("yada yada"),
+				},
+			},
+		},
 		"ReadSecretError": {
 		"ReadSecretError": {
 			reason: "Should return error if vault client fails to read secret.",
 			reason: "Should return error if vault client fails to read secret.",
 			args: args{
 			args: args{