webhook.go 12 KB

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