webhook.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "strconv"
  20. "time"
  21. "github.com/PaesslerAG/jsonpath"
  22. corev1 "k8s.io/api/core/v1"
  23. "sigs.k8s.io/controller-runtime/pkg/client"
  24. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  25. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  26. "github.com/external-secrets/external-secrets/pkg/common/webhook"
  27. "github.com/external-secrets/external-secrets/pkg/esutils"
  28. )
  29. const (
  30. errNotImplemented = "not implemented"
  31. errFailedToGetStore = "failed to get store: %w"
  32. )
  33. // https://github.com/external-secrets/external-secrets/issues/644
  34. var _ esv1.SecretsClient = &WebHook{}
  35. var _ esv1.Provider = &Provider{}
  36. // Provider satisfies the provider interface.
  37. type Provider struct{}
  38. // WebHook implements the External Secrets webhook provider.
  39. type WebHook struct {
  40. wh webhook.Webhook
  41. store esv1.GenericStore
  42. storeKind string
  43. url string
  44. }
  45. func init() {
  46. esv1.Register(&Provider{}, &esv1.SecretStoreProvider{
  47. Webhook: &esv1.WebhookProvider{},
  48. }, esv1.MaintenanceStatusMaintained)
  49. }
  50. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  51. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  52. return esv1.SecretStoreReadOnly
  53. }
  54. // NewClient creates a new WebHook client for accessing secrets.
  55. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
  56. wh := webhook.Webhook{
  57. Kube: kube,
  58. Namespace: namespace,
  59. StoreKind: store.GetObjectKind().GroupVersionKind().Kind,
  60. }
  61. whClient := &WebHook{
  62. store: store,
  63. wh: wh,
  64. storeKind: store.GetObjectKind().GroupVersionKind().Kind,
  65. }
  66. whClient.wh.EnforceLabels = true
  67. if whClient.storeKind == esv1.ClusterSecretStoreKind {
  68. whClient.wh.ClusterScoped = true
  69. }
  70. provider, err := getProvider(store)
  71. if err != nil {
  72. return nil, err
  73. }
  74. whClient.url = provider.URL
  75. whClient.wh.HTTP, err = whClient.wh.GetHTTPClient(ctx, provider)
  76. if err != nil {
  77. return nil, err
  78. }
  79. return whClient, nil
  80. }
  81. // ValidateStore validates the provider-specific store configuration.
  82. func (p *Provider) ValidateStore(_ esv1.GenericStore) (admission.Warnings, error) {
  83. return nil, nil
  84. }
  85. func getProvider(store esv1.GenericStore) (*webhook.Spec, error) {
  86. spc := store.GetSpec()
  87. if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
  88. return nil, errors.New("missing store provider webhook")
  89. }
  90. out := webhook.Spec{}
  91. d, err := json.Marshal(spc.Provider.Webhook)
  92. if err != nil {
  93. return nil, err
  94. }
  95. err = json.Unmarshal(d, &out)
  96. return &out, err
  97. }
  98. // DeleteSecret deletes a secret from a remote store.
  99. func (w *WebHook) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  100. return errors.New(errNotImplemented)
  101. }
  102. // SecretExists checks if a secret exists in the remote store.
  103. func (w *WebHook) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  104. return false, errors.New(errNotImplemented)
  105. }
  106. // PushSecret pushes a secret to a remote store.
  107. func (w *WebHook) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  108. if data.GetRemoteKey() == "" {
  109. return errors.New("remote key must be defined")
  110. }
  111. provider, err := getProvider(w.store)
  112. if err != nil {
  113. return fmt.Errorf(errFailedToGetStore, err)
  114. }
  115. value, err := esutils.ExtractSecretData(data, secret)
  116. if err != nil {
  117. return err
  118. }
  119. if err := w.wh.PushWebhookData(ctx, provider, value, data); err != nil {
  120. return fmt.Errorf("failed to push webhook data: %w", err)
  121. }
  122. return nil
  123. }
  124. // GetAllSecrets Empty .
  125. func (w *WebHook) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  126. // TO be implemented
  127. return nil, errors.New(errNotImplemented)
  128. }
  129. // GetSecret gets a secret from the remote store.
  130. func (w *WebHook) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  131. provider, err := getProvider(w.store)
  132. if err != nil {
  133. return nil, fmt.Errorf(errFailedToGetStore, err)
  134. }
  135. result, err := w.wh.GetWebhookData(ctx, provider, &ref)
  136. if err != nil {
  137. return nil, err
  138. }
  139. // Only parse as json if we have a jsonpath set
  140. data, err := w.wh.GetTemplateData(ctx, &ref, provider.Secrets, false)
  141. if err != nil {
  142. return nil, err
  143. }
  144. resultJSONPath, err := webhook.ExecuteTemplateString(provider.Result.JSONPath, data)
  145. if err != nil {
  146. return nil, err
  147. }
  148. if resultJSONPath != "" {
  149. jsondata := any(nil)
  150. if err := json.Unmarshal(result, &jsondata); err != nil {
  151. return nil, fmt.Errorf("failed to parse response json: %w", err)
  152. }
  153. jsondata, err = jsonpath.Get(resultJSONPath, jsondata)
  154. if err != nil {
  155. return nil, fmt.Errorf("failed to get response path %s: %w", resultJSONPath, err)
  156. }
  157. return extractSecretData(jsondata)
  158. }
  159. return result, nil
  160. }
  161. // tries to extract data from an any
  162. // it is supposed to return a single value.
  163. func extractSecretData(jsondata any) ([]byte, error) {
  164. switch val := jsondata.(type) {
  165. case bool:
  166. return []byte(strconv.FormatBool(val)), nil
  167. case nil:
  168. return []byte{}, nil
  169. case int:
  170. return []byte(strconv.Itoa(val)), nil
  171. case float64:
  172. return []byte(strconv.FormatFloat(val, 'f', 0, 64)), nil
  173. case []byte:
  174. return val, nil
  175. case string:
  176. return []byte(val), nil
  177. // due to backwards compatibility we must keep this!
  178. // in case we see a []something we pick the first element and return it
  179. case []any:
  180. if len(val) == 0 {
  181. return nil, errors.New("filter worked but didn't get any result")
  182. }
  183. return extractSecretData(val[0])
  184. // in case we encounter a map we serialize it instead of erroring out
  185. // The user should use that data from within a template and figure
  186. // out how to deal with it.
  187. case map[string]any:
  188. return json.Marshal(val)
  189. default:
  190. return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
  191. }
  192. }
  193. // GetSecretMap gets a map of secrets from the remote store.
  194. func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  195. provider, err := getProvider(w.store)
  196. if err != nil {
  197. return nil, fmt.Errorf(errFailedToGetStore, err)
  198. }
  199. data, err := w.wh.GetTemplateData(ctx, &ref, provider.Secrets, false)
  200. if err != nil {
  201. return nil, fmt.Errorf("cannot get template data: %w", err)
  202. }
  203. resultJSONPath, err := webhook.ExecuteTemplateString(provider.Result.JSONPath, data)
  204. if err != nil {
  205. return nil, fmt.Errorf("cannot get templated json path: %w", err)
  206. }
  207. provider.Result.JSONPath = resultJSONPath
  208. return w.wh.GetSecretMap(ctx, provider, &ref)
  209. }
  210. // Close closes the connection to the webhook provider.
  211. func (w *WebHook) Close(_ context.Context) error {
  212. return nil
  213. }
  214. // Validate checks if the webhook provider is configured correctly.
  215. func (w *WebHook) Validate() (esv1.ValidationResult, error) {
  216. timeout := 15 * time.Second
  217. url := w.url
  218. if err := esutils.NetworkValidate(url, timeout); err != nil {
  219. return esv1.ValidationResultError, err
  220. }
  221. return esv1.ValidationResultReady, nil
  222. }