mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
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>
This commit is contained in:
parent
4dfa4d2622
commit
4f3909e0c9
3 changed files with 166 additions and 17 deletions
|
@ -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 />
|
stored in your secret located at Items[0].ItemValue to retrieve a specific value.<br />
|
||||||
See example JSON secret below.
|
See example JSON secret below.
|
||||||
|
|
||||||
### Examples
|
#### Examples
|
||||||
Using the json formatted secret below:
|
Using the json formatted secret below:
|
||||||
|
|
||||||
- Lookup a single top level property using secret ID.
|
- 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -61,18 +61,37 @@ func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
|
||||||
if ref.Property == "" {
|
if ref.Property == "" {
|
||||||
return jsonStr, nil
|
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.
|
// Not supported at this time.
|
||||||
|
|
|
@ -79,6 +79,20 @@ func getJSONData() (*server.Secret, error) {
|
||||||
return s, nil
|
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 {
|
func newTestClient() esv1beta1.SecretsClient {
|
||||||
return &client{
|
return &client{
|
||||||
api: &fakeAPI{
|
api: &fakeAPI{
|
||||||
|
@ -86,16 +100,18 @@ func newTestClient() esv1beta1.SecretsClient {
|
||||||
createSecret(1000, "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}"),
|
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(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\"}"),
|
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()
|
ctx := context.Background()
|
||||||
c := newTestClient()
|
c := newTestClient()
|
||||||
s, _ := getJSONData()
|
s, _ := getJSONData()
|
||||||
jsonStr, _ := json.Marshal(s)
|
jsonStr, _ := json.Marshal(s)
|
||||||
|
jsonStr2, _ := json.Marshal(createTestSecretFromCode(4000))
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
ref esv1beta1.ExternalSecretDataRemoteRef
|
ref esv1beta1.ExternalSecretDataRemoteRef
|
||||||
|
@ -116,33 +132,69 @@ func TestGetSecret(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: []byte(`robertOppenheimer`),
|
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{
|
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
Key: "1000",
|
Key: "1000",
|
||||||
Property: "password",
|
Property: "password",
|
||||||
},
|
},
|
||||||
want: []byte(`badPassword`),
|
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{
|
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
Key: "2000",
|
Key: "2000",
|
||||||
Property: "server.1",
|
Property: "server.1",
|
||||||
},
|
},
|
||||||
want: []byte(`192.168.1.51`),
|
want: []byte(`192.168.1.51`),
|
||||||
},
|
},
|
||||||
"existent key with non-existing propery": {
|
"Secret from JSON: existent key with non-existing propery": {
|
||||||
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
Key: "3000",
|
Key: "3000",
|
||||||
Property: "foo.bar",
|
Property: "foo.bar",
|
||||||
},
|
},
|
||||||
err: esv1beta1.NoSecretError{},
|
err: esv1beta1.NoSecretError{},
|
||||||
},
|
},
|
||||||
"existent 'name' key with no propery": {
|
"Secret from JSON: existent 'name' key with no propery": {
|
||||||
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
ref: esv1beta1.ExternalSecretDataRemoteRef{
|
||||||
Key: "1000",
|
Key: "1000",
|
||||||
},
|
},
|
||||||
want: jsonStr,
|
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 {
|
for name, tc := range testCases {
|
||||||
|
|
Loading…
Reference in a new issue