provider.go 8.9 KB

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