client_get_all_secrets.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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 ovh
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. ppath "path"
  19. "regexp"
  20. "strings"
  21. "github.com/google/uuid"
  22. "github.com/ovh/okms-sdk-go/types"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. )
  25. const retrieveMultipleSecretsError = "failed to retrieve multiple secrets"
  26. // GetAllSecrets retrieves multiple secrets from the Secret Manager.
  27. // You can optionally filter secrets by name using a regular expression.
  28. // When path is set to "" or left empty, the search starts from the Secret Manager root.
  29. func (cl *ovhClient) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  30. // List Secret Manager secrets.
  31. secrets, err := getSecretsList(ctx, cl.okmsClient, cl.okmsID, ref.Path)
  32. if err != nil {
  33. return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
  34. }
  35. if len(secrets) == 0 {
  36. return map[string][]byte{}, nil
  37. }
  38. // Compile the regular expression defined in ref.Name.RegExp, if present.
  39. var regex *regexp.Regexp
  40. if ref.Name != nil {
  41. regex, err = regexp.Compile(ref.Name.RegExp)
  42. if err != nil {
  43. return map[string][]byte{}, fmt.Errorf(
  44. "%s: could not parse regex: %w",
  45. retrieveMultipleSecretsError,
  46. err,
  47. )
  48. }
  49. if regex == nil {
  50. return map[string][]byte{}, fmt.Errorf(
  51. "%s: compiled regex is nil for expression %q",
  52. retrieveMultipleSecretsError,
  53. ref.Name.RegExp,
  54. )
  55. }
  56. }
  57. secretsMap, err := filterSecretsListWithRegexp(ctx, cl, secrets, regex)
  58. if err != nil {
  59. return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
  60. }
  61. return secretsMap, nil
  62. }
  63. // Retrieve secrets located under the specified path.
  64. // If the path is omitted, all secrets from the Secret Manager are returned.
  65. func getSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path *string) ([]string, error) {
  66. // Ignore invalid path
  67. if path != nil && (strings.HasPrefix(*path, "/") || strings.Contains(*path, "//")) {
  68. return []string{}, fmt.Errorf("invalid path %q: cannot start with a / or contain a //", *path)
  69. }
  70. formatPath := ""
  71. if path != nil && *path != "" {
  72. formatPath = *path
  73. }
  74. // Ensure `formatPath` does not end with '/', otherwise, GetSecretsMetadata
  75. // will not be able to retrieve secrets as it should.
  76. formatPath = strings.TrimSuffix(formatPath, "/")
  77. return recursivelyGetSecretsList(ctx, okmsClient, okmsID, formatPath)
  78. }
  79. // Recursively traverses the path to retrieve all secrets it contains.
  80. //
  81. // The recursion stops when the for loop finishes iterating over the list
  82. // returned by GetSecretsMetadata, or when an error occurs.
  83. //
  84. // A recursive call is triggered whenever a key ends with '/'.
  85. //
  86. // Example:
  87. // Given the secrets ["secret1", "path/secret", "path/to/secret"] stored in the
  88. // Secret Manager, an initial call to recursivelyGetSecretsList with path="path"
  89. // will cause GetSecretsMetadata to return ["secret", "to/"]
  90. // (see Note below for details on this behavior).
  91. //
  92. // - "secret" is added to the local secret list.
  93. // - "to/" triggers a recursive call with path="path/to".
  94. //
  95. // In the second call, GetSecretsMetadata returns ["secret"], which is added to
  96. // the local list. Since no key ends with '/', the recursion stops and the list
  97. // is returned and merged into the result of the first call.
  98. //
  99. // Note: OVH's SDK GetSecretsMetadata does not return full paths.
  100. // It returns only the next element of the hierarchy, and adds a trailing '/'
  101. // when the element is a directory (i.e., not the last component).
  102. //
  103. // Examples:
  104. //
  105. // secret1 = "path/to/secret1"
  106. // secret2 = "path/secret2"
  107. // secret3 = "path/secrets/secret3"
  108. //
  109. // For the path "path", GetSecretsMetadata returns:
  110. //
  111. // ["to/", "secret2", "secrets/"]
  112. func recursivelyGetSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
  113. // Retrieve the list of KMS secrets for the given path.
  114. // If no path is provided, retrieve all existing secrets from KMS.
  115. secrets, err := okmsClient.GetSecretsMetadata(ctx, okmsID, path, true)
  116. if err != nil {
  117. return nil, fmt.Errorf("could not list secrets at path %q: %w", path, err)
  118. }
  119. if secrets == nil || secrets.Data == nil || secrets.Data.Keys == nil || len(*secrets.Data.Keys) == 0 {
  120. return nil, nil
  121. }
  122. return secretListLoop(ctx, secrets, okmsClient, okmsID, path)
  123. }
  124. // Loop over each key under 'path'.
  125. // If a key represents a directory (ends with '/')
  126. // and is valid (does not begin with '/' and does not contain successive '/'),
  127. // a recursive call is made.
  128. // Otherwise, the key is a secret and is added to the result list.
  129. func secretListLoop(ctx context.Context, secrets *types.GetMetadataResponse, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
  130. secretsList := make([]string, 0, len(*secrets.Data.Keys))
  131. for _, key := range *secrets.Data.Keys {
  132. if key == "" || strings.HasPrefix(key, "/") {
  133. continue
  134. }
  135. if before, ok := strings.CutSuffix(key, "/"); ok {
  136. toAppend, err := recursivelyGetSecretsList(ctx, okmsClient, okmsID, ppath.Join(path, before))
  137. if err != nil {
  138. return nil, err
  139. }
  140. secretsList = append(secretsList, toAppend...)
  141. continue
  142. }
  143. secretsList = append(secretsList, ppath.Join(path, key))
  144. }
  145. return secretsList, nil
  146. }
  147. // Filter the list of secrets using a regular expression.
  148. func filterSecretsListWithRegexp(ctx context.Context, cl *ovhClient, secrets []string, regex *regexp.Regexp) (map[string][]byte, error) {
  149. secretsDataMap := make(map[string][]byte)
  150. for _, secret := range secrets {
  151. // Insert the secret if no regex is provided;
  152. // otherwise, insert only matching secrets.
  153. secretData, ok, err := fetchSecretData(ctx, cl, secret, regex)
  154. if err != nil {
  155. return map[string][]byte{}, err
  156. }
  157. if ok {
  158. secretsDataMap[secret] = secretData
  159. }
  160. }
  161. return secretsDataMap, nil
  162. }
  163. // fetchSecretData retrieves a secret data if it passes the name/regex filter.
  164. func fetchSecretData(ctx context.Context, cl *ovhClient, secret string, regex *regexp.Regexp) ([]byte, bool, error) {
  165. // Skip the secret if a name filter is defined but the regex is nil or does not match.
  166. if regex != nil && !regex.MatchString(secret) {
  167. return nil, false, nil
  168. }
  169. // fetch secret data
  170. secretData, err := cl.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
  171. Key: secret,
  172. })
  173. if err != nil {
  174. if errors.Is(err, esv1.NoSecretErr) {
  175. return nil, false, nil
  176. }
  177. return nil, false, err
  178. }
  179. return secretData, true, nil
  180. }