| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package ovh
- import (
- "context"
- "errors"
- "fmt"
- ppath "path"
- "regexp"
- "strings"
- "github.com/google/uuid"
- "github.com/ovh/okms-sdk-go/types"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- )
- const retrieveMultipleSecretsError = "failed to retrieve multiple secrets"
- // GetAllSecrets retrieves multiple secrets from the Secret Manager.
- // You can optionally filter secrets by name using a regular expression.
- // When path is set to "" or left empty, the search starts from the Secret Manager root.
- func (cl *ovhClient) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
- // List Secret Manager secrets.
- secrets, err := getSecretsList(ctx, cl.okmsClient, cl.okmsID, ref.Path)
- if err != nil {
- return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
- }
- if len(secrets) == 0 {
- return map[string][]byte{}, nil
- }
- // Compile the regular expression defined in ref.Name.RegExp, if present.
- var regex *regexp.Regexp
- if ref.Name != nil {
- regex, err = regexp.Compile(ref.Name.RegExp)
- if err != nil {
- return map[string][]byte{}, fmt.Errorf(
- "%s: could not parse regex: %w",
- retrieveMultipleSecretsError,
- err,
- )
- }
- if regex == nil {
- return map[string][]byte{}, fmt.Errorf(
- "%s: compiled regex is nil for expression %q",
- retrieveMultipleSecretsError,
- ref.Name.RegExp,
- )
- }
- }
- secretsMap, err := filterSecretsListWithRegexp(ctx, cl, secrets, regex)
- if err != nil {
- return map[string][]byte{}, fmt.Errorf("%s: %w", retrieveMultipleSecretsError, err)
- }
- return secretsMap, nil
- }
- // Retrieve secrets located under the specified path.
- // If the path is omitted, all secrets from the Secret Manager are returned.
- func getSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path *string) ([]string, error) {
- // Ignore invalid path
- if path != nil && (strings.HasPrefix(*path, "/") || strings.Contains(*path, "//")) {
- return []string{}, fmt.Errorf("invalid path %q: cannot start with a / or contain a //", *path)
- }
- formatPath := ""
- if path != nil && *path != "" {
- formatPath = *path
- }
- // Ensure `formatPath` does not end with '/', otherwise, GetSecretsMetadata
- // will not be able to retrieve secrets as it should.
- formatPath = strings.TrimSuffix(formatPath, "/")
- return recursivelyGetSecretsList(ctx, okmsClient, okmsID, formatPath)
- }
- // Recursively traverses the path to retrieve all secrets it contains.
- //
- // The recursion stops when the for loop finishes iterating over the list
- // returned by GetSecretsMetadata, or when an error occurs.
- //
- // A recursive call is triggered whenever a key ends with '/'.
- //
- // Example:
- // Given the secrets ["secret1", "path/secret", "path/to/secret"] stored in the
- // Secret Manager, an initial call to recursivelyGetSecretsList with path="path"
- // will cause GetSecretsMetadata to return ["secret", "to/"]
- // (see Note below for details on this behavior).
- //
- // - "secret" is added to the local secret list.
- // - "to/" triggers a recursive call with path="path/to".
- //
- // In the second call, GetSecretsMetadata returns ["secret"], which is added to
- // the local list. Since no key ends with '/', the recursion stops and the list
- // is returned and merged into the result of the first call.
- //
- // Note: OVH's SDK GetSecretsMetadata does not return full paths.
- // It returns only the next element of the hierarchy, and adds a trailing '/'
- // when the element is a directory (i.e., not the last component).
- //
- // Examples:
- //
- // secret1 = "path/to/secret1"
- // secret2 = "path/secret2"
- // secret3 = "path/secrets/secret3"
- //
- // For the path "path", GetSecretsMetadata returns:
- //
- // ["to/", "secret2", "secrets/"]
- func recursivelyGetSecretsList(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
- // Retrieve the list of KMS secrets for the given path.
- // If no path is provided, retrieve all existing secrets from KMS.
- secrets, err := okmsClient.GetSecretsMetadata(ctx, okmsID, path, true)
- if err != nil {
- return nil, fmt.Errorf("could not list secrets at path %q: %w", path, err)
- }
- if secrets == nil || secrets.Data == nil || secrets.Data.Keys == nil || len(*secrets.Data.Keys) == 0 {
- return nil, nil
- }
- return secretListLoop(ctx, secrets, okmsClient, okmsID, path)
- }
- // Loop over each key under 'path'.
- // If a key represents a directory (ends with '/')
- // and is valid (does not begin with '/' and does not contain successive '/'),
- // a recursive call is made.
- // Otherwise, the key is a secret and is added to the result list.
- func secretListLoop(ctx context.Context, secrets *types.GetMetadataResponse, okmsClient OkmsClient, okmsID uuid.UUID, path string) ([]string, error) {
- secretsList := make([]string, 0, len(*secrets.Data.Keys))
- for _, key := range *secrets.Data.Keys {
- if key == "" || strings.HasPrefix(key, "/") {
- continue
- }
- if before, ok := strings.CutSuffix(key, "/"); ok {
- toAppend, err := recursivelyGetSecretsList(ctx, okmsClient, okmsID, ppath.Join(path, before))
- if err != nil {
- return nil, err
- }
- secretsList = append(secretsList, toAppend...)
- continue
- }
- secretsList = append(secretsList, ppath.Join(path, key))
- }
- return secretsList, nil
- }
- // Filter the list of secrets using a regular expression.
- func filterSecretsListWithRegexp(ctx context.Context, cl *ovhClient, secrets []string, regex *regexp.Regexp) (map[string][]byte, error) {
- secretsDataMap := make(map[string][]byte)
- for _, secret := range secrets {
- // Insert the secret if no regex is provided;
- // otherwise, insert only matching secrets.
- secretData, ok, err := fetchSecretData(ctx, cl, secret, regex)
- if err != nil {
- return map[string][]byte{}, err
- }
- if ok {
- secretsDataMap[secret] = secretData
- }
- }
- return secretsDataMap, nil
- }
- // fetchSecretData retrieves a secret data if it passes the name/regex filter.
- func fetchSecretData(ctx context.Context, cl *ovhClient, secret string, regex *regexp.Regexp) ([]byte, bool, error) {
- // Skip the secret if a name filter is defined but the regex is nil or does not match.
- if regex != nil && !regex.MatchString(secret) {
- return nil, false, nil
- }
- // fetch secret data
- secretData, err := cl.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
- Key: secret,
- })
- if err != nil {
- if errors.Is(err, esv1.NoSecretErr) {
- return nil, false, nil
- }
- return nil, false, err
- }
- return secretData, true, nil
- }
|