provider.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 common
  13. import (
  14. "context"
  15. "crypto/sha256"
  16. "encoding/hex"
  17. "encoding/json"
  18. "fmt"
  19. "sync"
  20. "time"
  21. "github.com/go-logr/logr"
  22. "github.com/yandex-cloud/go-sdk/iamkey"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  26. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  27. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  28. clock2 "github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
  29. )
  30. const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
  31. // https://github.com/external-secrets/external-secrets/issues/644
  32. var _ esv1beta1.Provider = &YandexCloudProvider{}
  33. // Implementation of v1beta1.Provider.
  34. type YandexCloudProvider struct {
  35. logger logr.Logger
  36. clock clock2.Clock
  37. adaptInputFunc AdaptInputFunc
  38. newSecretGetterFunc NewSecretGetterFunc
  39. newIamTokenFunc NewIamTokenFunc
  40. secretGetteMap map[string]SecretGetter // apiEndpoint -> SecretGetter
  41. secretGetterMapMutex sync.Mutex
  42. iamTokenMap map[iamTokenKey]*IamToken
  43. iamTokenMapMutex sync.Mutex
  44. }
  45. type iamTokenKey struct {
  46. authorizedKeyID string
  47. serviceAccountID string
  48. privateKeyHash string
  49. }
  50. func InitYandexCloudProvider(
  51. logger logr.Logger,
  52. clock clock2.Clock,
  53. adaptInputFunc AdaptInputFunc,
  54. newSecretGetterFunc NewSecretGetterFunc,
  55. newIamTokenFunc NewIamTokenFunc,
  56. iamTokenCleanupDelay time.Duration,
  57. ) *YandexCloudProvider {
  58. provider := &YandexCloudProvider{
  59. logger: logger,
  60. clock: clock,
  61. adaptInputFunc: adaptInputFunc,
  62. newSecretGetterFunc: newSecretGetterFunc,
  63. newIamTokenFunc: newIamTokenFunc,
  64. secretGetteMap: make(map[string]SecretGetter),
  65. iamTokenMap: make(map[iamTokenKey]*IamToken),
  66. }
  67. if iamTokenCleanupDelay > 0 {
  68. go func() {
  69. for {
  70. time.Sleep(iamTokenCleanupDelay)
  71. provider.CleanUpIamTokenMap()
  72. }
  73. }()
  74. }
  75. return provider
  76. }
  77. type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error)
  78. type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
  79. type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
  80. type IamToken struct {
  81. Token string
  82. ExpiresAt time.Time
  83. }
  84. type SecretsClientInput struct {
  85. APIEndpoint string
  86. AuthorizedKey esmeta.SecretKeySelector
  87. CACertificate *esmeta.SecretKeySelector
  88. }
  89. // NewClient constructs a Yandex.Cloud Provider.
  90. func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
  91. input, err := p.adaptInputFunc(store)
  92. if err != nil {
  93. return nil, err
  94. }
  95. objectKey := types.NamespacedName{
  96. Name: input.AuthorizedKey.Name,
  97. Namespace: namespace,
  98. }
  99. // only ClusterStore is allowed to set namespace (and then it's required)
  100. if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
  101. if input.AuthorizedKey.Namespace == nil {
  102. return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
  103. }
  104. objectKey.Namespace = *input.AuthorizedKey.Namespace
  105. }
  106. authorizedKeySecret := &corev1.Secret{}
  107. err = kube.Get(ctx, objectKey, authorizedKeySecret)
  108. if err != nil {
  109. return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
  110. }
  111. authorizedKeySecretData := authorizedKeySecret.Data[input.AuthorizedKey.Key]
  112. if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
  113. return nil, fmt.Errorf("missing AuthorizedKey")
  114. }
  115. var authorizedKey iamkey.Key
  116. err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
  117. if err != nil {
  118. return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
  119. }
  120. var caCertificateData []byte
  121. if input.CACertificate != nil {
  122. certObjectKey := types.NamespacedName{
  123. Name: input.CACertificate.Name,
  124. Namespace: namespace,
  125. }
  126. if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
  127. if input.CACertificate.Namespace == nil {
  128. return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
  129. }
  130. certObjectKey.Namespace = *input.CACertificate.Namespace
  131. }
  132. caCertificateSecret := &corev1.Secret{}
  133. err := kube.Get(ctx, certObjectKey, caCertificateSecret)
  134. if err != nil {
  135. return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
  136. }
  137. caCertificateData = caCertificateSecret.Data[input.CACertificate.Key]
  138. if (caCertificateData == nil) || (len(caCertificateData) == 0) {
  139. return nil, fmt.Errorf("missing CA Certificate")
  140. }
  141. }
  142. secretGetter, err := p.getOrCreateSecretGetter(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
  143. if err != nil {
  144. return nil, fmt.Errorf("failed to create Yandex.Cloud client: %w", err)
  145. }
  146. iamToken, err := p.getOrCreateIamToken(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
  147. if err != nil {
  148. return nil, fmt.Errorf("failed to create IAM token: %w", err)
  149. }
  150. return &yandexCloudSecretsClient{secretGetter, iamToken.Token}, nil
  151. }
  152. func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {
  153. p.secretGetterMapMutex.Lock()
  154. defer p.secretGetterMapMutex.Unlock()
  155. if _, ok := p.secretGetteMap[apiEndpoint]; !ok {
  156. p.logger.Info("creating SecretGetter", "apiEndpoint", apiEndpoint)
  157. secretGetter, err := p.newSecretGetterFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
  158. if err != nil {
  159. return nil, err
  160. }
  161. p.secretGetteMap[apiEndpoint] = secretGetter
  162. }
  163. return p.secretGetteMap[apiEndpoint], nil
  164. }
  165. func (p *YandexCloudProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
  166. p.iamTokenMapMutex.Lock()
  167. defer p.iamTokenMapMutex.Unlock()
  168. iamTokenKey := buildIamTokenKey(authorizedKey)
  169. if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
  170. p.logger.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
  171. iamToken, err := p.newIamTokenFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
  172. if err != nil {
  173. return nil, err
  174. }
  175. p.logger.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
  176. p.iamTokenMap[iamTokenKey] = iamToken
  177. }
  178. return p.iamTokenMap[iamTokenKey], nil
  179. }
  180. func (p *YandexCloudProvider) isIamTokenUsable(iamToken *IamToken) bool {
  181. now := p.clock.CurrentTime()
  182. return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
  183. }
  184. func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
  185. privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
  186. return iamTokenKey{
  187. authorizedKey.GetId(),
  188. authorizedKey.GetServiceAccountId(),
  189. hex.EncodeToString(privateKeyHash[:]),
  190. }
  191. }
  192. // Used for testing.
  193. func (p *YandexCloudProvider) IsIamTokenCached(authorizedKey *iamkey.Key) bool {
  194. p.iamTokenMapMutex.Lock()
  195. defer p.iamTokenMapMutex.Unlock()
  196. _, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
  197. return ok
  198. }
  199. func (p *YandexCloudProvider) CleanUpIamTokenMap() {
  200. p.iamTokenMapMutex.Lock()
  201. defer p.iamTokenMapMutex.Unlock()
  202. for key, value := range p.iamTokenMap {
  203. if p.clock.CurrentTime().After(value.ExpiresAt) {
  204. p.logger.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
  205. delete(p.iamTokenMap, key)
  206. }
  207. }
  208. }
  209. func (p *YandexCloudProvider) ValidateStore(store esv1beta1.GenericStore) error {
  210. _, err := p.adaptInputFunc(store) // adaptInputFunc validates the input store
  211. if err != nil {
  212. return err
  213. }
  214. return nil
  215. }