provider.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. kclient "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. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  27. clock2 "github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
  28. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  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 _ esv1.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 NewSecretSetterFunc func()
  78. type AdaptInputFunc func(store esv1.GenericStore) (*SecretsClientInput, error)
  79. type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
  80. type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
  81. type IamToken struct {
  82. Token string
  83. ExpiresAt time.Time
  84. }
  85. type SecretsClientInput struct {
  86. APIEndpoint string
  87. AuthorizedKey *esmeta.SecretKeySelector
  88. CACertificate *esmeta.SecretKeySelector
  89. ResourceKeyType ResourceKeyType
  90. FolderID string
  91. }
  92. type ResourceKeyType int
  93. const (
  94. ResourceKeyTypeId ResourceKeyType = iota
  95. ResourceKeyTypeName ResourceKeyType = iota
  96. )
  97. func (p *YandexCloudProvider) Capabilities() esv1.SecretStoreCapabilities {
  98. return esv1.SecretStoreReadOnly
  99. }
  100. // NewClient constructs a Yandex.Cloud Provider.
  101. func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  102. input, err := p.adaptInputFunc(store)
  103. if err != nil {
  104. return nil, err
  105. }
  106. var authorizedKey *iamkey.Key
  107. if input.AuthorizedKey != nil {
  108. key, err := resolvers.SecretKeyRef(
  109. ctx,
  110. kube,
  111. store.GetKind(),
  112. namespace,
  113. input.AuthorizedKey,
  114. )
  115. if err != nil {
  116. return nil, err
  117. }
  118. authorizedKey = &iamkey.Key{}
  119. err = json.Unmarshal([]byte(key), authorizedKey)
  120. if err != nil {
  121. return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
  122. }
  123. }
  124. var caCertificateData []byte
  125. if input.CACertificate != nil {
  126. caCert, err := resolvers.SecretKeyRef(
  127. ctx,
  128. kube,
  129. store.GetKind(),
  130. namespace,
  131. input.CACertificate,
  132. )
  133. if err != nil {
  134. return nil, err
  135. }
  136. caCertificateData = []byte(caCert)
  137. }
  138. secretGetter, err := p.getOrCreateSecretGetter(ctx, input.APIEndpoint, authorizedKey, caCertificateData)
  139. if err != nil {
  140. return nil, fmt.Errorf("failed to create Yandex.Cloud client: %w", err)
  141. }
  142. iamToken, err := p.getOrCreateIamToken(ctx, input.APIEndpoint, authorizedKey, caCertificateData)
  143. if err != nil {
  144. return nil, fmt.Errorf("failed to create IAM token: %w", err)
  145. }
  146. return &yandexCloudSecretsClient{secretGetter, nil, iamToken.Token, input.ResourceKeyType, input.FolderID}, nil
  147. }
  148. func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {
  149. p.secretGetterMapMutex.Lock()
  150. defer p.secretGetterMapMutex.Unlock()
  151. if _, ok := p.secretGetteMap[apiEndpoint]; !ok {
  152. p.logger.Info("creating SecretGetter", "apiEndpoint", apiEndpoint)
  153. secretGetter, err := p.newSecretGetterFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
  154. if err != nil {
  155. return nil, err
  156. }
  157. p.secretGetteMap[apiEndpoint] = secretGetter
  158. }
  159. return p.secretGetteMap[apiEndpoint], nil
  160. }
  161. func (p *YandexCloudProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
  162. p.iamTokenMapMutex.Lock()
  163. defer p.iamTokenMapMutex.Unlock()
  164. iamTokenKey := buildIamTokenKey(authorizedKey)
  165. if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
  166. if authorizedKey != nil {
  167. p.logger.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
  168. } else {
  169. p.logger.Info("creating instance SA IAM token")
  170. }
  171. iamToken, err := p.newIamTokenFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
  172. if err != nil {
  173. return nil, err
  174. }
  175. if authorizedKey != nil {
  176. p.logger.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
  177. } else {
  178. p.logger.Info("created instance SA IAM token", "expiresAt", iamToken.ExpiresAt)
  179. }
  180. p.iamTokenMap[iamTokenKey] = iamToken
  181. }
  182. return p.iamTokenMap[iamTokenKey], nil
  183. }
  184. func (p *YandexCloudProvider) isIamTokenUsable(iamToken *IamToken) bool {
  185. now := p.clock.CurrentTime()
  186. return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
  187. }
  188. func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
  189. if authorizedKey == nil {
  190. return iamTokenKey{}
  191. }
  192. privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
  193. return iamTokenKey{
  194. authorizedKey.GetId(),
  195. authorizedKey.GetServiceAccountId(),
  196. hex.EncodeToString(privateKeyHash[:]),
  197. }
  198. }
  199. // Used for testing.
  200. func (p *YandexCloudProvider) IsIamTokenCached(authorizedKey *iamkey.Key) bool {
  201. p.iamTokenMapMutex.Lock()
  202. defer p.iamTokenMapMutex.Unlock()
  203. _, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
  204. return ok
  205. }
  206. func (p *YandexCloudProvider) CleanUpIamTokenMap() {
  207. p.iamTokenMapMutex.Lock()
  208. defer p.iamTokenMapMutex.Unlock()
  209. for key, value := range p.iamTokenMap {
  210. if p.clock.CurrentTime().After(value.ExpiresAt) {
  211. p.logger.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
  212. delete(p.iamTokenMap, key)
  213. }
  214. }
  215. }
  216. func (p *YandexCloudProvider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  217. _, err := p.adaptInputFunc(store) // adaptInputFunc validates the input store
  218. if err != nil {
  219. return nil, err
  220. }
  221. return nil, nil
  222. }