webhook.go 12 KB

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