provider.go 7.7 KB

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