/* 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 }