webhook.go 14 KB

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