Skip to content
This repository was archived by the owner on Aug 4, 2025. It is now read-only.

Commit c5fa8d8

Browse files
moolenSkarlso
andauthored
fix: webhook support more types when parsing response (external-secrets#2899)
* fix: support more types in webhook response Signed-off-by: Moritz Johner <[email protected]> * fix: properly decode json Signed-off-by: Moritz Johner <[email protected]> * Update pkg/provider/webhook/webhook.go Co-authored-by: Gergely Brautigam <[email protected]> Signed-off-by: Moritz Johner <[email protected]> * Update pkg/provider/webhook/webhook.go Co-authored-by: Gergely Brautigam <[email protected]> Signed-off-by: Moritz Johner <[email protected]> * fix: expose errors Signed-off-by: Moritz Johner <[email protected]> --------- Signed-off-by: Moritz Johner <[email protected]> Signed-off-by: Moritz Johner <[email protected]> Co-authored-by: Gergely Brautigam <[email protected]>
1 parent 7489753 commit c5fa8d8

File tree

7 files changed

+130
-129
lines changed

7 files changed

+130
-129
lines changed

pkg/generator/vault/vault.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
3131
provider "github.com/external-secrets/external-secrets/pkg/provider/vault"
32+
"github.com/external-secrets/external-secrets/pkg/utils"
3233
)
3334

3435
type Generator struct{}
@@ -114,7 +115,7 @@ func (g *Generator) generate(ctx context.Context, c *provider.Connector, jsonSpe
114115
}
115116

116117
for k := range data {
117-
response[k], err = provider.GetTypedKey(data, k)
118+
response[k], err = utils.GetByteValueFromMap(data, k)
118119
if err != nil {
119120
return nil, err
120121
}

pkg/provider/delinea/client.go

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,13 @@ import (
1717
"context"
1818
"encoding/json"
1919
"errors"
20-
"fmt"
21-
"reflect"
22-
"strconv"
23-
"strings"
2420

2521
"github.com/DelineaXPM/dsv-sdk-go/v2/vault"
2622
"github.com/tidwall/gjson"
2723
corev1 "k8s.io/api/core/v1"
2824

2925
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
30-
)
31-
32-
const (
33-
errSecretKeyFmt = "cannot find secret data for key: %q"
34-
errUnexpectedKey = "unexpected key in data: %s"
35-
errSecretFormat = "secret data for property %s not in expected format: %s"
26+
"github.com/external-secrets/external-secrets/pkg/utils"
3627
)
3728

3829
type client struct {
@@ -91,7 +82,7 @@ func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
9182
}
9283
byteMap := make(map[string][]byte, len(secret.Data))
9384
for k := range secret.Data {
94-
byteMap[k], err = getTypedKey(secret.Data, k)
85+
byteMap[k], err = utils.GetByteValueFromMap(secret.Data, k)
9586
if err != nil {
9687
return nil, err
9788
}
@@ -116,34 +107,3 @@ func (c *client) getSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRe
116107
}
117108
return c.api.Secret(ref.Key)
118109
}
119-
120-
// getTypedKey is copied from pkg/provider/vault/vault.go.
121-
func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
122-
v, ok := data[key]
123-
if !ok {
124-
return nil, fmt.Errorf(errUnexpectedKey, key)
125-
}
126-
switch t := v.(type) {
127-
case string:
128-
return []byte(t), nil
129-
case map[string]interface{}:
130-
return json.Marshal(t)
131-
case []string:
132-
return []byte(strings.Join(t, "\n")), nil
133-
case []byte:
134-
return t, nil
135-
// also covers int and float32 due to json.Marshal
136-
case float64:
137-
return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
138-
case json.Number:
139-
return []byte(t.String()), nil
140-
case []interface{}:
141-
return json.Marshal(t)
142-
case bool:
143-
return []byte(strconv.FormatBool(t)), nil
144-
case nil:
145-
return []byte(nil), nil
146-
default:
147-
return nil, fmt.Errorf(errSecretFormat, key, reflect.TypeOf(t))
148-
}
149-
}

pkg/provider/ibm/provider.go

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"encoding/json"
1919
"fmt"
2020
"os"
21-
"strconv"
2221
"strings"
2322
"time"
2423

@@ -565,37 +564,14 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
565564
func byteArrayMap(secretData map[string]interface{}, secretMap map[string][]byte) map[string][]byte {
566565
var err error
567566
for k, v := range secretData {
568-
secretMap[k], err = getTypedKey(v)
567+
secretMap[k], err = utils.GetByteValue(v)
569568
if err != nil {
570569
return nil
571570
}
572571
}
573572
return secretMap
574573
}
575574

576-
// kudos Vault Provider - convert from various types.
577-
func getTypedKey(v interface{}) ([]byte, error) {
578-
switch t := v.(type) {
579-
case string:
580-
return []byte(t), nil
581-
case map[string]interface{}:
582-
return json.Marshal(t)
583-
case map[string]string:
584-
return json.Marshal(t)
585-
case []byte:
586-
return t, nil
587-
// also covers int and float32 due to json.Marshal
588-
case float64:
589-
return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
590-
case bool:
591-
return []byte(strconv.FormatBool(t)), nil
592-
case nil:
593-
return []byte(nil), nil
594-
default:
595-
return nil, fmt.Errorf("secret not in expected format")
596-
}
597-
}
598-
599575
func (ibm *providerIBM) Close(_ context.Context) error {
600576
return nil
601577
}

pkg/provider/vault/vault.go

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import (
2424
"fmt"
2525
"net/http"
2626
"os"
27-
"reflect"
28-
"strconv"
2927
"strings"
3028
"time"
3129

@@ -89,7 +87,6 @@ const (
8987
errDataField = "failed to find data field"
9088
errJSONUnmarshall = "failed to unmarshall JSON"
9189
errPathInvalid = "provided Path isn't a valid kv v2 path"
92-
errSecretFormat = "secret data for property %s not in expected format: %s"
9390
errUnexpectedKey = "unexpected key in data: %s"
9491
errVaultToken = "cannot parse Vault authentication token: %w"
9592
errVaultRequest = "error from Vault request: %w"
@@ -758,7 +755,7 @@ func (v *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
758755
// actual keys to take precedence over gjson syntax
759756
// (2): extract key from secret with property
760757
if _, ok := data[ref.Property]; ok {
761-
return GetTypedKey(data, ref.Property)
758+
return utils.GetByteValueFromMap(data, ref.Property)
762759
}
763760

764761
// (3): extract key from secret using gjson
@@ -785,7 +782,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
785782
}
786783
byteMap := make(map[string][]byte, len(secretData))
787784
for k := range secretData {
788-
byteMap[k], err = GetTypedKey(secretData, k)
785+
byteMap[k], err = utils.GetByteValueFromMap(secretData, k)
789786
if err != nil {
790787
return nil, err
791788
}
@@ -794,36 +791,6 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
794791
return byteMap, nil
795792
}
796793

797-
func GetTypedKey(data map[string]interface{}, key string) ([]byte, error) {
798-
v, ok := data[key]
799-
if !ok {
800-
return nil, fmt.Errorf(errUnexpectedKey, key)
801-
}
802-
switch t := v.(type) {
803-
case string:
804-
return []byte(t), nil
805-
case map[string]interface{}:
806-
return json.Marshal(t)
807-
case []string:
808-
return []byte(strings.Join(t, "\n")), nil
809-
case []byte:
810-
return t, nil
811-
// also covers int and float32 due to json.Marshal
812-
case float64:
813-
return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
814-
case json.Number:
815-
return []byte(t.String()), nil
816-
case []interface{}:
817-
return json.Marshal(t)
818-
case bool:
819-
return []byte(strconv.FormatBool(t)), nil
820-
case nil:
821-
return []byte(nil), nil
822-
default:
823-
return nil, fmt.Errorf(errSecretFormat, key, reflect.TypeOf(t))
824-
}
825-
}
826-
827794
func (v *client) Close(ctx context.Context) error {
828795
// Revoke the token if we have one set, it wasn't sourced from a TokenSecretRef,
829796
// and token caching isn't enabled

pkg/provider/webhook/webhook.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ import (
1919
"context"
2020
"crypto/tls"
2121
"crypto/x509"
22+
"encoding/json"
2223
"fmt"
2324
"io"
2425
"net/http"
2526
"net/url"
27+
"strconv"
2628
"strings"
2729
tpl "text/template"
2830
"time"
2931

3032
"github.com/PaesslerAG/jsonpath"
31-
"gopkg.in/yaml.v3"
3233
corev1 "k8s.io/api/core/v1"
3334
"sigs.k8s.io/controller-runtime/pkg/client"
3435

@@ -152,30 +153,54 @@ func (w *WebHook) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDat
152153
}
153154
if resultJSONPath != "" {
154155
jsondata := interface{}(nil)
155-
if err := yaml.Unmarshal(result, &jsondata); err != nil {
156+
if err := json.Unmarshal(result, &jsondata); err != nil {
156157
return nil, fmt.Errorf("failed to parse response json: %w", err)
157158
}
158159
jsondata, err = jsonpath.Get(resultJSONPath, jsondata)
159160
if err != nil {
160161
return nil, fmt.Errorf("failed to get response path %s: %w", resultJSONPath, err)
161162
}
162-
jsonvalue, ok := jsondata.(string)
163-
if !ok {
164-
jsonvalues, ok := jsondata.([]interface{})
165-
if !ok {
166-
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
167-
}
168-
if len(jsonvalues) == 0 {
169-
return nil, fmt.Errorf("filter worked but didn't get any result")
170-
}
171-
jsonvalue = jsonvalues[0].(string)
172-
}
173-
return []byte(jsonvalue), nil
163+
return extractSecretData(jsondata)
174164
}
175165

176166
return result, nil
177167
}
178168

169+
// tries to extract data from an interface{}
170+
// it is supposed to return a single value.
171+
func extractSecretData(jsondata any) ([]byte, error) {
172+
switch val := jsondata.(type) {
173+
case bool:
174+
return []byte(strconv.FormatBool(val)), nil
175+
case nil:
176+
return []byte{}, nil
177+
case int:
178+
return []byte(strconv.Itoa(val)), nil
179+
case float64:
180+
return []byte(strconv.FormatFloat(val, 'f', 0, 64)), nil
181+
case []byte:
182+
return val, nil
183+
case string:
184+
return []byte(val), nil
185+
186+
// due to backwards compatibility we must keep this!
187+
// in case we see a []something we pick the first element and return it
188+
case []any:
189+
if len(val) == 0 {
190+
return nil, fmt.Errorf("filter worked but didn't get any result")
191+
}
192+
return extractSecretData(val[0])
193+
194+
// in case we encounter a map we serialize it instead of erroring out
195+
// The user should use that data from within a template and figure
196+
// out how to deal with it.
197+
case map[string]any:
198+
return json.Marshal(val)
199+
default:
200+
return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
201+
}
202+
}
203+
179204
func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
180205
provider, err := getProvider(w.store)
181206
if err != nil {
@@ -188,7 +213,7 @@ func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecret
188213

189214
// We always want json here, so just parse it out
190215
jsondata := interface{}(nil)
191-
if err := yaml.Unmarshal(result, &jsondata); err != nil {
216+
if err := json.Unmarshal(result, &jsondata); err != nil {
192217
return nil, fmt.Errorf("failed to parse response json: %w", err)
193218
}
194219
// Get subdata via jsonpath, if given
@@ -203,7 +228,7 @@ func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecret
203228
if ok {
204229
// This could also happen if the response was a single json-encoded string
205230
// but that is an extremely unlikely scenario
206-
if err := yaml.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
231+
if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
207232
return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
208233
}
209234
}

pkg/provider/webhook/webhook_test.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ want:
119119
path: /api/getsecret?id=testkey&version=1
120120
err: failed to get response path
121121
---
122-
case: error bad json data
122+
case: pull data out of map
123123
args:
124124
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
125125
key: testkey
@@ -128,7 +128,8 @@ args:
128128
response: '{"result":{"thesecret":{"one":"secret-value"}}}'
129129
want:
130130
path: /api/getsecret?id=testkey&version=1
131-
err: failed to get response (wrong type
131+
err: ''
132+
result: '{"one":"secret-value"}'
132133
---
133134
case: error timeout
134135
args:
@@ -199,10 +200,8 @@ args:
199200
response: 'some simple string'
200201
want:
201202
path: /api/getsecret?id=testkey&version=1
202-
err: failed to get response (wrong type
203-
resultmap:
204-
thesecret: secret-value
205-
alsosecret: another-value
203+
err: "failed to parse response json: invalid character"
204+
resultmap: {}
206205
---
207206
case: error json map
208207
args:
@@ -213,10 +212,8 @@ args:
213212
response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
214213
want:
215214
path: /api/getsecret?id=testkey&version=1
216-
err: failed to get response (wrong type
217-
resultmap:
218-
thesecret: secret-value
219-
alsosecret: another-value
215+
err: "failed to parse response json from jsonpath"
216+
resultmap: {}
220217
---
221218
case: good json with good templated jsonpath
222219
args:
@@ -265,6 +262,42 @@ args:
265262
want:
266263
path: /api/getsecret?id=testkey&version=1
267264
err: "filter worked but didn't get any result"
265+
---
266+
case: success with jsonpath filter and result array
267+
args:
268+
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
269+
key: testkey
270+
version: 1
271+
jsonpath: $..name
272+
response: '{"secrets": [{"name": "thesecret", "value": "secret-value"}, {"name": "alsosecret", "value": "another-value"}]}'
273+
want:
274+
path: /api/getsecret?id=testkey&version=1
275+
err: ''
276+
result: 'thesecret'
277+
---
278+
case: success with jsonpath filter and result array of ints
279+
args:
280+
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
281+
key: testkey
282+
version: 1
283+
jsonpath: $..name
284+
response: '{"secrets": [{"name": 123, "value": "secret-value"}, {"name": 456, "value": "another-value"}]}'
285+
want:
286+
path: /api/getsecret?id=testkey&version=1
287+
err: ''
288+
result: 123
289+
---
290+
case: support backslash
291+
args:
292+
url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
293+
key: testkey
294+
version: 1
295+
jsonpath: $.refresh_token
296+
response: '{"access_token":"REDACTED","refresh_token":"RE\/DACTED=="}'
297+
want:
298+
path: /api/getsecret?id=testkey&version=1
299+
err: ''
300+
result: "RE/DACTED=="
268301
`
269302

270303
func TestWebhookGetSecret(t *testing.T) {

0 commit comments

Comments
 (0)