vault/command/kv_helpers.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

270 lines
7.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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
}