Browse Source

fix: Support for Non-json secret fetched from Delinea SecretServer (#4743)

* fix: Support for Non-json secret fetched from Delinea SecretServer

This commit enhances the Delinea Secret Server provider to support secrets created using custom templates that contain only a single non-JSON field.

Previously, the provider assumed that `Items[0].ItemValue` always contained a JSON object. This caused failures when the value was plain text (as is common in single-field custom templates).

The updated logic introduces a hybrid strategy:
- If `Items[0].ItemValue` exists and is a valid JSON string, it uses GJSON to extract the desired property.
- If not, it falls back to a flattened map lookup using `fieldName` and `slug` to locate the value directly in the Fields array.

This ensures compatibility with both:
- Legacy structured secrets (nested JSON within `ItemValue`)
- Simpler templates where `ItemValue` is plain text (e.g. `"value": "abc123"`)

This fix improves interoperability with a wider range of Delinea secret templates without breaking compatibility with existing ones.

Tested with:
- Single-field plaintext custom templates
- Multi-field secrets with JSON-encoded values
- Empty or missing properties (returns full object)

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update client_test.go

Added test cases for Non-JSON secret and Malformed JSON secret

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update secretserver.md

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Remove test for malformed json secret

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update unit test cases in client_test.go

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update client_test.go

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Updated comment in  client.go

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update client_test.go

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

* Update client_test.go for test coverage

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>

---------

Signed-off-by: DelineaSahilWankhede <161290557+DelineaSahilWankhede@users.noreply.github.com>
Co-authored-by: Gustavo Fernandes de Carvalho <17139678+gusfcarvalho@users.noreply.github.com>
DelineaSahilWankhede 11 months ago
parent
commit
8debc0e7e7

+ 26 - 0
docs/provider/secretserver.md

@@ -64,6 +64,32 @@ spec:
           property: "array.0.value" #<GJSON_PROPERTY> * an empty property will return the entire secret
 ```
 
+### Support for Non-JSON Secret Templates
+
+The Secret Server provider now supports secrets that are not formatted as JSON. This enhancement allows users to retrieve and utilize secrets stored in formats such as plain text, XML, or other non-JSON structures without requiring additional parsing or transformation.​
+
+When working with non-JSON secrets, you can omit the remoteRef.property field in your ExternalSecret configuration. The entire content of the secret will be retrieved and stored as-is in the corresponding Kubernetes Secret.​
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+    name: secret-server-external-secret
+spec:
+    refreshInterval: 1h
+    secretStoreRef:
+      kind: SecretStore
+      name: secret-server-store
+    data:
+      - secretKey: SecretServerValue
+        remoteRef:
+          key: "52622"  # Secret ID
+```
+
+In this example, the secret with ID 52622 is retrieved in its entirety and stored under the key SecretServerValue in the Kubernetes Secret.​
+
+This feature simplifies the integration process for applications that require secrets in specific formats, eliminating the need for custom parsing logic within your applications.
+
 ### Preparing your secret
 You can either retrieve your entire secret or you can use a JSON formatted string
 stored in your secret located at Items[0].ItemValue to retrieve a specific value.<br />

+ 20 - 25
pkg/provider/secretserver/client.go

@@ -57,41 +57,36 @@ func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemot
 	if err != nil {
 		return nil, err
 	}
-	// If no property is defined return the full secret as raw json
+	// Intentionally fetch and return the full secret as raw JSON when no specific property is provided.
+	// This requires calling the API to retrieve the entire secret object.
 	if ref.Property == "" {
 		return jsonStr, nil
 	}
 
-	// Keep original behavior of decoding first Item into gjson
-	if len(secret.Fields) == 1 {
-		// extract first "field" i.e. Items.0.ItemValue, data from secret using gjson
-		val := gjson.Get(string(jsonStr), "Items.0.ItemValue")
-		if !val.Exists() {
-			return nil, esv1.NoSecretError{}
-		}
+	// extract first "field" i.e. Items.0.ItemValue, data from secret using gjson
+	val := gjson.Get(string(jsonStr), "Items.0.ItemValue")
+	if val.Exists() && gjson.Valid(val.String()) {
 		// extract specific value from data directly above using gjson
 		out := gjson.Get(val.String(), ref.Property)
-		if !out.Exists() {
-			return nil, esv1.NoSecretError{}
-		}
-		return []byte(out.String()), nil
-	} else {
-		// More general case Fields is an array in DelineaXPM/tss-sdk-go/v2/server
-		// https://github.com/DelineaXPM/tss-sdk-go/blob/571e5674a8103031ad6f873453db27959ec1ca67/server/secret.go#L23
-		secretMap := make(map[string]string)
-
-		for index := range secret.Fields {
-			secretMap[secret.Fields[index].FieldName] = secret.Fields[index].ItemValue
-			secretMap[secret.Fields[index].Slug] = secret.Fields[index].ItemValue
+		if out.Exists() {
+			return []byte(out.String()), nil
 		}
+	}
 
-		out, ok := secretMap[ref.Property]
-		if !ok {
-			return nil, esv1.NoSecretError{}
-		}
+	// More general case Fields is an array in DelineaXPM/tss-sdk-go/v2/server
+	// https://github.com/DelineaXPM/tss-sdk-go/blob/571e5674a8103031ad6f873453db27959ec1ca67/server/secret.go#L23
+	secretMap := make(map[string]string)
+	for index := range secret.Fields {
+		secretMap[secret.Fields[index].FieldName] = secret.Fields[index].ItemValue
+		secretMap[secret.Fields[index].Slug] = secret.Fields[index].ItemValue
+	}
 
-		return []byte(out), nil
+	out, ok := secretMap[ref.Property]
+	if !ok {
+		return nil, esv1.NoSecretError{}
 	}
+
+	return []byte(out), nil
 }
 
 // Not supported at this time.

+ 69 - 2
pkg/provider/secretserver/client_test.go

@@ -93,6 +93,33 @@ func createTestSecretFromCode(id int) *server.Secret {
 	return s
 }
 
+func createPlainTextSecret(id int) *server.Secret {
+	s := new(server.Secret)
+	s.ID = id
+	s.Name = "PlainTextSecret"
+	s.Fields = make([]server.SecretField, 1)
+	s.Fields[0].FieldName = "Content"
+	s.Fields[0].Slug = "content"
+	s.Fields[0].ItemValue = `non-json-secret-value`
+	return s
+}
+
+func createNilFieldsSecret(id int) *server.Secret {
+	s := new(server.Secret)
+	s.ID = id
+	s.Name = "NilFieldsSecret"
+	s.Fields = nil
+	return s
+}
+
+func createEmptyFieldsSecret(id int) *server.Secret {
+	s := new(server.Secret)
+	s.ID = id
+	s.Name = "EmptyFieldsSecret"
+	s.Fields = []server.SecretField{}
+	return s
+}
+
 func newTestClient() esv1.SecretsClient {
 	return &client{
 		api: &fakeAPI{
@@ -101,6 +128,10 @@ func newTestClient() esv1.SecretsClient {
 				createSecret(2000, "{ \"user\": \"helloWorld\", \"password\": \"badPassword\",\"server\":[ \"192.168.1.50\",\"192.168.1.51\"] }"),
 				createSecret(3000, "{ \"user\": \"chuckTesta\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}"),
 				createTestSecretFromCode(4000),
+				createPlainTextSecret(5000),
+				createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }"),
+				createNilFieldsSecret(7000),
+				createEmptyFieldsSecret(8000),
 			},
 		},
 	}
@@ -112,6 +143,7 @@ func TestGetSecretSecretServer(t *testing.T) {
 	s, _ := getJSONData()
 	jsonStr, _ := json.Marshal(s)
 	jsonStr2, _ := json.Marshal(createTestSecretFromCode(4000))
+	jsonStr3, _ := json.Marshal(createPlainTextSecret(5000))
 
 	testCases := map[string]struct {
 		ref  esv1.ExternalSecretDataRemoteRef
@@ -146,14 +178,14 @@ func TestGetSecretSecretServer(t *testing.T) {
 			},
 			want: []byte(`192.168.1.51`),
 		},
-		"Secret from JSON: existent key with non-existing propery": {
+		"Secret from JSON: existent key with non-existing property": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key:      "3000",
 				Property: "foo.bar",
 			},
 			err: esv1.NoSecretError{},
 		},
-		"Secret from JSON: existent 'name' key with no propery": {
+		"Secret from JSON: existent 'name' key with no property": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key: "1000",
 			},
@@ -172,6 +204,41 @@ func TestGetSecretSecretServer(t *testing.T) {
 			},
 			want: []byte(`usernamevalue`),
 		},
+		"Plain text secret: existent key with no property": {
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: "5000",
+			},
+			want: jsonStr3,
+		},
+		"Plain text secret: key with property returns expected value": {
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      "5000",
+				Property: "Content",
+			},
+			want: []byte(`non-json-secret-value`),
+		},
+		"Secret from code: valid ItemValue but incorrect property returns noSecretError": {
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      "6000",
+				Property: "missing",
+			},
+			want: []byte(nil),
+			err:  esv1.NoSecretError{},
+		},
+		"Secret from code: valid ItemValue but nil Fields returns nil": {
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key: "7000",
+			},
+			want: []byte(nil),
+		},
+		"Secret from code: empty Fields returns noSecretError": {
+			ref: esv1.ExternalSecretDataRemoteRef{
+				Key:      "8000",
+				Property: "missing",
+			},
+			want: []byte(nil),
+			err:  esv1.NoSecretError{},
+		},
 		"Secret from code: 'name' and password slug returns a single value": {
 			ref: esv1.ExternalSecretDataRemoteRef{
 				Key:      "Secretname",