webhook.go 13 KB

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