diff --git a/docs/provider/secretserver.md b/docs/provider/secretserver.md index e1ed50c7c..2f389971f 100644 --- a/docs/provider/secretserver.md +++ b/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.
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)
+spec.data.remoteRef.property = "Username" (Items.0.FieldName)
+returns: usernamevalue + +- Lookup a nested property using secret name. + +>spec.data.remoteRef.key = "Secretname" (name of the secret)
+spec.data.remoteRef.property = "password" (Items.1.slug)
+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)
+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 + } + ] +} +``` diff --git a/pkg/provider/secretserver/client.go b/pkg/provider/secretserver/client.go index c3f19db3b..49398675e 100644 --- a/pkg/provider/secretserver/client.go +++ b/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. diff --git a/pkg/provider/secretserver/client_test.go b/pkg/provider/secretserver/client_test.go index c338de70e..026136c36 100644 --- a/pkg/provider/secretserver/client_test.go +++ b/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 {