Browse Source

Add support for multiple Items fields in DelineSecretServer secrets (#4051)

* Add support for multiple fields in DelineSecretServer secrets

Signed-off-by: Ronaldo Saheki <rsaheki@gmail.com>

* Add tested cases for errors and update documentation

Signed-off-by: Ronaldo Saheki <rsaheki@gmail.com>

* Update docs/provider/secretserver.md

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Ronaldo Saheki <rsaheki@gmail.com>
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Ronaldo Saheki <ronaldo.saheki@veeam.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Ronaldo 1 year ago
parent
commit
4f3909e0c9

+ 79 - 1
docs/provider/secretserver.md

@@ -69,7 +69,7 @@ You can either retrieve your entire secret or you can use a JSON formatted strin
 stored in your secret located at Items[0].ItemValue to retrieve a specific value.<br />
 See example JSON secret below.
 
-### Examples
+#### Examples
 Using the json formatted secret below:
 
 - Lookup a single top level property using secret ID.
@@ -131,3 +131,81 @@ returns: The entire secret in JSON format as displayed below
   ]
 }
 ```
+
+### Referencing Secrets in multiple Items secrets
+
+If there is more then one Item in the secret, it supports to retrieve them (all Item.\*.ItemValue) looking up by Item.\*.FieldName or Item.\*.Slug, instead of the above behaviour to use gjson only on the first item Items.0.ItemValue only.
+
+#### Examples
+
+Using the json formatted secret below:
+
+- Lookup a single top level property using secret ID.
+
+>spec.data.remoteRef.key = 4000 (id of the secret)<br />
+spec.data.remoteRef.property = "Username" (Items.0.FieldName)<br />
+returns: usernamevalue
+
+- Lookup a nested property using secret name.
+
+>spec.data.remoteRef.key = "Secretname" (name of the secret)<br />
+spec.data.remoteRef.property = "password" (Items.1.slug)<br />
+returns: passwordvalue
+
+- Lookup by secret ID (*secret name will work as well*) and return the entire secret.
+
+>spec.data.remoteRef.key = "4000" (id of the secret)<br />
+returns: The entire secret in JSON format as displayed below
+
+
+```JSON
+{
+  "Name": "Secretname",
+  "FolderID": 0,
+  "ID": 4000,
+  "SiteID": 0,
+  "SecretTemplateID": 0,
+  "LauncherConnectAsSecretID": 0,
+  "CheckOutIntervalMinutes": 0,
+  "Active": false,
+  "CheckedOut": false,
+  "CheckOutEnabled": false,
+  "AutoChangeEnabled": false,
+  "CheckOutChangePasswordEnabled": false,
+  "DelayIndexing": false,
+  "EnableInheritPermissions": false,
+  "EnableInheritSecretPolicy": false,
+  "ProxyEnabled": false,
+  "RequiresComment": false,
+  "SessionRecordingEnabled": false,
+  "WebLauncherRequiresIncognitoMode": false,
+  "Items": [
+    {
+      "ItemID": 0,
+      "FieldID": 0,
+      "FileAttachmentID": 0,
+      "FieldName": "Username",
+      "Slug": "username",
+      "FieldDescription": "",
+      "Filename": "",
+      "ItemValue": "usernamevalue",
+      "IsFile": false,
+      "IsNotes": false,
+      "IsPassword": false
+    },
+    {
+      "ItemID": 0,
+      "FieldID": 0,
+      "FileAttachmentID": 0,
+      "FieldName": "Password",
+      "Slug": "password",
+      "FieldDescription": "",
+      "Filename": "",
+      "ItemValue": "passwordvalue",
+      "IsFile": false,
+      "IsNotes": false,
+      "IsPassword": false
+    }
+  ]
+}
+```

+ 30 - 11
pkg/provider/secretserver/client.go

@@ -61,18 +61,37 @@ func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 	if ref.Property == "" {
 		return jsonStr, nil
 	}
-	// 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, esv1beta1.NoSecretError{}
-	}
-	// extract specific value from data directly above using gjson
-	out := gjson.Get(val.String(), ref.Property)
-	if !out.Exists() {
-		return nil, esv1beta1.NoSecretError{}
-	}
 
-	return []byte(out.String()), 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, esv1beta1.NoSecretError{}
+		}
+		// extract specific value from data directly above using gjson
+		out := gjson.Get(val.String(), ref.Property)
+		if !out.Exists() {
+			return nil, esv1beta1.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
+		}
+
+		out, ok := secretMap[ref.Property]
+		if !ok {
+			return nil, esv1beta1.NoSecretError{}
+		}
+
+		return []byte(out), nil
+	}
 }
 
 // Not supported at this time.

+ 57 - 5
pkg/provider/secretserver/client_test.go

@@ -79,6 +79,20 @@ func getJSONData() (*server.Secret, error) {
 	return s, nil
 }
 
+func createTestSecretFromCode(id int) *server.Secret {
+	s := new(server.Secret)
+	s.ID = id
+	s.Name = "Secretname"
+	s.Fields = make([]server.SecretField, 2)
+	s.Fields[0].ItemValue = "usernamevalue"
+	s.Fields[0].FieldName = "Username"
+	s.Fields[0].Slug = "username"
+	s.Fields[1].FieldName = "Password"
+	s.Fields[1].Slug = "password"
+	s.Fields[1].ItemValue = "passwordvalue"
+	return s
+}
+
 func newTestClient() esv1beta1.SecretsClient {
 	return &client{
 		api: &fakeAPI{
@@ -86,16 +100,18 @@ func newTestClient() esv1beta1.SecretsClient {
 				createSecret(1000, "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}"),
 				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),
 			},
 		},
 	}
 }
 
-func TestGetSecret(t *testing.T) {
+func TestGetSecretSecretServer(t *testing.T) {
 	ctx := context.Background()
 	c := newTestClient()
 	s, _ := getJSONData()
 	jsonStr, _ := json.Marshal(s)
+	jsonStr2, _ := json.Marshal(createTestSecretFromCode(4000))
 
 	testCases := map[string]struct {
 		ref  esv1beta1.ExternalSecretDataRemoteRef
@@ -116,33 +132,69 @@ func TestGetSecret(t *testing.T) {
 			},
 			want: []byte(`robertOppenheimer`),
 		},
-		"key and password property returns a single value": {
+		"Secret from JSON: key and password property returns a single value": {
 			ref: esv1beta1.ExternalSecretDataRemoteRef{
 				Key:      "1000",
 				Property: "password",
 			},
 			want: []byte(`badPassword`),
 		},
-		"key and nested property returns a single value": {
+		"Secret from JSON: key and nested property returns a single value": {
 			ref: esv1beta1.ExternalSecretDataRemoteRef{
 				Key:      "2000",
 				Property: "server.1",
 			},
 			want: []byte(`192.168.1.51`),
 		},
-		"existent key with non-existing propery": {
+		"Secret from JSON: existent key with non-existing propery": {
 			ref: esv1beta1.ExternalSecretDataRemoteRef{
 				Key:      "3000",
 				Property: "foo.bar",
 			},
 			err: esv1beta1.NoSecretError{},
 		},
-		"existent 'name' key with no propery": {
+		"Secret from JSON: existent 'name' key with no propery": {
 			ref: esv1beta1.ExternalSecretDataRemoteRef{
 				Key: "1000",
 			},
 			want: jsonStr,
 		},
+		"Secret from code: existent key with no property": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "4000",
+			},
+			want: jsonStr2,
+		},
+		"Secret from code: key and username fieldnamereturns a single value": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "4000",
+				Property: "Username",
+			},
+			want: []byte(`usernamevalue`),
+		},
+		"Secret from code: 'name' and password slug returns a single value": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "Secretname",
+				Property: "password",
+			},
+			want: []byte(`passwordvalue`),
+		},
+		"Secret from code: 'name' not found and password slug returns error": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "Secretnameerror",
+				Property: "password",
+			},
+			want: []byte(nil),
+			err:  errNotFound,
+		},
+		"Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "Secretname",
+				Property: "passwordkey",
+			},
+			want: []byte(nil),
+			err:  esv1beta1.NoSecretError{},
+		},
 	}
 
 	for name, tc := range testCases {