| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| paths "path" |
| "sort" |
| "strings" |
| |
| "github.com/hashicorp/go-secure-stdlib/strutil" |
| "github.com/hashicorp/vault/api" |
| "github.com/mitchellh/cli" |
| ) |
| |
| func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) { |
| r := client.NewRequest("GET", "/v1/"+path) |
| for k, v := range params { |
| r.Params.Set(k, v) |
| } |
| resp, err := client.RawRequest(r) |
| if resp != nil { |
| defer resp.Body.Close() |
| } |
| if resp != nil && resp.StatusCode == 404 { |
| secret, parseErr := api.ParseSecret(resp.Body) |
| switch parseErr { |
| case nil: |
| case io.EOF: |
| return nil, nil |
| default: |
| return nil, err |
| } |
| if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) { |
| return secret, nil |
| } |
| return nil, nil |
| } |
| if err != nil { |
| return nil, err |
| } |
| |
| return api.ParseSecret(resp.Body) |
| } |
| |
| func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { |
| // We don't want to use a wrapping call here so save any custom value and |
| // restore after |
| currentWrappingLookupFunc := client.CurrentWrappingLookupFunc() |
| client.SetWrappingLookupFunc(nil) |
| defer client.SetWrappingLookupFunc(currentWrappingLookupFunc) |
| currentOutputCurlString := client.OutputCurlString() |
| client.SetOutputCurlString(false) |
| defer client.SetOutputCurlString(currentOutputCurlString) |
| currentOutputPolicy := client.OutputPolicy() |
| client.SetOutputPolicy(false) |
| defer client.SetOutputPolicy(currentOutputPolicy) |
| |
| r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path) |
| resp, err := client.RawRequest(r) |
| if resp != nil { |
| defer resp.Body.Close() |
| } |
| if err != nil { |
| // If we get a 404 we are using an older version of vault, default to |
| // version 1 |
| if resp != nil { |
| if resp.StatusCode == 404 { |
| return "", 1, nil |
| } |
| |
| // if the original request had the -output-curl-string or -output-policy flag, |
| if (currentOutputCurlString || currentOutputPolicy) && resp.StatusCode == 403 { |
| // we provide a more helpful error for the user, |
| // who may not understand why the flag isn't working. |
| err = fmt.Errorf( |
| `This output flag requires the success of a preflight request |
| to determine the version of a KV secrets engine. Please |
| re-run this command with a token with read access to %s. |
| Note that if the path you are trying to reach is a KV v2 path, your token's policy must |
| allow read access to that path in the format 'mount-path/data/foo', not just 'mount-path/foo'.`, path) |
| } |
| } |
| |
| return "", 0, err |
| } |
| |
| secret, err := api.ParseSecret(resp.Body) |
| if err != nil { |
| return "", 0, err |
| } |
| if secret == nil { |
| return "", 0, errors.New("nil response from pre-flight request") |
| } |
| var mountPath string |
| if mountPathRaw, ok := secret.Data["path"]; ok { |
| mountPath = mountPathRaw.(string) |
| } |
| options := secret.Data["options"] |
| if options == nil { |
| return mountPath, 1, nil |
| } |
| versionRaw := options.(map[string]interface{})["version"] |
| if versionRaw == nil { |
| return mountPath, 1, nil |
| } |
| version := versionRaw.(string) |
| switch version { |
| case "", "1": |
| return mountPath, 1, nil |
| case "2": |
| return mountPath, 2, nil |
| } |
| |
| return mountPath, 1, nil |
| } |
| |
| func isKVv2(path string, client *api.Client) (string, bool, error) { |
| mountPath, version, err := kvPreflightVersionRequest(client, path) |
| if err != nil { |
| return "", false, err |
| } |
| |
| return mountPath, version == 2, nil |
| } |
| |
| func addPrefixToKVPath(path, mountPath, apiPrefix string, skipIfExists bool) string { |
| if path == mountPath || path == strings.TrimSuffix(mountPath, "/") { |
| return paths.Join(mountPath, apiPrefix) |
| } |
| |
| pathSuffix := strings.TrimPrefix(path, mountPath) |
| for { |
| // If the entire mountPath is included in the path, we are done |
| if pathSuffix != path { |
| break |
| } |
| // Trim the parts of the mountPath that are not included in the |
| // path, for example, in cases where the mountPath contains |
| // namespaces which are not included in the path. |
| partialMountPath := strings.SplitN(mountPath, "/", 2) |
| if len(partialMountPath) <= 1 || partialMountPath[1] == "" { |
| break |
| } |
| mountPath = strings.TrimSuffix(partialMountPath[1], "/") |
| pathSuffix = strings.TrimPrefix(pathSuffix, mountPath) |
| } |
| |
| if skipIfExists { |
| if strings.HasPrefix(pathSuffix, apiPrefix) || strings.HasPrefix(pathSuffix, "/"+apiPrefix) { |
| return paths.Join(mountPath, pathSuffix) |
| } |
| } |
| |
| return paths.Join(mountPath, apiPrefix, pathSuffix) |
| } |
| |
| func getHeaderForMap(header string, data map[string]interface{}) string { |
| maxKey := 0 |
| for k := range data { |
| if len(k) > maxKey { |
| maxKey = len(k) |
| } |
| } |
| |
| // 4 for the column spaces and 5 for the len("value") |
| totalLen := maxKey + 4 + 5 |
| |
| return padEqualSigns(header, totalLen) |
| } |
| |
| func kvParseVersionsFlags(versions []string) []string { |
| versionsOut := make([]string, 0, len(versions)) |
| for _, v := range versions { |
| versionsOut = append(versionsOut, strutil.ParseStringSlice(v, ",")...) |
| } |
| |
| return versionsOut |
| } |
| |
| func outputPath(ui cli.Ui, path string, title string) { |
| ui.Info(padEqualSigns(title, len(path))) |
| ui.Info(path) |
| ui.Info("") |
| } |
| |
| // Pad the table header with equal signs on each side |
| func padEqualSigns(header string, totalLen int) string { |
| equalSigns := totalLen - (len(header) + 2) |
| |
| // If we have zero or fewer equal signs bump it back up to two on either |
| // side of the header. |
| if equalSigns <= 0 { |
| equalSigns = 4 |
| } |
| |
| // If the number of equal signs is not divisible by two add a sign. |
| if equalSigns%2 != 0 { |
| equalSigns = equalSigns + 1 |
| } |
| |
| return fmt.Sprintf("%s %s %s", strings.Repeat("=", equalSigns/2), header, strings.Repeat("=", equalSigns/2)) |
| } |
| |
| // walkSecretsTree dfs-traverses the secrets tree rooted at the given path |
| // and calls the `visit` functor for each of the directory and leaf paths. |
| // Note: for kv-v2, a "metadata" path is expected and "metadata" paths will be |
| // returned in the visit functor. |
| func walkSecretsTree(ctx context.Context, client *api.Client, path string, visit func(path string, directory bool) error) error { |
| resp, err := client.Logical().ListWithContext(ctx, path) |
| if err != nil { |
| return fmt.Errorf("could not list %q path: %w", path, err) |
| } |
| |
| if resp == nil || resp.Data == nil { |
| return fmt.Errorf("no value found at %q: %w", path, err) |
| } |
| |
| keysRaw, ok := resp.Data["keys"] |
| if !ok { |
| return fmt.Errorf("unexpected list response at %q", path) |
| } |
| |
| keysRawSlice, ok := keysRaw.([]interface{}) |
| if !ok { |
| return fmt.Errorf("unexpected list response type %T at %q", keysRaw, path) |
| } |
| |
| keys := make([]string, 0, len(keysRawSlice)) |
| |
| for _, keyRaw := range keysRawSlice { |
| key, ok := keyRaw.(string) |
| if !ok { |
| return fmt.Errorf("unexpected key type %T at %q", keyRaw, path) |
| } |
| keys = append(keys, key) |
| } |
| |
| // sort the keys for a deterministic output |
| sort.Strings(keys) |
| |
| for _, key := range keys { |
| // the keys are relative to the current path: combine them |
| child := paths.Join(path, key) |
| |
| if strings.HasSuffix(key, "/") { |
| // visit the directory |
| if err := visit(child, true); err != nil { |
| return err |
| } |
| |
| // this is not a leaf node: we need to go deeper... |
| if err := walkSecretsTree(ctx, client, child, visit); err != nil { |
| return err |
| } |
| } else { |
| // this is a leaf node: add it to the list |
| if err := visit(child, false); err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |