webhook_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package webhook
  13. import (
  14. "bytes"
  15. "context"
  16. "errors"
  17. "io"
  18. "net/http"
  19. "net/http/httptest"
  20. "strings"
  21. "testing"
  22. "time"
  23. "gopkg.in/yaml.v3"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  26. )
  27. type testCase struct {
  28. Case string `json:"case,omitempty"`
  29. Args args `json:"args"`
  30. Want want `json:"want"`
  31. }
  32. type args struct {
  33. URL string `json:"url,omitempty"`
  34. Body string `json:"body,omitempty"`
  35. Timeout string `json:"timeout,omitempty"`
  36. Key string `json:"key,omitempty"`
  37. Property string `json:"property,omitempty"`
  38. Version string `json:"version,omitempty"`
  39. JSONPath string `json:"jsonpath,omitempty"`
  40. Response string `json:"response,omitempty"`
  41. StatusCode int `json:"statuscode,omitempty"`
  42. }
  43. type want struct {
  44. Path string `json:"path,omitempty"`
  45. Err string `json:"err,omitempty"`
  46. Result string `json:"result,omitempty"`
  47. ResultMap map[string]string `json:"resultmap,omitempty"`
  48. }
  49. var testCases = `
  50. case: error url
  51. args:
  52. url: /api/getsecret?id={{ .unclosed.template
  53. want:
  54. err: failed to parse url
  55. ---
  56. case: error body
  57. args:
  58. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  59. body: Body error {{ .unclosed.template
  60. want:
  61. err: failed to parse body
  62. ---
  63. case: error connection
  64. args:
  65. url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  66. want:
  67. err: failed to call endpoint
  68. ---
  69. case: error not found
  70. args:
  71. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  72. key: testkey
  73. version: 1
  74. statuscode: 404
  75. response: not found
  76. want:
  77. path: /api/getsecret?id=testkey&version=1
  78. err: endpoint gave error 404
  79. ---
  80. case: error bad json
  81. args:
  82. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  83. key: testkey
  84. version: 1
  85. jsonpath: $.result.thesecret
  86. response: '{"result":{"thesecret":"secret-value"}'
  87. want:
  88. path: /api/getsecret?id=testkey&version=1
  89. err: failed to parse response json
  90. ---
  91. case: error bad jsonpath
  92. args:
  93. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  94. key: testkey
  95. version: 1
  96. jsonpath: $.result.thesecret
  97. response: '{"result":{"nosecret":"secret-value"}}'
  98. want:
  99. path: /api/getsecret?id=testkey&version=1
  100. err: failed to get response path
  101. ---
  102. case: error bad json data
  103. args:
  104. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  105. key: testkey
  106. version: 1
  107. jsonpath: $.result.thesecret
  108. response: '{"result":{"thesecret":{"one":"secret-value"}}}'
  109. want:
  110. path: /api/getsecret?id=testkey&version=1
  111. err: failed to get response (wrong type
  112. ---
  113. case: error timeout
  114. args:
  115. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  116. key: testkey
  117. version: 1
  118. response: secret-value
  119. timeout: 0.01ms
  120. want:
  121. path: /api/getsecret?id=testkey&version=1
  122. err: context deadline exceeded
  123. ---
  124. case: good plaintext
  125. args:
  126. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  127. key: testkey
  128. version: 1
  129. response: secret-value
  130. want:
  131. path: /api/getsecret?id=testkey&version=1
  132. err: ''
  133. result: secret-value
  134. ---
  135. case: good json
  136. args:
  137. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  138. key: testkey
  139. version: 1
  140. jsonpath: $.result.thesecret
  141. response: '{"result":{"thesecret":"secret-value"}}'
  142. want:
  143. path: /api/getsecret?id=testkey&version=1
  144. err: ''
  145. result: secret-value
  146. ---
  147. case: good json map
  148. args:
  149. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  150. key: testkey
  151. version: 1
  152. jsonpath: $.result
  153. response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
  154. want:
  155. path: /api/getsecret?id=testkey&version=1
  156. err: ''
  157. resultmap:
  158. thesecret: secret-value
  159. alsosecret: another-value
  160. ---
  161. case: good json map string
  162. args:
  163. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  164. key: testkey
  165. version: 1
  166. response: '{"thesecret":"secret-value","alsosecret":"another-value"}'
  167. want:
  168. path: /api/getsecret?id=testkey&version=1
  169. err: ''
  170. resultmap:
  171. thesecret: secret-value
  172. alsosecret: another-value
  173. ---
  174. case: error json map string
  175. args:
  176. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  177. key: testkey
  178. version: 1
  179. response: 'some simple string'
  180. want:
  181. path: /api/getsecret?id=testkey&version=1
  182. err: failed to get response (wrong type
  183. resultmap:
  184. thesecret: secret-value
  185. alsosecret: another-value
  186. ---
  187. case: error json map
  188. args:
  189. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  190. key: testkey
  191. version: 1
  192. jsonpath: $.result.thesecret
  193. response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
  194. want:
  195. path: /api/getsecret?id=testkey&version=1
  196. err: failed to get response (wrong type
  197. resultmap:
  198. thesecret: secret-value
  199. alsosecret: another-value
  200. ---
  201. case: good json with good templated jsonpath
  202. args:
  203. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  204. key: testkey
  205. property: thesecret
  206. version: 1
  207. jsonpath: $.result.{{ .remoteRef.property }}
  208. response: '{"result":{"thesecret":"secret-value"}}'
  209. want:
  210. path: /api/getsecret?id=testkey&version=1
  211. err: ''
  212. result: secret-value
  213. ---
  214. case: good json with bad temlated jsonpath
  215. args:
  216. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  217. key: testkey
  218. property: thesecret
  219. version: 1
  220. jsonpath: $.result.{{ .remoteRef.property }
  221. response: '{"result":{"thesecret":"secret-value"}}'
  222. want:
  223. path: /api/getsecret?id=testkey&version=1
  224. err: 'template: webhooktemplate:1: unexpected "}" in operand'
  225. `
  226. func TestWebhookGetSecret(t *testing.T) {
  227. ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
  228. for {
  229. var tc testCase
  230. if err := ydec.Decode(&tc); err != nil {
  231. if !errors.Is(err, io.EOF) {
  232. t.Errorf("testcase decode error %v", err)
  233. }
  234. break
  235. }
  236. runTestCase(tc, t)
  237. }
  238. }
  239. func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
  240. // Start a new server for every test case because the server wants to check the expected api path
  241. return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  242. if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
  243. t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
  244. }
  245. if tc.Args.StatusCode != 0 {
  246. rw.WriteHeader(tc.Args.StatusCode)
  247. }
  248. rw.Write([]byte(tc.Args.Response))
  249. }))
  250. }
  251. func parseTimeout(timeout string) (*metav1.Duration, error) {
  252. if timeout == "" {
  253. return nil, nil
  254. }
  255. dur, err := time.ParseDuration(timeout)
  256. if err != nil {
  257. return nil, err
  258. }
  259. return &metav1.Duration{Duration: dur}, nil
  260. }
  261. func runTestCase(tc testCase, t *testing.T) {
  262. ts := testCaseServer(tc, t)
  263. defer ts.Close()
  264. testStore := makeClusterSecretStore(ts.URL, tc.Args)
  265. var err error
  266. timeout, err := parseTimeout(tc.Args.Timeout)
  267. if err != nil {
  268. t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
  269. return
  270. }
  271. testStore.Spec.Provider.Webhook.Timeout = timeout
  272. testProv := &Provider{}
  273. client, err := testProv.NewClient(context.Background(), testStore, nil, "testnamespace")
  274. if err != nil {
  275. t.Errorf("%s: error creating client: %s", tc.Case, err.Error())
  276. return
  277. }
  278. if tc.Want.ResultMap != nil {
  279. testGetSecretMap(tc, t, client)
  280. } else {
  281. testGetSecret(tc, t, client)
  282. }
  283. }
  284. func testGetSecretMap(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
  285. testRef := esv1beta1.ExternalSecretDataRemoteRef{
  286. Key: tc.Args.Key,
  287. Version: tc.Args.Version,
  288. }
  289. secretmap, err := client.GetSecretMap(context.Background(), testRef)
  290. errStr := ""
  291. if err != nil {
  292. errStr = err.Error()
  293. }
  294. if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
  295. t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
  296. }
  297. if err == nil {
  298. for wantkey, wantval := range tc.Want.ResultMap {
  299. gotval, ok := secretmap[wantkey]
  300. if !ok {
  301. t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
  302. } else if string(gotval) != wantval {
  303. t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
  304. }
  305. }
  306. }
  307. }
  308. func testGetSecret(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
  309. testRef := esv1beta1.ExternalSecretDataRemoteRef{
  310. Key: tc.Args.Key,
  311. Property: tc.Args.Property,
  312. Version: tc.Args.Version,
  313. }
  314. ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  315. defer cancel()
  316. secret, err := client.GetSecret(ctx, testRef)
  317. errStr := ""
  318. if err != nil {
  319. errStr = err.Error()
  320. }
  321. if !strings.Contains(errStr, tc.Want.Err) {
  322. t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
  323. }
  324. if err == nil && string(secret) != tc.Want.Result {
  325. t.Errorf("%s: unexpected response: '%s' (expected '%s')", tc.Case, secret, tc.Want.Result)
  326. }
  327. }
  328. func makeClusterSecretStore(url string, args args) *esv1beta1.ClusterSecretStore {
  329. store := &esv1beta1.ClusterSecretStore{
  330. TypeMeta: metav1.TypeMeta{
  331. Kind: "ClusterSecretStore",
  332. },
  333. ObjectMeta: metav1.ObjectMeta{
  334. Name: "wehbook-store",
  335. Namespace: "default",
  336. },
  337. Spec: esv1beta1.SecretStoreSpec{
  338. Provider: &esv1beta1.SecretStoreProvider{
  339. Webhook: &esv1beta1.WebhookProvider{
  340. URL: url + args.URL,
  341. Body: args.Body,
  342. Headers: map[string]string{
  343. "Content-Type": "application.json",
  344. "X-SecretKey": "{{ .remoteRef.key }}",
  345. },
  346. Result: esv1beta1.WebhookResult{
  347. JSONPath: args.JSONPath,
  348. },
  349. },
  350. },
  351. },
  352. }
  353. return store
  354. }