webhook.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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/metrics"
  34. "github.com/external-secrets/external-secrets/pkg/template/v2"
  35. "github.com/external-secrets/external-secrets/pkg/utils"
  36. )
  37. type Webhook struct {
  38. Kube client.Client
  39. Namespace string
  40. StoreKind string
  41. HTTP *http.Client
  42. EnforceLabels bool
  43. ClusterScoped bool
  44. }
  45. func (w *Webhook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
  46. ke := client.ObjectKey{
  47. Name: ref.Name,
  48. Namespace: w.Namespace,
  49. }
  50. if w.ClusterScoped {
  51. if ref.Namespace == nil {
  52. return nil, fmt.Errorf("no namespace on ClusterScoped webhook secret %s", ref.Name)
  53. }
  54. ke.Namespace = *ref.Namespace
  55. }
  56. secret := &corev1.Secret{}
  57. if err := w.Kube.Get(ctx, ke, secret); err != nil {
  58. return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
  59. }
  60. if w.EnforceLabels {
  61. expected, ok := secret.Labels["external-secrets.io/type"]
  62. if !ok {
  63. return nil, errors.New("secret does not contain needed label 'external-secrets.io/type: webhook'. Update secret label to use it with webhook")
  64. }
  65. if expected != "webhook" {
  66. return nil, errors.New("secret type is not 'webhook'")
  67. }
  68. }
  69. return secret, nil
  70. }
  71. func (w *Webhook) GetSecretMap(ctx context.Context, provider *Spec, ref *esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  72. result, err := w.GetWebhookData(ctx, provider, ref)
  73. if err != nil {
  74. return nil, err
  75. }
  76. // We always want json here, so just parse it out
  77. jsondata := any(nil)
  78. if err := json.Unmarshal(result, &jsondata); err != nil {
  79. return nil, fmt.Errorf("failed to parse response json: %w", err)
  80. }
  81. // Get subdata via jsonpath, if given
  82. if provider.Result.JSONPath != "" {
  83. jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
  84. if err != nil {
  85. return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
  86. }
  87. }
  88. // If the value is a string, try to parse it as json
  89. jsonstring, ok := jsondata.(string)
  90. if ok {
  91. // This could also happen if the response was a single json-encoded string
  92. // but that is an extremely unlikely scenario
  93. if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
  94. return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
  95. }
  96. }
  97. // Use the data as a key-value map
  98. jsonvalue, ok := jsondata.(map[string]any)
  99. if !ok {
  100. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  101. }
  102. // Change the map of generic objects to a map of byte arrays
  103. values := make(map[string][]byte)
  104. for rKey := range jsonvalue {
  105. values[rKey], err = utils.GetByteValueFromMap(jsonvalue, rKey)
  106. if err != nil {
  107. return nil, fmt.Errorf("failed to get response for key '%s': %w", rKey, err)
  108. }
  109. }
  110. return values, nil
  111. }
  112. func (w *Webhook) GetTemplateData(ctx context.Context, ref *esv1.ExternalSecretDataRemoteRef, secrets []Secret, urlEncode bool) (map[string]map[string]string, error) {
  113. data := map[string]map[string]string{}
  114. if ref != nil {
  115. if urlEncode {
  116. data["remoteRef"] = map[string]string{
  117. "key": url.QueryEscape(ref.Key),
  118. "version": url.QueryEscape(ref.Version),
  119. "property": url.QueryEscape(ref.Property),
  120. "namespace": w.Namespace,
  121. }
  122. } else {
  123. data["remoteRef"] = map[string]string{
  124. "key": ref.Key,
  125. "version": ref.Version,
  126. "property": ref.Property,
  127. "namespace": w.Namespace,
  128. }
  129. }
  130. }
  131. if err := w.getTemplatedSecrets(ctx, secrets, data); err != nil {
  132. return nil, err
  133. }
  134. return data, nil
  135. }
  136. func (w *Webhook) GetTemplatePushData(ctx context.Context, ref esv1.PushSecretData, secrets []Secret, urlEncode bool) (map[string]map[string]string, error) {
  137. data := map[string]map[string]string{}
  138. if ref != nil {
  139. if urlEncode {
  140. data["remoteRef"] = map[string]string{
  141. "remoteKey": url.QueryEscape(ref.GetRemoteKey()),
  142. }
  143. if v := ref.GetSecretKey(); v != "" {
  144. data["remoteRef"]["secretKey"] = url.QueryEscape(v)
  145. }
  146. } else {
  147. data["remoteRef"] = map[string]string{
  148. "remoteKey": ref.GetRemoteKey(),
  149. }
  150. if v := ref.GetSecretKey(); v != "" {
  151. data["remoteRef"]["secretKey"] = v
  152. }
  153. }
  154. }
  155. if err := w.getTemplatedSecrets(ctx, secrets, data); err != nil {
  156. return nil, err
  157. }
  158. return data, nil
  159. }
  160. func (w *Webhook) getTemplatedSecrets(ctx context.Context, secrets []Secret, data map[string]map[string]string) error {
  161. for _, secref := range secrets {
  162. if _, ok := data[secref.Name]; !ok {
  163. data[secref.Name] = make(map[string]string)
  164. }
  165. secret, err := w.getStoreSecret(ctx, secref.SecretRef)
  166. if err != nil {
  167. return err
  168. }
  169. for sKey, sVal := range secret.Data {
  170. data[secref.Name][sKey] = string(sVal)
  171. }
  172. }
  173. return nil
  174. }
  175. func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  176. if w.HTTP == nil {
  177. return nil, errors.New("http client not initialized")
  178. }
  179. // Parse store secrets
  180. escapedData, err := w.GetTemplateData(ctx, ref, provider.Secrets, true)
  181. if err != nil {
  182. return nil, err
  183. }
  184. rawData, err := w.GetTemplateData(ctx, ref, provider.Secrets, false)
  185. if err != nil {
  186. return nil, err
  187. }
  188. // set method
  189. method := provider.Method
  190. if method == "" {
  191. method = http.MethodGet
  192. }
  193. // set url
  194. url, err := ExecuteTemplateString(provider.URL, escapedData)
  195. if err != nil {
  196. return nil, fmt.Errorf("failed to parse url: %w", err)
  197. }
  198. // set body
  199. body, err := ExecuteTemplate(provider.Body, rawData)
  200. if err != nil {
  201. return nil, fmt.Errorf("failed to parse body: %w", err)
  202. }
  203. return w.executeRequest(ctx, provider, body.Bytes(), url, method, rawData)
  204. }
  205. func (w *Webhook) PushWebhookData(ctx context.Context, provider *Spec, data []byte, remoteKey esv1.PushSecretData) error {
  206. if w.HTTP == nil {
  207. return errors.New("http client not initialized")
  208. }
  209. method := provider.Method
  210. if method == "" {
  211. method = http.MethodPost
  212. }
  213. escapedData, err := w.GetTemplatePushData(ctx, remoteKey, provider.Secrets, true)
  214. if err != nil {
  215. return err
  216. }
  217. escapedData["remoteRef"][remoteKey.GetRemoteKey()] = url.QueryEscape(string(data))
  218. rawData, err := w.GetTemplatePushData(ctx, remoteKey, provider.Secrets, false)
  219. if err != nil {
  220. return err
  221. }
  222. rawData["remoteRef"][remoteKey.GetRemoteKey()] = string(data)
  223. url, err := ExecuteTemplateString(provider.URL, escapedData)
  224. if err != nil {
  225. return fmt.Errorf("failed to parse url: %w", err)
  226. }
  227. bodyt := provider.Body
  228. if bodyt == "" {
  229. bodyt = fmt.Sprintf("{{ .remoteRef.%s }}", remoteKey.GetRemoteKey())
  230. }
  231. body, err := ExecuteTemplate(bodyt, rawData)
  232. if err != nil {
  233. return fmt.Errorf("failed to parse body: %w", err)
  234. }
  235. if _, err := w.executeRequest(ctx, provider, body.Bytes(), url, method, rawData); err != nil {
  236. return fmt.Errorf("failed to push webhook data: %w", err)
  237. }
  238. return nil
  239. }
  240. func (w *Webhook) executeRequest(ctx context.Context, provider *Spec, data []byte, url, method string, rawData map[string]map[string]string) ([]byte, error) {
  241. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
  242. if err != nil {
  243. return nil, fmt.Errorf("failed to create request: %w", err)
  244. }
  245. if provider.Headers != nil {
  246. req, err = w.ReqAddHeaders(req, provider, rawData)
  247. if err != nil {
  248. return nil, err
  249. }
  250. }
  251. if provider.Auth != nil {
  252. req, err = w.ReqAddAuth(req, provider, ctx)
  253. if err != nil {
  254. return nil, err
  255. }
  256. }
  257. resp, err := w.HTTP.Do(req)
  258. metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
  259. if err != nil {
  260. return nil, fmt.Errorf("failed to call endpoint: %w", err)
  261. }
  262. defer func() {
  263. _ = resp.Body.Close()
  264. }()
  265. if resp.StatusCode == 404 {
  266. return nil, esv1.NoSecretError{}
  267. }
  268. if resp.StatusCode == http.StatusNotModified {
  269. return nil, esv1.NotModifiedError{}
  270. }
  271. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  272. return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
  273. }
  274. // return response body
  275. return io.ReadAll(resp.Body)
  276. }
  277. func (w *Webhook) ReqAddHeaders(r *http.Request, provider *Spec, rawData map[string]map[string]string) (*http.Request, error) {
  278. reqWithHeaders := r
  279. for hKey, hValueTpl := range provider.Headers {
  280. hValue, err := ExecuteTemplateString(hValueTpl, rawData)
  281. if err != nil {
  282. return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
  283. }
  284. reqWithHeaders.Header.Add(hKey, hValue)
  285. }
  286. return reqWithHeaders, nil
  287. }
  288. func (w *Webhook) ReqAddAuth(r *http.Request, provider *Spec, ctx context.Context) (*http.Request, error) {
  289. reqWithAuth := r
  290. //nolint:gocritic // singleCaseSwitch: we prefer to keep it as a switch for clarity
  291. switch {
  292. case provider.Auth.NTLM != nil:
  293. userSecretRef := provider.Auth.NTLM.UserName
  294. userSecret, err := w.getStoreSecret(ctx, userSecretRef)
  295. if err != nil {
  296. return nil, err
  297. }
  298. username := string(userSecret.Data[userSecretRef.Key])
  299. PasswordSecretRef := provider.Auth.NTLM.Password
  300. PasswordSecret, err := w.getStoreSecret(ctx, PasswordSecretRef)
  301. if err != nil {
  302. return nil, err
  303. }
  304. password := string(PasswordSecret.Data[PasswordSecretRef.Key])
  305. // This overwrites auth headers set by providers.headers
  306. reqWithAuth.SetBasicAuth(username, password)
  307. }
  308. return reqWithAuth, nil
  309. }
  310. func (w *Webhook) GetHTTPClient(ctx context.Context, provider *Spec) (*http.Client, error) {
  311. client := &http.Client{}
  312. // add timeout to client if it is there
  313. if provider.Timeout != nil {
  314. client.Timeout = provider.Timeout.Duration
  315. }
  316. // add CA to client if it is there
  317. if len(provider.CABundle) > 0 || provider.CAProvider != nil {
  318. caCertPool, err := w.GetCACertPool(ctx, provider)
  319. if err != nil {
  320. return nil, err
  321. }
  322. tlsConf := &tls.Config{
  323. RootCAs: caCertPool,
  324. MinVersion: tls.VersionTLS12,
  325. Renegotiation: tls.RenegotiateOnceAsClient,
  326. }
  327. client.Transport = &http.Transport{TLSClientConfig: tlsConf}
  328. }
  329. // add authentication method if it s there
  330. if provider.Auth != nil {
  331. if provider.Auth.NTLM != nil {
  332. client.Transport =
  333. &ntlmssp.Negotiator{
  334. RoundTripper: &http.Transport{
  335. TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, // Needed to disable HTTP/2
  336. },
  337. }
  338. }
  339. // add additional auth methods here
  340. }
  341. // return client with all add-ons
  342. return client, nil
  343. }
  344. func (w *Webhook) GetCACertPool(ctx context.Context, provider *Spec) (*x509.CertPool, error) {
  345. caCertPool := x509.NewCertPool()
  346. ca, err := utils.FetchCACertFromSource(ctx, utils.CreateCertOpts{
  347. CABundle: provider.CABundle,
  348. CAProvider: provider.CAProvider,
  349. StoreKind: w.StoreKind,
  350. Namespace: w.Namespace,
  351. Client: w.Kube,
  352. })
  353. if err != nil {
  354. return nil, err
  355. }
  356. ok := caCertPool.AppendCertsFromPEM(ca)
  357. if !ok {
  358. return nil, errors.New("failed to append cabundle")
  359. }
  360. return caCertPool, 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(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. }