csm_client.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 adapter
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "sync"
  18. "time"
  19. iamAuthV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
  20. smssdk "github.com/cloudru-tech/secret-manager-sdk"
  21. smsV1 "github.com/cloudru-tech/secret-manager-sdk/api/v1"
  22. smsV2 "github.com/cloudru-tech/secret-manager-sdk/api/v2"
  23. "google.golang.org/grpc/codes"
  24. "google.golang.org/grpc/metadata"
  25. "google.golang.org/grpc/status"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. )
  28. // CredentialsResolver returns the actual client credentials.
  29. type CredentialsResolver interface {
  30. Resolve(ctx context.Context) (*Credentials, error)
  31. }
  32. // APIClient - Cloudru Secret Manager Service Client.
  33. type APIClient struct {
  34. cr CredentialsResolver
  35. iamClient iamAuthV1.AuthServiceClient
  36. smsClient *smssdk.Client
  37. mu sync.Mutex
  38. accessToken string
  39. accessTokenExpiresAt time.Time
  40. }
  41. // ListSecretsRequest is a request to list secrets.
  42. type ListSecretsRequest struct {
  43. ProjectID string
  44. Labels map[string]string
  45. NameExact string
  46. NameRegex string
  47. }
  48. // Credentials holds the keyID and secret for the CSM client.
  49. type Credentials struct {
  50. KeyID string
  51. Secret string
  52. }
  53. // NewCredentials creates a new Credentials object.
  54. func NewCredentials(kid, secret string) (*Credentials, error) {
  55. if kid == "" || secret == "" {
  56. return nil, errors.New("keyID and secret must be provided")
  57. }
  58. return &Credentials{KeyID: kid, Secret: secret}, nil
  59. }
  60. // NewAPIClient creates a new grpc SecretManager client.
  61. func NewAPIClient(cr CredentialsResolver, iamClient iamAuthV1.AuthServiceClient, client *smssdk.Client) *APIClient {
  62. return &APIClient{
  63. cr: cr,
  64. iamClient: iamClient,
  65. smsClient: client,
  66. }
  67. }
  68. func (c *APIClient) ListSecrets(ctx context.Context, req *ListSecretsRequest) ([]*smsV2.Secret, error) {
  69. searchReq := &smsV2.SearchSecretRequest{
  70. ProjectId: req.ProjectID,
  71. Labels: req.Labels,
  72. Depth: -1,
  73. }
  74. switch {
  75. case req.NameExact != "":
  76. searchReq.Name = &smsV2.SearchSecretRequest_Exact{Exact: req.NameExact}
  77. case req.NameRegex != "":
  78. searchReq.Name = &smsV2.SearchSecretRequest_Regex{Regex: req.NameRegex}
  79. }
  80. var err error
  81. ctx, err = c.authCtx(ctx)
  82. if err != nil {
  83. return nil, fmt.Errorf("unauthorized: %w", err)
  84. }
  85. resp, err := c.smsClient.V2.SecretService.Search(ctx, searchReq)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return resp.Secrets, nil
  90. }
  91. func (c *APIClient) AccessSecretVersionByPath(ctx context.Context, projectID, path string, version *int32) ([]byte, error) {
  92. var err error
  93. ctx, err = c.authCtx(ctx)
  94. if err != nil {
  95. return nil, fmt.Errorf("unauthorized: %w", err)
  96. }
  97. req := &smsV2.AccessSecretRequest{
  98. ProjectId: projectID,
  99. Path: path,
  100. Version: version,
  101. }
  102. secret, err := c.smsClient.V2.SecretService.Access(ctx, req)
  103. if err != nil {
  104. st, _ := status.FromError(err)
  105. if st.Code() == codes.NotFound {
  106. return nil, esv1.NoSecretErr
  107. }
  108. return nil, fmt.Errorf("failed to get the secret by path '%s': %w", path, err)
  109. }
  110. return secret.GetPayload().GetValue(), nil
  111. }
  112. func (c *APIClient) AccessSecretVersion(ctx context.Context, id, version string) ([]byte, error) {
  113. var err error
  114. ctx, err = c.authCtx(ctx)
  115. if err != nil {
  116. return nil, fmt.Errorf("unauthorized: %w", err)
  117. }
  118. if version == "" {
  119. version = "latest"
  120. }
  121. req := &smsV1.AccessSecretVersionRequest{
  122. SecretId: id,
  123. SecretVersionId: version,
  124. }
  125. secret, err := c.smsClient.SecretService.AccessSecretVersion(ctx, req)
  126. if err != nil {
  127. st, _ := status.FromError(err)
  128. if st.Code() == codes.NotFound {
  129. return nil, esv1.NoSecretErr
  130. }
  131. return nil, fmt.Errorf("failed to get the secret by id '%s v%s': %w", id, version, err)
  132. }
  133. return secret.GetData().GetValue(), nil
  134. }
  135. func (c *APIClient) authCtx(ctx context.Context) (context.Context, error) {
  136. md, ok := metadata.FromOutgoingContext(ctx)
  137. if !ok {
  138. md = metadata.New(map[string]string{})
  139. }
  140. token, err := c.getOrCreateToken(ctx)
  141. if err != nil {
  142. return ctx, fmt.Errorf("fetch IAM access token: %w", err)
  143. }
  144. md.Set("authorization", "Bearer "+token)
  145. return metadata.NewOutgoingContext(ctx, md), nil
  146. }
  147. func (c *APIClient) getOrCreateToken(ctx context.Context) (string, error) {
  148. c.mu.Lock()
  149. defer c.mu.Unlock()
  150. if c.accessToken != "" && c.accessTokenExpiresAt.After(time.Now()) {
  151. return c.accessToken, nil
  152. }
  153. creds, err := c.cr.Resolve(ctx)
  154. if err != nil {
  155. return "", fmt.Errorf("resolve API credentials: %w", err)
  156. }
  157. resp, err := c.iamClient.GetToken(ctx, &iamAuthV1.GetTokenRequest{KeyId: creds.KeyID, Secret: creds.Secret})
  158. if err != nil {
  159. return "", fmt.Errorf("get access token: %w", err)
  160. }
  161. c.accessToken = resp.AccessToken
  162. c.accessTokenExpiresAt = time.Now().Add(time.Second * time.Duration(resp.ExpiresIn))
  163. return c.accessToken, nil
  164. }