webhook.go 9.2 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. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  30. "github.com/external-secrets/external-secrets/pkg/constants"
  31. "github.com/external-secrets/external-secrets/pkg/metrics"
  32. "github.com/external-secrets/external-secrets/pkg/template/v2"
  33. "github.com/external-secrets/external-secrets/pkg/utils"
  34. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  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 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 *esv1beta1.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 *esv1beta1.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. }
  120. } else {
  121. data["remoteRef"] = map[string]string{
  122. "key": ref.Key,
  123. "version": ref.Version,
  124. "property": ref.Property,
  125. }
  126. }
  127. }
  128. for _, secref := range secrets {
  129. if _, ok := data[secref.Name]; !ok {
  130. data[secref.Name] = make(map[string]string)
  131. }
  132. secret, err := w.getStoreSecret(ctx, secref.SecretRef)
  133. if err != nil {
  134. return nil, err
  135. }
  136. for sKey, sVal := range secret.Data {
  137. data[secref.Name][sKey] = string(sVal)
  138. }
  139. }
  140. return data, nil
  141. }
  142. func (w *Webhook) GetWebhookData(ctx context.Context, provider *Spec, ref *esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  143. if w.HTTP == nil {
  144. return nil, errors.New("http client not initialized")
  145. }
  146. escapedData, err := w.GetTemplateData(ctx, ref, provider.Secrets, true)
  147. if err != nil {
  148. return nil, err
  149. }
  150. rawData, err := w.GetTemplateData(ctx, ref, provider.Secrets, false)
  151. if err != nil {
  152. return nil, err
  153. }
  154. method := provider.Method
  155. if method == "" {
  156. method = http.MethodGet
  157. }
  158. url, err := ExecuteTemplateString(provider.URL, escapedData)
  159. if err != nil {
  160. return nil, fmt.Errorf("failed to parse url: %w", err)
  161. }
  162. body, err := ExecuteTemplate(provider.Body, rawData)
  163. if err != nil {
  164. return nil, fmt.Errorf("failed to parse body: %w", err)
  165. }
  166. req, err := http.NewRequestWithContext(ctx, method, url, &body)
  167. if err != nil {
  168. return nil, fmt.Errorf("failed to create request: %w", err)
  169. }
  170. for hKey, hValueTpl := range provider.Headers {
  171. hValue, err := ExecuteTemplateString(hValueTpl, rawData)
  172. if err != nil {
  173. return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
  174. }
  175. req.Header.Add(hKey, hValue)
  176. }
  177. resp, err := w.HTTP.Do(req)
  178. metrics.ObserveAPICall(constants.ProviderWebhook, constants.CallWebhookHTTPReq, err)
  179. if err != nil {
  180. return nil, fmt.Errorf("failed to call endpoint: %w", err)
  181. }
  182. defer resp.Body.Close()
  183. if resp.StatusCode == 404 {
  184. return nil, esv1beta1.NoSecretError{}
  185. }
  186. if resp.StatusCode == http.StatusNotModified {
  187. return nil, esv1beta1.NotModifiedError{}
  188. }
  189. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  190. return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
  191. }
  192. return io.ReadAll(resp.Body)
  193. }
  194. func (w *Webhook) GetHTTPClient(ctx context.Context, provider *Spec) (*http.Client, error) {
  195. client := &http.Client{}
  196. if provider.Timeout != nil {
  197. client.Timeout = provider.Timeout.Duration
  198. }
  199. if len(provider.CABundle) == 0 && provider.CAProvider == nil {
  200. // No need to process ca stuff if it is not there
  201. return client, nil
  202. }
  203. caCertPool, err := w.GetCACertPool(ctx, provider)
  204. if err != nil {
  205. return nil, err
  206. }
  207. tlsConf := &tls.Config{
  208. RootCAs: caCertPool,
  209. MinVersion: tls.VersionTLS12,
  210. Renegotiation: tls.RenegotiateOnceAsClient,
  211. }
  212. client.Transport = &http.Transport{TLSClientConfig: tlsConf}
  213. return client, nil
  214. }
  215. func (w *Webhook) GetCACertPool(ctx context.Context, provider *Spec) (*x509.CertPool, error) {
  216. caCertPool := x509.NewCertPool()
  217. ca, err := utils.FetchCACertFromSource(ctx, utils.CreateCertOpts{
  218. CABundle: provider.CABundle,
  219. CAProvider: provider.CAProvider,
  220. StoreKind: w.StoreKind,
  221. Namespace: w.Namespace,
  222. Client: w.Kube,
  223. })
  224. if err != nil {
  225. return nil, err
  226. }
  227. ok := caCertPool.AppendCertsFromPEM(ca)
  228. if !ok {
  229. return nil, errors.New("failed to append cabundle")
  230. }
  231. return caCertPool, nil
  232. }
  233. func (w *Webhook) GetCertFromSecret(provider *Spec) ([]byte, error) {
  234. secretRef := esmeta.SecretKeySelector{
  235. Name: provider.CAProvider.Name,
  236. Namespace: &w.Namespace,
  237. Key: provider.CAProvider.Key,
  238. }
  239. if provider.CAProvider.Namespace != nil {
  240. secretRef.Namespace = provider.CAProvider.Namespace
  241. }
  242. ctx := context.Background()
  243. cert, err := resolvers.SecretKeyRef(
  244. ctx,
  245. w.Kube,
  246. w.StoreKind,
  247. w.Namespace,
  248. &secretRef,
  249. )
  250. if err != nil {
  251. return nil, err
  252. }
  253. return []byte(cert), nil
  254. }
  255. func (w *Webhook) GetCertFromConfigMap(provider *Spec) ([]byte, error) {
  256. objKey := client.ObjectKey{
  257. Name: provider.CAProvider.Name,
  258. }
  259. if provider.CAProvider.Namespace != nil {
  260. objKey.Namespace = *provider.CAProvider.Namespace
  261. }
  262. configMapRef := &corev1.ConfigMap{}
  263. ctx := context.Background()
  264. err := w.Kube.Get(ctx, objKey, configMapRef)
  265. if err != nil {
  266. return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
  267. }
  268. val, ok := configMapRef.Data[provider.CAProvider.Key]
  269. if !ok {
  270. return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
  271. }
  272. return []byte(val), nil
  273. }
  274. func ExecuteTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
  275. result, err := ExecuteTemplate(tmpl, data)
  276. if err != nil {
  277. return "", err
  278. }
  279. return result.String(), nil
  280. }
  281. func ExecuteTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
  282. var result bytes.Buffer
  283. if tmpl == "" {
  284. return result, nil
  285. }
  286. urlt, err := tpl.New("webhooktemplate").Funcs(template.FuncMap()).Parse(tmpl)
  287. if err != nil {
  288. return result, err
  289. }
  290. if err := urlt.Execute(&result, data); err != nil {
  291. return result, err
  292. }
  293. return result, nil
  294. }