webhook.go 9.1 KB

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