client.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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 doppler implements a provider for Doppler secrets management.
  14. package doppler
  15. import (
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "net/url"
  21. "strings"
  22. "time"
  23. corev1 "k8s.io/api/core/v1"
  24. typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
  25. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. dclient "github.com/external-secrets/external-secrets/providers/v1/doppler/client"
  28. "github.com/external-secrets/external-secrets/runtime/esutils"
  29. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  30. "github.com/external-secrets/external-secrets/runtime/find"
  31. )
  32. const (
  33. customBaseURLEnvVar = "DOPPLER_BASE_URL"
  34. verifyTLSOverrideEnvVar = "DOPPLER_VERIFY_TLS"
  35. errGetSecret = "could not get secret %s: %w"
  36. errGetSecrets = "could not get secrets %w"
  37. errDeleteSecrets = "could not delete secrets %s: %w"
  38. errPushSecrets = "could not push secrets %s: %w"
  39. errUnmarshalSecretMap = "unable to unmarshal secret %s: %w"
  40. secretsDownloadFileKey = "DOPPLER_SECRETS_FILE"
  41. errDopplerTokenSecretName = "missing auth.secretRef.dopplerToken.name"
  42. errInvalidClusterStoreMissingDopplerTokenNamespace = "missing auth.secretRef.dopplerToken.namespace"
  43. )
  44. // Client implements the SecretsClient interface for Doppler.
  45. type Client struct {
  46. doppler SecretsClientInterface
  47. dopplerToken string
  48. project string
  49. config string
  50. nameTransformer string
  51. format string
  52. kube kclient.Client
  53. corev1 typedcorev1.CoreV1Interface
  54. store *esv1.DopplerProvider
  55. namespace string
  56. storeKind string
  57. storeName string
  58. oidcManager *OIDCTokenManager
  59. }
  60. // SecretsClientInterface defines the required Doppler Client methods.
  61. type SecretsClientInterface interface {
  62. BaseURL() *url.URL
  63. Authenticate() error
  64. GetSecret(request dclient.SecretRequest) (*dclient.SecretResponse, error)
  65. GetSecrets(request dclient.SecretsRequest) (*dclient.SecretsResponse, error)
  66. UpdateSecrets(request dclient.UpdateSecretsRequest) error
  67. }
  68. func (c *Client) setAuth(ctx context.Context) error {
  69. if c.store.Auth.SecretRef != nil {
  70. token, err := resolvers.SecretKeyRef(
  71. ctx,
  72. c.kube,
  73. c.storeKind,
  74. c.namespace,
  75. &c.store.Auth.SecretRef.DopplerToken)
  76. if err != nil {
  77. return err
  78. }
  79. c.dopplerToken = token
  80. } else if c.store.Auth.OIDCConfig != nil {
  81. token, err := c.oidcManager.Token(ctx)
  82. if err != nil {
  83. return fmt.Errorf("failed to get OIDC token: %w", err)
  84. }
  85. c.dopplerToken = token
  86. } else {
  87. return errors.New("no authentication method configured: either secretRef or oidcConfig must be specified")
  88. }
  89. return nil
  90. }
  91. func (c *Client) refreshAuthIfNeeded(ctx context.Context) error {
  92. if c.store != nil && c.store.Auth != nil && c.store.Auth.OIDCConfig != nil && c.oidcManager != nil {
  93. token, err := c.oidcManager.Token(ctx)
  94. if err != nil {
  95. return fmt.Errorf("failed to refresh OIDC token: %w", err)
  96. }
  97. if doppler, ok := c.doppler.(*dclient.DopplerClient); ok {
  98. doppler.DopplerToken = token
  99. }
  100. }
  101. return nil
  102. }
  103. // Validate validates the Doppler client configuration.
  104. func (c *Client) Validate() (esv1.ValidationResult, error) {
  105. timeout := 15 * time.Second
  106. clientURL := c.doppler.BaseURL().String()
  107. if err := esutils.NetworkValidate(clientURL, timeout); err != nil {
  108. return esv1.ValidationResultError, err
  109. }
  110. if err := c.doppler.Authenticate(); err != nil {
  111. return esv1.ValidationResultError, err
  112. }
  113. return esv1.ValidationResultReady, nil
  114. }
  115. func (c *Client) storeIdentity() storeIdentity {
  116. return storeIdentity{
  117. namespace: c.namespace,
  118. name: c.storeName,
  119. kind: c.storeKind,
  120. }
  121. }
  122. // DeleteSecret removes a secret from Doppler.
  123. func (c *Client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  124. if err := c.refreshAuthIfNeeded(ctx); err != nil {
  125. return err
  126. }
  127. request := dclient.UpdateSecretsRequest{
  128. ChangeRequests: []dclient.Change{
  129. {
  130. Name: ref.GetRemoteKey(),
  131. OriginalName: ref.GetRemoteKey(),
  132. ShouldDelete: true,
  133. },
  134. },
  135. Project: c.project,
  136. Config: c.config,
  137. }
  138. err := c.doppler.UpdateSecrets(request)
  139. if err != nil {
  140. return fmt.Errorf(errDeleteSecrets, ref.GetRemoteKey(), err)
  141. }
  142. etagCache.invalidate(c.storeIdentity())
  143. return nil
  144. }
  145. // SecretExists checks if a secret exists in Doppler.
  146. func (c *Client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  147. return false, errors.New("not implemented")
  148. }
  149. // PushSecret creates or updates a secret in Doppler.
  150. func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  151. if err := c.refreshAuthIfNeeded(ctx); err != nil {
  152. return err
  153. }
  154. request := dclient.UpdateSecretsRequest{
  155. Secrets: dclient.Secrets{
  156. data.GetRemoteKey(): string(secret.Data[data.GetSecretKey()]),
  157. },
  158. Project: c.project,
  159. Config: c.config,
  160. }
  161. err := c.doppler.UpdateSecrets(request)
  162. if err != nil {
  163. return fmt.Errorf(errPushSecrets, data.GetRemoteKey(), err)
  164. }
  165. etagCache.invalidate(c.storeIdentity())
  166. return nil
  167. }
  168. // GetSecret retrieves a secret from Doppler.
  169. func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  170. if err := c.refreshAuthIfNeeded(ctx); err != nil {
  171. return nil, err
  172. }
  173. var etag string
  174. cached, hasCached := etagCache.get(c.storeIdentity(), ref.Key)
  175. if hasCached {
  176. etag = cached.etag
  177. }
  178. request := dclient.SecretRequest{
  179. Name: ref.Key,
  180. Project: c.project,
  181. Config: c.config,
  182. ETag: etag,
  183. }
  184. response, err := c.doppler.GetSecret(request)
  185. if err != nil {
  186. return nil, fmt.Errorf(errGetSecret, ref.Key, err)
  187. }
  188. if !response.Modified && hasCached {
  189. return []byte(cached.secrets[ref.Key]), nil
  190. }
  191. etagCache.set(c.storeIdentity(), ref.Key, &cacheEntry{
  192. etag: response.ETag,
  193. secrets: dclient.Secrets{response.Name: response.Value},
  194. })
  195. return []byte(response.Value), nil
  196. }
  197. // GetSecretMap retrieves a secret from Doppler and returns it as a map.
  198. func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  199. data, err := c.GetSecret(ctx, ref)
  200. if err != nil {
  201. return nil, err
  202. }
  203. kv := make(map[string]json.RawMessage)
  204. err = json.Unmarshal(data, &kv)
  205. if err != nil {
  206. return nil, fmt.Errorf(errUnmarshalSecretMap, ref.Key, err)
  207. }
  208. secretData := make(map[string][]byte)
  209. for k, v := range kv {
  210. var strVal string
  211. err = json.Unmarshal(v, &strVal)
  212. if err == nil {
  213. secretData[k] = []byte(strVal)
  214. } else {
  215. secretData[k] = v
  216. }
  217. }
  218. return secretData, nil
  219. }
  220. // GetAllSecrets retrieves all secrets from Doppler that match the given criteria.
  221. func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  222. secrets, err := c.secrets(ctx)
  223. selected := map[string][]byte{}
  224. if err != nil {
  225. return nil, err
  226. }
  227. if ref.Name == nil && ref.Path == nil {
  228. return secrets, nil
  229. }
  230. var matcher *find.Matcher
  231. if ref.Name != nil {
  232. m, err := find.New(*ref.Name)
  233. if err != nil {
  234. return nil, err
  235. }
  236. matcher = m
  237. }
  238. for key, value := range secrets {
  239. if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
  240. continue
  241. }
  242. selected[key] = value
  243. }
  244. return selected, nil
  245. }
  246. // Close implements cleanup operations for the Doppler client.
  247. func (c *Client) Close(_ context.Context) error {
  248. return nil
  249. }
  250. func (c *Client) secrets(ctx context.Context) (map[string][]byte, error) {
  251. if err := c.refreshAuthIfNeeded(ctx); err != nil {
  252. return nil, err
  253. }
  254. var etag string
  255. cached, hasCached := etagCache.get(c.storeIdentity(), "")
  256. if hasCached {
  257. etag = cached.etag
  258. }
  259. request := dclient.SecretsRequest{
  260. Project: c.project,
  261. Config: c.config,
  262. NameTransformer: c.nameTransformer,
  263. Format: c.format,
  264. ETag: etag,
  265. }
  266. response, err := c.doppler.GetSecrets(request)
  267. if err != nil {
  268. return nil, fmt.Errorf(errGetSecrets, err)
  269. }
  270. if !response.Modified && hasCached {
  271. if c.format != "" {
  272. return map[string][]byte{
  273. secretsDownloadFileKey: cached.body,
  274. }, nil
  275. }
  276. return externalSecretsFormat(cached.secrets), nil
  277. }
  278. etagCache.set(c.storeIdentity(), "", &cacheEntry{
  279. etag: response.ETag,
  280. secrets: response.Secrets,
  281. body: response.Body,
  282. })
  283. if c.format != "" {
  284. return map[string][]byte{
  285. secretsDownloadFileKey: response.Body,
  286. }, nil
  287. }
  288. return externalSecretsFormat(response.Secrets), nil
  289. }
  290. func externalSecretsFormat(secrets dclient.Secrets) map[string][]byte {
  291. converted := make(map[string][]byte, len(secrets))
  292. for key, value := range secrets {
  293. converted[key] = []byte(value)
  294. }
  295. return converted
  296. }