token_getter.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 mysterybox
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. lru "github.com/hashicorp/golang-lru"
  20. "github.com/nebius/gosdk/auth"
  21. "golang.org/x/sync/singleflight"
  22. "k8s.io/utils/clock"
  23. "github.com/external-secrets/external-secrets/providers/v1/nebius/common/sdk/iam"
  24. )
  25. const (
  26. errInvalidSubjectCreds = "invalid subject credentials: malformed JSON"
  27. )
  28. // TokenGetter is an interface for generating and retrieving authentication tokens.
  29. type TokenGetter interface {
  30. GetToken(ctx context.Context, apiDomain, subjectCreds string, caCert []byte) (string, error)
  31. }
  32. type tokenCacheKey struct {
  33. APIDomain string
  34. PublicKeyID string
  35. ServiceAccountID string
  36. PrivateKeyHash string
  37. }
  38. func (k *tokenCacheKey) String() string {
  39. return k.APIDomain + "|" + k.PublicKeyID + "|" + k.ServiceAccountID + "|" + k.PrivateKeyHash
  40. }
  41. // CachedTokenGetter is responsible for managing Nebius IAM token caching and token exchange processes.
  42. type CachedTokenGetter struct {
  43. TokenExchanger iam.TokenExchanger
  44. Clock clock.Clock
  45. tokenCache *lru.Cache
  46. sf singleflight.Group
  47. }
  48. // NewCachedTokenGetter initializes a CachedTokenGetter with the specified cache size, token exchanger, and clock.
  49. // Returns a CachedTokenGetter instance and an error if LRU cache creation fails.
  50. func NewCachedTokenGetter(cacheSize int, tokenExchanger iam.TokenExchanger, clock clock.Clock) (*CachedTokenGetter, error) {
  51. cache, err := lru.New(cacheSize)
  52. if err != nil {
  53. return nil, err
  54. }
  55. return &CachedTokenGetter{
  56. tokenCache: cache,
  57. TokenExchanger: tokenExchanger,
  58. Clock: clock,
  59. }, nil
  60. }
  61. func isTokenExpired(token *iam.Token, clk clock.Clock) bool {
  62. now := clk.Now()
  63. if token.ExpiresAt.After(now) {
  64. total := token.ExpiresAt.Sub(token.IssuedAt)
  65. remaining := token.ExpiresAt.Sub(now)
  66. if remaining > total/10 {
  67. return false
  68. }
  69. }
  70. return true
  71. }
  72. // GetToken retrieves an IAM token for the given API domain and subject credentials, using a cache to optimize requests.
  73. // It exchanges credentials for a new token if no valid cached token exists or the cached token is nearing expiration.
  74. func (c *CachedTokenGetter) GetToken(ctx context.Context, apiDomain, subjectCreds string, caCert []byte) (string, error) {
  75. byteCreds := []byte(subjectCreds)
  76. cacheKey, err := buildTokenCacheKey(byteCreds, apiDomain)
  77. if err != nil {
  78. return "", err
  79. }
  80. value, ok := c.tokenCache.Get(*cacheKey)
  81. if ok {
  82. token := value.(*iam.Token)
  83. tokenExpired := isTokenExpired(token, c.Clock)
  84. if !tokenExpired {
  85. return token.Token, nil
  86. }
  87. }
  88. tokenCacheKeyString := cacheKey.String()
  89. token, err, _ := c.sf.Do(tokenCacheKeyString, func() (any, error) {
  90. if v, ok := c.tokenCache.Get(*cacheKey); ok {
  91. tok := v.(*iam.Token)
  92. if !isTokenExpired(tok, c.Clock) {
  93. return tok.Token, nil
  94. }
  95. }
  96. newToken, err := c.TokenExchanger.ExchangeIamToken(ctx, apiDomain, subjectCreds, c.Clock.Now(), caCert)
  97. if err != nil {
  98. return "", fmt.Errorf("could not exchange creds to iam token: %w", MapGrpcErrors("create token", err))
  99. }
  100. c.tokenCache.Add(*cacheKey, newToken)
  101. return newToken.Token, nil
  102. })
  103. if err != nil {
  104. return "", err
  105. }
  106. return token.(string), nil
  107. }
  108. func buildTokenCacheKey(subjectCreds []byte, apiDomain string) (*tokenCacheKey, error) {
  109. parsedSubjectCreds := &auth.ServiceAccountCredentials{}
  110. err := json.Unmarshal(subjectCreds, parsedSubjectCreds)
  111. if err != nil {
  112. return nil, errors.New(errInvalidSubjectCreds)
  113. }
  114. return &tokenCacheKey{
  115. APIDomain: apiDomain,
  116. PublicKeyID: parsedSubjectCreds.SubjectCredentials.KeyID,
  117. ServiceAccountID: parsedSubjectCreds.SubjectCredentials.Subject,
  118. PrivateKeyHash: HashBytes([]byte(parsedSubjectCreds.SubjectCredentials.PrivateKey)),
  119. }, nil
  120. }
  121. var _ TokenGetter = &CachedTokenGetter{}