webhook.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package webhook
  14. import (
  15. "bytes"
  16. "context"
  17. "crypto/tls"
  18. "crypto/x509"
  19. "encoding/json"
  20. "errors"
  21. "fmt"
  22. "io"
  23. "net/http"
  24. "net/url"
  25. tpl "text/template"
  26. "github.com/Azure/go-ntlmssp"
  27. "github.com/PaesslerAG/jsonpath"
  28. corev1 "k8s.io/api/core/v1"
  29. "sigs.k8s.io/controller-runtime/pkg/client"
  30. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  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/esutils"
  34. "github.com/external-secrets/external-secrets/pkg/metrics"
  35. "github.com/external-secrets/external-secrets/pkg/template/v2"
  36. )
  37. // Webhook implements functionality to interact with webhook endpoints
  38. // to retrieve and push secrets.
  39. type Webhook struct {
  40. Kube client.Client
  41. Namespace string
  42. StoreKind string
  43. HTTP *http.Client
  44. EnforceLabels bool
  45. ClusterScoped bool
  46. }
  47. func (w *Webhook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
  48. ke := client.ObjectKey{
  49. Name: ref.Name,
  50. Namespace: w.Namespace,
  51. }
  52. if w.ClusterScoped {
  53. if ref.Namespace == nil {
  54. return nil, fmt.Errorf("no namespace on ClusterScoped webhook secret %s", ref.Name)
  55. }
  56. ke.Namespace = *ref.Namespace
  57. }
  58. secret := &corev1.Secret{}
  59. if err := w.Kube.Get(ctx, ke, secret); err != nil {
  60. return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
  61. }
  62. if w.EnforceLabels {
  63. expected, ok := secret.Labels["external-secrets.io/type"]
  64. if !ok {
  65. return nil, errors.New("secret does not contain needed label 'external-secrets.io/type: webhook'. Update secret label to use it with webhook")
  66. }
  67. if expected != "webhook" {
  68. return nil, errors.New("secret type is not 'webhook'")
  69. }
  70. }
  71. return secret, nil
  72. }
  73. // GetSecretMap retrieves a secret from a webhook endpoint and processes
  74. // the response as a map of key-value pairs.
  75. func (w *Webhook) GetSecretMap(ctx context.Context, provider *Spec, ref *esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  76. result, err := w.GetWebhookData(ctx, provider, ref)
  77. if err != nil {
  78. return nil, err
  79. }
  80. // We always want json here, so just parse it out
  81. jsondata := any(nil)
  82. if err := json.Unmarshal(result, &jsondata); err != nil {
  83. return nil, fmt.Errorf("failed to parse response json: %w", err)
  84. }
  85. // Get subdata via jsonpath, if given
  86. if provider.Result.JSONPath != "" {
  87. jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
  88. if err != nil {
  89. return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
  90. }
  91. }
  92. // If the value is a string, try to parse it as json
  93. jsonstring, ok := jsondata.(string)
  94. if ok {
  95. // This could also happen if the response was a single json-encoded string
  96. // but that is an extremely unlikely scenario
  97. if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
  98. return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
  99. }
  100. }
  101. // Use the data as a key-value map
  102. jsonvalue, ok := jsondata.(map[string]any)
  103. if !ok {
  104. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  105. }
  106. // Change the map of generic objects to a map of byte arrays
  107. values := make(map[string][]byte)
  108. for rKey := range jsonvalue {
  109. values[rKey], err = esutils.GetByteValueFromMap(jsonvalue, rKey)
  110. if err != nil {
  111. return nil, fmt.Errorf("failed to get response for key '%s': %w", rKey, err)
  112. }
  113. }
  114. return values, nil
  115. }
  116. // GetTemplateData prepares the template data for webhook requests based on the given remote reference.
  117. func (w *Webhook) GetTemplateData(ctx context.Context, ref *esv1.ExternalSecretDataRemoteRef, secrets []Secret, urlEncode bool) (map[string]map[string]string, error) {
  118. data := map[string]map[string]string{}
  119. if ref != nil {
  120. if urlEncode {
  121. data["remoteRef"] = map[string]string{
  122. "key": url.QueryEscape(ref.Key),
  123. "version": url.QueryEscape(ref.Version),
  124. "property": url.QueryEscape(ref.Property),
  125. "namespace": w.Namespace,
  126. }
  127. } else {
  128. data["remoteRef"] = map[string]string{
  129. "key": ref.Key,
  130. "version": ref.Version,
  131. "property": ref.Property,
  132. "namespace": w.Namespace,
  133. }
  134. }
  135. }
  136. if err := w.getTemplatedSecrets(ctx, secrets, data); err != nil {
  137. return nil, err
  138. }
  139. return data, nil
  140. }
  141. // GetTemplatePushData prepares the template data for webhook push requests.
  142. func (w *Webhook) GetTemplatePushData(ctx context.Context, ref esv1.PushSecretData, secrets []Secret, urlEncode bool) (map[string]map[string]string, error) {
  143. data := map[string]map[string]string{}
  144. if ref != nil {
  145. if urlEncode {
  146. data["remoteRef"] = map[string]string{
  147. "remoteKey": url.QueryEscape(ref.GetRemoteKey()),
  148. }
  149. if v := ref.GetSecretKey(); v != "" {
  150. data["remoteRef"]["secretKey"] = url.QueryEscape(v)
  151. }
  152. } else {
  153. data["remoteRef"] = map[string]string{
  154. "remoteKey": ref.GetRemoteKey(),
  155. }
  156. if v := ref.GetSecretKey(); v != "" {
  157. data["remoteRef"]["secretKey"] = v
  158. }
  159. }
  160. }
  161. if err := w.getTemplatedSecrets(ctx, secrets, data); err != nil {
  162. return nil, err
  163. }
  164. return data, nil
  165. }
  166. func (w *Webhook) getTemplatedSecrets(ctx context.Context, secrets []Secret, data map[string]map[string]string) error {
  167. for _, secref := range secrets {
  168. if _, ok := data[secref.Name]; !ok {
  169. data[secref.Name] = make(map[string]string)
  170. }
  171. secret, err := w.getStoreSecret(ctx, secref.SecretRef)
  172. if err != nil {
  173. return err
  174. }
  175. for sKey, sVal := range secret.Data {
  176. data[secref.Name][sKey] = string(sVal)
  177. }
  178. }
  179. return nil
  180. }
  181. // GetWebhookData makes a request to the webhook endpoint and returns the raw response data.
  182. func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  183. if w.HTTP == nil {
  184. return nil, errors.New("http client not initialized")
  185. }
  186. // Parse store secrets
  187. escapedData, err := w.GetTemplateData(ctx, ref, provider.Secrets, true)
  188. if err != nil {
  189. return nil, err
  190. }
  191. rawData, err := w.GetTemplateData(ctx, ref, provider.Secrets, false)
  192. if err != nil {
  193. return nil, err
  194. }
  195. // set method
  196. method := provider.Method
  197. if method == "" {
  198. method = http.MethodGet
  199. }
  200. // set url
  201. url, err := ExecuteTemplateString(provider.URL, escapedData)
  202. if err != nil {
  203. return nil, fmt.Errorf("failed to parse url: %w", err)
  204. }
  205. // set body
  206. body, err := ExecuteTemplate(provider.Body, rawData)
  207. if err != nil {
  208. return nil, fmt.Errorf("failed to parse body: %w", err)
  209. }
  210. return w.executeRequest(ctx, provider, body.Bytes(), url, method, rawData)
  211. }
  212. // PushWebhookData pushes data to a webhook endpoint.
  213. func (w *Webhook) PushWebhookData(ctx context.Context, provider *Spec, data []byte, remoteKey esv1.PushSecretData) error {
  214. if w.HTTP == nil {
  215. return errors.New("http client not initialized")
  216. }
  217. method := provider.Method
  218. if method == "" {
  219. method = http.MethodPost
  220. }
  221. escapedData, err := w.GetTemplatePushData(ctx, remoteKey, provider.Secrets, true)
  222. if err != nil {
  223. return err
  224. }
  225. escapedData["remoteRef"][remoteKey.GetRemoteKey()] = url.QueryEscape(string(data))
  226. rawData, err := w.GetTemplatePushData(ctx, remoteKey, provider.Secrets, false)
  227. if err != nil {
  228. return err
  229. }
  230. rawData["remoteRef"][remoteKey.GetRemoteKey()] = string(data)
  231. url, err := ExecuteTemplateString(provider.URL, escapedData)
  232. if err != nil {
  233. return fmt.Errorf("failed to parse url: %w", err)
  234. }
  235. bodyt := provider.Body
  236. if bodyt == "" {
  237. bodyt = fmt.Sprintf("{{ .remoteRef.%s }}", remoteKey.GetRemoteKey())
  238. }
  239. body, err := ExecuteTemplate(bodyt, rawData)
  240. if err != nil {
  241. return fmt.Errorf("failed to parse body: %w", err)
  242. }
  243. if _, err := w.executeRequest(ctx, provider, body.Bytes(), url, method, rawData); err != nil {
  244. return fmt.Errorf("failed to push webhook data: %w", err)
  245. }
  246. return nil
  247. }
  248. func (w *Webhook) executeRequest(ctx context.Context, provider *Spec, data []byte, url, method string, rawData map[string]map[string]string) ([]byte, error) {
  249. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
  250. if err != nil {
  251. return nil, fmt.Errorf("failed to create request: %w", err)
  252. }
  253. if provider.Headers != nil {
  254. req, err = w.ReqAddHeaders(req, provider, rawData)
  255. if err != nil {
  256. return nil, err
  257. }
  258. }
  259. if provider.Auth != nil {
  260. req, err = w.ReqAddAuth(ctx, req, provider)
  261. if err != nil {
  262. return nil, err
  263. }
  264. }
  265. resp, err := w.HTTP.Do(req)
  266. metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
  267. if err != nil {
  268. return nil, fmt.Errorf("failed to call endpoint: %w", err)
  269. }
  270. defer func() {
  271. _ = resp.Body.Close()
  272. }()
  273. if resp.StatusCode == 404 {
  274. return nil, esv1.NoSecretError{}
  275. }
  276. if resp.StatusCode == http.StatusNotModified {
  277. return nil, esv1.NotModifiedError{}
  278. }
  279. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  280. return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
  281. }
  282. // return response body
  283. return io.ReadAll(resp.Body)
  284. }
  285. // ReqAddHeaders adds headers to an HTTP request based on provider configuration.
  286. func (w *Webhook) ReqAddHeaders(r *http.Request, provider *Spec, rawData map[string]map[string]string) (*http.Request, error) {
  287. reqWithHeaders := r
  288. for hKey, hValueTpl := range provider.Headers {
  289. hValue, err := ExecuteTemplateString(hValueTpl, rawData)
  290. if err != nil {
  291. return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
  292. }
  293. reqWithHeaders.Header.Add(hKey, hValue)
  294. }
  295. return reqWithHeaders, nil
  296. }
  297. // ReqAddAuth adds authentication to an HTTP request based on provider configuration.
  298. func (w *Webhook) ReqAddAuth(ctx context.Context, r *http.Request, provider *Spec) (*http.Request, error) {
  299. reqWithAuth := r
  300. //nolint:gocritic // singleCaseSwitch: we prefer to keep it as a switch for clarity
  301. switch {
  302. case provider.Auth.NTLM != nil:
  303. userSecretRef := provider.Auth.NTLM.UserName
  304. userSecret, err := w.getStoreSecret(ctx, userSecretRef)
  305. if err != nil {
  306. return nil, err
  307. }
  308. username := string(userSecret.Data[userSecretRef.Key])
  309. PasswordSecretRef := provider.Auth.NTLM.Password
  310. PasswordSecret, err := w.getStoreSecret(ctx, PasswordSecretRef)
  311. if err != nil {
  312. return nil, err
  313. }
  314. password := string(PasswordSecret.Data[PasswordSecretRef.Key])
  315. // This overwrites auth headers set by providers.headers
  316. reqWithAuth.SetBasicAuth(username, password)
  317. }
  318. return reqWithAuth, nil
  319. }
  320. // GetHTTPClient returns an HTTP client configured according to the provider specification.
  321. func (w *Webhook) GetHTTPClient(ctx context.Context, provider *Spec) (*http.Client, error) {
  322. c := &http.Client{}
  323. // add timeout to client if it is there
  324. if provider.Timeout != nil {
  325. c.Timeout = provider.Timeout.Duration
  326. }
  327. // add CA to client if it is there
  328. if len(provider.CABundle) > 0 || provider.CAProvider != nil {
  329. caCertPool, err := w.GetCACertPool(ctx, provider)
  330. if err != nil {
  331. return nil, err
  332. }
  333. tlsConf := &tls.Config{
  334. RootCAs: caCertPool,
  335. MinVersion: tls.VersionTLS12,
  336. Renegotiation: tls.RenegotiateOnceAsClient,
  337. }
  338. c.Transport = &http.Transport{TLSClientConfig: tlsConf}
  339. }
  340. // add authentication method if it s there
  341. if provider.Auth != nil {
  342. if provider.Auth.NTLM != nil {
  343. c.Transport =
  344. &ntlmssp.Negotiator{
  345. RoundTripper: &http.Transport{
  346. TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, // Needed to disable HTTP/2
  347. },
  348. }
  349. }
  350. // add additional auth methods here
  351. }
  352. // return client with all add-ons
  353. return c, nil
  354. }
  355. // GetCACertPool returns a certificate pool for TLS connections based on provider configuration.
  356. func (w *Webhook) GetCACertPool(ctx context.Context, provider *Spec) (*x509.CertPool, error) {
  357. caCertPool := x509.NewCertPool()
  358. ca, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
  359. CABundle: provider.CABundle,
  360. CAProvider: provider.CAProvider,
  361. StoreKind: w.StoreKind,
  362. Namespace: w.Namespace,
  363. Client: w.Kube,
  364. })
  365. if err != nil {
  366. return nil, err
  367. }
  368. ok := caCertPool.AppendCertsFromPEM(ca)
  369. if !ok {
  370. return nil, errors.New("failed to append cabundle")
  371. }
  372. return caCertPool, nil
  373. }
  374. // ExecuteTemplateString executes a template and returns the result as a string.
  375. func ExecuteTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
  376. result, err := ExecuteTemplate(tmpl, data)
  377. if err != nil {
  378. return "", err
  379. }
  380. return result.String(), nil
  381. }
  382. // ExecuteTemplate executes a template and returns the result as a bytes.Buffer.
  383. func ExecuteTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
  384. var result bytes.Buffer
  385. if tmpl == "" {
  386. return result, nil
  387. }
  388. urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
  389. if err != nil {
  390. return result, err
  391. }
  392. if err := urlt.Execute(&result, data); err != nil {
  393. return result, err
  394. }
  395. return result, nil
  396. }