webhook.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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/PaesslerAG/jsonpath"
  26. corev1 "k8s.io/api/core/v1"
  27. "sigs.k8s.io/controller-runtime/pkg/client"
  28. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  29. "github.com/external-secrets/external-secrets/pkg/constants"
  30. "github.com/external-secrets/external-secrets/pkg/metrics"
  31. "github.com/external-secrets/external-secrets/pkg/template/v2"
  32. "github.com/external-secrets/external-secrets/pkg/utils"
  33. )
  34. type Webhook struct {
  35. Kube client.Client
  36. Namespace string
  37. StoreKind string
  38. HTTP *http.Client
  39. EnforceLabels bool
  40. ClusterScoped bool
  41. }
  42. func (w *Webhook) getStoreSecret(ctx context.Context, ref SecretKeySelector) (*corev1.Secret, error) {
  43. ke := client.ObjectKey{
  44. Name: ref.Name,
  45. Namespace: w.Namespace,
  46. }
  47. if w.ClusterScoped {
  48. if ref.Namespace == nil {
  49. return nil, fmt.Errorf("no namespace on ClusterScoped webhook secret %s", ref.Name)
  50. }
  51. ke.Namespace = *ref.Namespace
  52. }
  53. secret := &corev1.Secret{}
  54. if err := w.Kube.Get(ctx, ke, secret); err != nil {
  55. return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
  56. }
  57. if w.EnforceLabels {
  58. expected, ok := secret.Labels["external-secrets.io/type"]
  59. if !ok {
  60. return nil, errors.New("secret does not contain needed label 'external-secrets.io/type: webhook'. Update secret label to use it with webhook")
  61. }
  62. if expected != "webhook" {
  63. return nil, errors.New("secret type is not 'webhook'")
  64. }
  65. }
  66. return secret, nil
  67. }
  68. func (w *Webhook) GetSecretMap(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  69. result, err := w.GetWebhookData(ctx, provider, ref)
  70. if err != nil {
  71. return nil, err
  72. }
  73. // We always want json here, so just parse it out
  74. jsondata := any(nil)
  75. if err := json.Unmarshal(result, &jsondata); err != nil {
  76. return nil, fmt.Errorf("failed to parse response json: %w", err)
  77. }
  78. // Get subdata via jsonpath, if given
  79. if provider.Result.JSONPath != "" {
  80. jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
  81. if err != nil {
  82. return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
  83. }
  84. }
  85. // If the value is a string, try to parse it as json
  86. jsonstring, ok := jsondata.(string)
  87. if ok {
  88. // This could also happen if the response was a single json-encoded string
  89. // but that is an extremely unlikely scenario
  90. if err := json.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
  91. return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
  92. }
  93. }
  94. // Use the data as a key-value map
  95. jsonvalue, ok := jsondata.(map[string]any)
  96. if !ok {
  97. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  98. }
  99. // Change the map of generic objects to a map of byte arrays
  100. values := make(map[string][]byte)
  101. for rKey := range jsonvalue {
  102. values[rKey], err = utils.GetByteValueFromMap(jsonvalue, rKey)
  103. if err != nil {
  104. return nil, fmt.Errorf("failed to get response for key '%s': %w", rKey, err)
  105. }
  106. }
  107. return values, nil
  108. }
  109. func (w *Webhook) GetTemplateData(ctx context.Context, ref *esv1beta1.ExternalSecretDataRemoteRef, secrets []Secret, urlEncode bool) (map[string]map[string]string, error) {
  110. data := map[string]map[string]string{}
  111. if ref != nil {
  112. if urlEncode {
  113. data["remoteRef"] = map[string]string{
  114. "key": url.QueryEscape(ref.Key),
  115. "version": url.QueryEscape(ref.Version),
  116. "property": url.QueryEscape(ref.Property),
  117. }
  118. } else {
  119. data["remoteRef"] = map[string]string{
  120. "key": ref.Key,
  121. "version": ref.Version,
  122. "property": ref.Property,
  123. }
  124. }
  125. }
  126. if err := w.getTemplatedSecrets(ctx, secrets, data); err != nil {
  127. return nil, err
  128. }
  129. return data, nil
  130. }
  131. func (w *Webhook) getTemplatedSecrets(ctx context.Context, secrets []Secret, data map[string]map[string]string) error {
  132. for _, secref := range secrets {
  133. if _, ok := data[secref.Name]; !ok {
  134. data[secref.Name] = make(map[string]string)
  135. }
  136. secret, err := w.getStoreSecret(ctx, secref.SecretRef)
  137. if err != nil {
  138. return err
  139. }
  140. for sKey, sVal := range secret.Data {
  141. data[secref.Name][sKey] = string(sVal)
  142. }
  143. }
  144. return nil
  145. }
  146. func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  147. if w.HTTP == nil {
  148. return nil, errors.New("http client not initialized")
  149. }
  150. escapedData, err := w.GetTemplateData(ctx, ref, provider.Secrets, true)
  151. if err != nil {
  152. return nil, err
  153. }
  154. rawData, err := w.GetTemplateData(ctx, ref, provider.Secrets, false)
  155. if err != nil {
  156. return nil, err
  157. }
  158. method := provider.Method
  159. if method == "" {
  160. method = http.MethodGet
  161. }
  162. url, err := ExecuteTemplateString(provider.URL, escapedData)
  163. if err != nil {
  164. return nil, fmt.Errorf("failed to parse url: %w", err)
  165. }
  166. body, err := ExecuteTemplate(provider.Body, rawData)
  167. if err != nil {
  168. return nil, fmt.Errorf("failed to parse body: %w", err)
  169. }
  170. return w.executeRequest(ctx, provider, body.Bytes(), url, method, rawData)
  171. }
  172. func (w *Webhook) PushWebhookData(ctx context.Context, provider *Spec, data []byte, remoteKey esv1beta1.PushSecretData) error {
  173. if w.HTTP == nil {
  174. return errors.New("http client not initialized")
  175. }
  176. method := provider.Method
  177. if method == "" {
  178. method = http.MethodPost
  179. }
  180. rawData := map[string]map[string]string{
  181. "remoteRef": {
  182. "remoteKey": url.QueryEscape(remoteKey.GetRemoteKey()),
  183. },
  184. }
  185. if remoteKey.GetSecretKey() != "" {
  186. rawData["remoteRef"]["secretKey"] = url.QueryEscape(remoteKey.GetSecretKey())
  187. }
  188. turl, err := ExecuteTemplateString(provider.URL, rawData)
  189. if err != nil {
  190. return fmt.Errorf("failed to parse url: %w", err)
  191. }
  192. dataMap := make(map[string]map[string]string)
  193. if err := w.getTemplatedSecrets(ctx, provider.Secrets, dataMap); err != nil {
  194. return err
  195. }
  196. // read the body into the void to prevent remaining garbage to be present
  197. if _, err := w.executeRequest(ctx, provider, data, turl, method, dataMap); err != nil {
  198. return fmt.Errorf("failed to push webhook data: %w", err)
  199. }
  200. return nil
  201. }
  202. func (w *Webhook) executeRequest(ctx context.Context, provider *Spec, data []byte, url, method string, rawData map[string]map[string]string) ([]byte, error) {
  203. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
  204. if err != nil {
  205. return nil, fmt.Errorf("failed to create request: %w", err)
  206. }
  207. for hKey, hValueTpl := range provider.Headers {
  208. hValue, err := ExecuteTemplateString(hValueTpl, rawData)
  209. if err != nil {
  210. return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
  211. }
  212. req.Header.Add(hKey, hValue)
  213. }
  214. resp, err := w.HTTP.Do(req)
  215. metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
  216. if err != nil {
  217. return nil, fmt.Errorf("failed to call endpoint: %w", err)
  218. }
  219. defer resp.Body.Close()
  220. if resp.StatusCode == 404 {
  221. return nil, esv1beta1.NoSecretError{}
  222. }
  223. if resp.StatusCode == http.StatusNotModified {
  224. return nil, esv1beta1.NotModifiedError{}
  225. }
  226. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  227. return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
  228. }
  229. return io.ReadAll(resp.Body)
  230. }
  231. func (w *Webhook) GetHTTPClient(ctx context.Context, provider *Spec) (*http.Client, error) {
  232. client := &http.Client{}
  233. if provider.Timeout != nil {
  234. client.Timeout = provider.Timeout.Duration
  235. }
  236. if len(provider.CABundle) == 0 && provider.CAProvider == nil {
  237. // No need to process ca stuff if it is not there
  238. return client, nil
  239. }
  240. caCertPool, err := w.GetCACertPool(ctx, provider)
  241. if err != nil {
  242. return nil, err
  243. }
  244. tlsConf := &tls.Config{
  245. RootCAs: caCertPool,
  246. MinVersion: tls.VersionTLS12,
  247. Renegotiation: tls.RenegotiateOnceAsClient,
  248. }
  249. client.Transport = &http.Transport{TLSClientConfig: tlsConf}
  250. return client, nil
  251. }
  252. func (w *Webhook) GetCACertPool(ctx context.Context, provider *Spec) (*x509.CertPool, error) {
  253. caCertPool := x509.NewCertPool()
  254. ca, err := utils.FetchCACertFromSource(ctx, utils.CreateCertOpts{
  255. CABundle: provider.CABundle,
  256. CAProvider: provider.CAProvider,
  257. StoreKind: w.StoreKind,
  258. Namespace: w.Namespace,
  259. Client: w.Kube,
  260. })
  261. if err != nil {
  262. return nil, err
  263. }
  264. ok := caCertPool.AppendCertsFromPEM(ca)
  265. if !ok {
  266. return nil, errors.New("failed to append cabundle")
  267. }
  268. return caCertPool, nil
  269. }
  270. func ExecuteTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
  271. result, err := ExecuteTemplate(tmpl, data)
  272. if err != nil {
  273. return "", err
  274. }
  275. return result.String(), nil
  276. }
  277. func ExecuteTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
  278. var result bytes.Buffer
  279. if tmpl == "" {
  280. return result, nil
  281. }
  282. urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
  283. if err != nil {
  284. return result, err
  285. }
  286. if err := urlt.Execute(&result, data); err != nil {
  287. return result, err
  288. }
  289. return result, nil
  290. }