webhook.go 9.4 KB

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