webhook.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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. "crypto/tls"
  17. "crypto/x509"
  18. "fmt"
  19. "io"
  20. "net/http"
  21. "net/url"
  22. "strings"
  23. tpl "text/template"
  24. "time"
  25. "github.com/PaesslerAG/jsonpath"
  26. "gopkg.in/yaml.v3"
  27. corev1 "k8s.io/api/core/v1"
  28. "sigs.k8s.io/controller-runtime/pkg/client"
  29. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  30. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  31. "github.com/external-secrets/external-secrets/pkg/constants"
  32. "github.com/external-secrets/external-secrets/pkg/metrics"
  33. "github.com/external-secrets/external-secrets/pkg/template/v2"
  34. "github.com/external-secrets/external-secrets/pkg/utils"
  35. )
  36. // https://github.com/external-secrets/external-secrets/issues/644
  37. var _ esv1beta1.SecretsClient = &WebHook{}
  38. var _ esv1beta1.Provider = &Provider{}
  39. // Provider satisfies the provider interface.
  40. type Provider struct{}
  41. type WebHook struct {
  42. kube client.Client
  43. store esv1beta1.GenericStore
  44. namespace string
  45. storeKind string
  46. http *http.Client
  47. url string
  48. }
  49. func init() {
  50. esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
  51. Webhook: &esv1beta1.WebhookProvider{},
  52. })
  53. }
  54. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  55. func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
  56. return esv1beta1.SecretStoreReadOnly
  57. }
  58. func (p *Provider) NewClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
  59. whClient := &WebHook{
  60. kube: kube,
  61. store: store,
  62. namespace: namespace,
  63. storeKind: store.GetObjectKind().GroupVersionKind().Kind,
  64. }
  65. provider, err := getProvider(store)
  66. if err != nil {
  67. return nil, err
  68. }
  69. whClient.url = provider.URL
  70. whClient.http, err = whClient.getHTTPClient(provider)
  71. if err != nil {
  72. return nil, err
  73. }
  74. return whClient, nil
  75. }
  76. func (p *Provider) ValidateStore(_ esv1beta1.GenericStore) error {
  77. return nil
  78. }
  79. func getProvider(store esv1beta1.GenericStore) (*esv1beta1.WebhookProvider, error) {
  80. spc := store.GetSpec()
  81. if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
  82. return nil, fmt.Errorf("missing store provider webhook")
  83. }
  84. return spc.Provider.Webhook, nil
  85. }
  86. func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
  87. ke := client.ObjectKey{
  88. Name: ref.Name,
  89. Namespace: w.namespace,
  90. }
  91. if w.storeKind == esv1beta1.ClusterSecretStoreKind {
  92. if ref.Namespace == nil {
  93. return nil, fmt.Errorf("no namespace on ClusterSecretStore webhook secret %s", ref.Name)
  94. }
  95. ke.Namespace = *ref.Namespace
  96. }
  97. secret := &corev1.Secret{}
  98. if err := w.kube.Get(ctx, ke, secret); err != nil {
  99. return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
  100. }
  101. return secret, nil
  102. }
  103. func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
  104. return fmt.Errorf("not implemented")
  105. }
  106. // Not Implemented PushSecret.
  107. func (w *WebHook) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
  108. return fmt.Errorf("not implemented")
  109. }
  110. // Empty GetAllSecrets.
  111. func (w *WebHook) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  112. // TO be implemented
  113. return nil, fmt.Errorf("GetAllSecrets not implemented")
  114. }
  115. func (w *WebHook) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  116. provider, err := getProvider(w.store)
  117. if err != nil {
  118. return nil, fmt.Errorf("failed to get store: %w", err)
  119. }
  120. result, err := w.getWebhookData(ctx, provider, ref)
  121. if err != nil {
  122. return nil, err
  123. }
  124. // Only parse as json if we have a jsonpath set
  125. data, err := w.getTemplateData(ctx, ref, provider.Secrets)
  126. if err != nil {
  127. return nil, err
  128. }
  129. resultJSONPath, err := executeTemplateString(provider.Result.JSONPath, data)
  130. if err != nil {
  131. return nil, err
  132. }
  133. if resultJSONPath != "" {
  134. jsondata := interface{}(nil)
  135. if err := yaml.Unmarshal(result, &jsondata); err != nil {
  136. return nil, fmt.Errorf("failed to parse response json: %w", err)
  137. }
  138. jsondata, err = jsonpath.Get(resultJSONPath, jsondata)
  139. if err != nil {
  140. return nil, fmt.Errorf("failed to get response path %s: %w", resultJSONPath, err)
  141. }
  142. jsonvalue, ok := jsondata.(string)
  143. if !ok {
  144. jsonvalues, ok := jsondata.([]interface{})
  145. if !ok {
  146. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  147. }
  148. if len(jsonvalues) == 0 {
  149. return nil, fmt.Errorf("filter worked but didn't get any result")
  150. }
  151. jsonvalue = jsonvalues[0].(string)
  152. }
  153. return []byte(jsonvalue), nil
  154. }
  155. return result, nil
  156. }
  157. func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  158. provider, err := getProvider(w.store)
  159. if err != nil {
  160. return nil, fmt.Errorf("failed to get store: %w", err)
  161. }
  162. result, err := w.getWebhookData(ctx, provider, ref)
  163. if err != nil {
  164. return nil, err
  165. }
  166. // We always want json here, so just parse it out
  167. jsondata := interface{}(nil)
  168. if err := yaml.Unmarshal(result, &jsondata); err != nil {
  169. return nil, fmt.Errorf("failed to parse response json: %w", err)
  170. }
  171. // Get subdata via jsonpath, if given
  172. if provider.Result.JSONPath != "" {
  173. jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
  174. if err != nil {
  175. return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
  176. }
  177. }
  178. // If the value is a string, try to parse it as json
  179. jsonstring, ok := jsondata.(string)
  180. if ok {
  181. // This could also happen if the response was a single json-encoded string
  182. // but that is an extremely unlikely scenario
  183. if err := yaml.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
  184. return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
  185. }
  186. }
  187. // Use the data as a key-value map
  188. jsonvalue, ok := jsondata.(map[string]interface{})
  189. if !ok {
  190. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  191. }
  192. // Change the map of generic objects to a map of byte arrays
  193. values := make(map[string][]byte)
  194. for rKey, rValue := range jsonvalue {
  195. jVal, ok := rValue.(string)
  196. if !ok {
  197. return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
  198. }
  199. values[rKey] = []byte(jVal)
  200. }
  201. return values, nil
  202. }
  203. func (w *WebHook) getTemplateData(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef, secrets []esv1beta1.WebhookSecret) (map[string]map[string]string, error) {
  204. data := map[string]map[string]string{
  205. "remoteRef": {
  206. "key": url.QueryEscape(ref.Key),
  207. "version": url.QueryEscape(ref.Version),
  208. "property": url.QueryEscape(ref.Property),
  209. },
  210. }
  211. for _, secref := range secrets {
  212. if _, ok := data[secref.Name]; !ok {
  213. data[secref.Name] = make(map[string]string)
  214. }
  215. secret, err := w.getStoreSecret(ctx, secref.SecretRef)
  216. if err != nil {
  217. return nil, err
  218. }
  219. for sKey, sVal := range secret.Data {
  220. data[secref.Name][sKey] = string(sVal)
  221. }
  222. }
  223. return data, nil
  224. }
  225. func (w *WebHook) getWebhookData(ctx context.Context, provider *esv1beta1.WebhookProvider, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  226. if w.http == nil {
  227. return nil, fmt.Errorf("http client not initialized")
  228. }
  229. data, err := w.getTemplateData(ctx, ref, provider.Secrets)
  230. if err != nil {
  231. return nil, err
  232. }
  233. method := provider.Method
  234. if method == "" {
  235. method = http.MethodGet
  236. }
  237. url, err := executeTemplateString(provider.URL, data)
  238. if err != nil {
  239. return nil, fmt.Errorf("failed to parse url: %w", err)
  240. }
  241. body, err := executeTemplate(provider.Body, data)
  242. if err != nil {
  243. return nil, fmt.Errorf("failed to parse body: %w", err)
  244. }
  245. req, err := http.NewRequestWithContext(ctx, method, url, &body)
  246. if err != nil {
  247. return nil, fmt.Errorf("failed to create request: %w", err)
  248. }
  249. for hKey, hValueTpl := range provider.Headers {
  250. hValue, err := executeTemplateString(hValueTpl, data)
  251. if err != nil {
  252. return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
  253. }
  254. req.Header.Add(hKey, hValue)
  255. }
  256. resp, err := w.http.Do(req)
  257. metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
  258. if err != nil {
  259. return nil, fmt.Errorf("failed to call endpoint: %w", err)
  260. }
  261. defer resp.Body.Close()
  262. if resp.StatusCode == 404 {
  263. return nil, esv1beta1.NoSecretError{}
  264. }
  265. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  266. return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
  267. }
  268. return io.ReadAll(resp.Body)
  269. }
  270. func (w *WebHook) getHTTPClient(provider *esv1beta1.WebhookProvider) (*http.Client, error) {
  271. client := &http.Client{}
  272. if provider.Timeout != nil {
  273. client.Timeout = provider.Timeout.Duration
  274. }
  275. if len(provider.CABundle) == 0 && provider.CAProvider == nil {
  276. // No need to process ca stuff if it is not there
  277. return client, nil
  278. }
  279. caCertPool, err := w.getCACertPool(provider)
  280. if err != nil {
  281. return nil, err
  282. }
  283. tlsConf := &tls.Config{
  284. RootCAs: caCertPool,
  285. MinVersion: tls.VersionTLS12,
  286. }
  287. client.Transport = &http.Transport{TLSClientConfig: tlsConf}
  288. return client, nil
  289. }
  290. func (w *WebHook) getCACertPool(provider *esv1beta1.WebhookProvider) (*x509.CertPool, error) {
  291. caCertPool := x509.NewCertPool()
  292. if len(provider.CABundle) > 0 {
  293. ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
  294. if !ok {
  295. return nil, fmt.Errorf("failed to append cabundle")
  296. }
  297. }
  298. if provider.CAProvider != nil && w.storeKind == esv1beta1.ClusterSecretStoreKind && provider.CAProvider.Namespace == nil {
  299. return nil, fmt.Errorf("missing namespace on CAProvider secret")
  300. }
  301. if provider.CAProvider != nil {
  302. var cert []byte
  303. var err error
  304. switch provider.CAProvider.Type {
  305. case esv1beta1.WebhookCAProviderTypeSecret:
  306. cert, err = w.getCertFromSecret(provider)
  307. case esv1beta1.WebhookCAProviderTypeConfigMap:
  308. cert, err = w.getCertFromConfigMap(provider)
  309. default:
  310. err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
  311. }
  312. if err != nil {
  313. return nil, err
  314. }
  315. ok := caCertPool.AppendCertsFromPEM(cert)
  316. if !ok {
  317. return nil, fmt.Errorf("failed to append cabundle")
  318. }
  319. }
  320. return caCertPool, nil
  321. }
  322. func (w *WebHook) getCertFromSecret(provider *esv1beta1.WebhookProvider) ([]byte, error) {
  323. secretRef := esmeta.SecretKeySelector{
  324. Name: provider.CAProvider.Name,
  325. Key: provider.CAProvider.Key,
  326. }
  327. if provider.CAProvider.Namespace != nil {
  328. secretRef.Namespace = provider.CAProvider.Namespace
  329. }
  330. ctx := context.Background()
  331. res, err := w.secretKeyRef(ctx, &secretRef)
  332. if err != nil {
  333. return nil, err
  334. }
  335. return []byte(res), nil
  336. }
  337. func (w *WebHook) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
  338. secret := &corev1.Secret{}
  339. ref := client.ObjectKey{
  340. Namespace: w.namespace,
  341. Name: secretRef.Name,
  342. }
  343. if (w.storeKind == esv1beta1.ClusterSecretStoreKind) &&
  344. (secretRef.Namespace != nil) {
  345. ref.Namespace = *secretRef.Namespace
  346. }
  347. err := w.kube.Get(ctx, ref, secret)
  348. if err != nil {
  349. return "", err
  350. }
  351. keyBytes, ok := secret.Data[secretRef.Key]
  352. if !ok {
  353. return "", err
  354. }
  355. value := string(keyBytes)
  356. valueStr := strings.TrimSpace(value)
  357. return valueStr, nil
  358. }
  359. func (w *WebHook) getCertFromConfigMap(provider *esv1beta1.WebhookProvider) ([]byte, error) {
  360. objKey := client.ObjectKey{
  361. Name: provider.CAProvider.Name,
  362. }
  363. if provider.CAProvider.Namespace != nil {
  364. objKey.Namespace = *provider.CAProvider.Namespace
  365. }
  366. configMapRef := &corev1.ConfigMap{}
  367. ctx := context.Background()
  368. err := w.kube.Get(ctx, objKey, configMapRef)
  369. if err != nil {
  370. return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
  371. }
  372. val, ok := configMapRef.Data[provider.CAProvider.Key]
  373. if !ok {
  374. return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
  375. }
  376. return []byte(val), nil
  377. }
  378. func (w *WebHook) Close(_ context.Context) error {
  379. return nil
  380. }
  381. func (w *WebHook) Validate() (esv1beta1.ValidationResult, error) {
  382. timeout := 15 * time.Second
  383. url := w.url
  384. if err := utils.NetworkValidate(url, timeout); err != nil {
  385. return esv1beta1.ValidationResultError, err
  386. }
  387. return esv1beta1.ValidationResultReady, nil
  388. }
  389. func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
  390. result, err := executeTemplate(tmpl, data)
  391. if err != nil {
  392. return "", err
  393. }
  394. return result.String(), nil
  395. }
  396. func executeTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
  397. var result bytes.Buffer
  398. if tmpl == "" {
  399. return result, nil
  400. }
  401. urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
  402. if err != nil {
  403. return result, err
  404. }
  405. if err := urlt.Execute(&result, data); err != nil {
  406. return result, err
  407. }
  408. return result, nil
  409. }