2023-03-15 16:00:52 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2018-03-21 22:02:41 +00:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2023-05-02 19:23:43 +00:00
|
|
|
"context"
|
2019-05-03 16:10:41 +00:00
|
|
|
"errors"
|
2018-03-21 22:02:41 +00:00
|
|
|
"fmt"
|
2018-04-04 02:35:45 +00:00
|
|
|
"io"
|
2023-05-02 19:23:43 +00:00
|
|
|
paths "path"
|
|
|
|
"sort"
|
2018-03-21 22:02:41 +00:00
|
|
|
"strings"
|
|
|
|
|
2021-07-16 00:17:31 +00:00
|
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
2018-03-21 22:02:41 +00:00
|
|
|
"github.com/hashicorp/vault/api"
|
2022-03-08 21:17:27 +00:00
|
|
|
"github.com/mitchellh/cli"
|
2018-03-21 22:02:41 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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 {
|
2018-04-04 16:26:06 +00:00
|
|
|
secret, parseErr := api.ParseSecret(resp.Body)
|
|
|
|
switch parseErr {
|
2018-04-04 02:35:45 +00:00
|
|
|
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
|
|
|
|
}
|
2018-03-21 22:02:41 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return api.ParseSecret(resp.Body)
|
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) {
|
Fix response wrapping from K/V version 2 (#4511)
This takes place in two parts, since working on this exposed an issue
with response wrapping when there is a raw body set. The changes are (in
diff order):
* A CurrentWrappingLookupFunc has been added to return the current
value. This is necessary for the lookahead call since we don't want the
lookahead call to be wrapped.
* Support for unwrapping < 0.6.2 tokens via the API/CLI has been
removed, because we now have backends returning 404s with data and can't
rely on the 404 trick. These can still be read manually via
cubbyhole/response.
* KV preflight version request now ensures that its calls is not
wrapped, and restores any given function after.
* When responding with a raw body, instead of always base64-decoding a
string value and erroring on failure, on failure we assume that it
simply wasn't a base64-encoded value and use it as is.
* A test that fails on master and works now that ensures that raw body
responses that are wrapped and then unwrapped return the expected
values.
* A flag for response data that indicates to the wrapping handling that
the data contained therein is already JSON decoded (more later).
* RespondWithStatusCode now defaults to a string so that the value is
HMAC'd during audit. The function always JSON encodes the body, so
before now it was always returning []byte which would skip HMACing. We
don't know what's in the data, so this is a "better safe than sorry"
issue. If different behavior is needed, backends can always manually
populate the data instead of relying on the helper function.
* We now check unwrapped data after unwrapping to see if there were raw
flags. If so, we try to detect whether the value can be unbase64'd. The
reason is that if it can it was probably originally a []byte and
shouldn't be audit HMAC'd; if not, it was probably originally a string
and should be. In either case, we then set the value as the raw body and
hit the flag indicating that it's already been JSON decoded so not to
try again before auditing. Doing it this way ensures the right typing.
* There is now a check to see if the data coming from unwrapping is
already JSON decoded and if so the decoding is skipped before setting
the audit response.
2018-05-10 19:40:03 +00:00
|
|
|
// 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)
|
2019-02-01 22:13:51 +00:00
|
|
|
currentOutputCurlString := client.OutputCurlString()
|
|
|
|
client.SetOutputCurlString(false)
|
|
|
|
defer client.SetOutputCurlString(currentOutputCurlString)
|
2022-04-27 23:35:18 +00:00
|
|
|
currentOutputPolicy := client.OutputPolicy()
|
|
|
|
client.SetOutputPolicy(false)
|
|
|
|
defer client.SetOutputPolicy(currentOutputPolicy)
|
Fix response wrapping from K/V version 2 (#4511)
This takes place in two parts, since working on this exposed an issue
with response wrapping when there is a raw body set. The changes are (in
diff order):
* A CurrentWrappingLookupFunc has been added to return the current
value. This is necessary for the lookahead call since we don't want the
lookahead call to be wrapped.
* Support for unwrapping < 0.6.2 tokens via the API/CLI has been
removed, because we now have backends returning 404s with data and can't
rely on the 404 trick. These can still be read manually via
cubbyhole/response.
* KV preflight version request now ensures that its calls is not
wrapped, and restores any given function after.
* When responding with a raw body, instead of always base64-decoding a
string value and erroring on failure, on failure we assume that it
simply wasn't a base64-encoded value and use it as is.
* A test that fails on master and works now that ensures that raw body
responses that are wrapped and then unwrapped return the expected
values.
* A flag for response data that indicates to the wrapping handling that
the data contained therein is already JSON decoded (more later).
* RespondWithStatusCode now defaults to a string so that the value is
HMAC'd during audit. The function always JSON encodes the body, so
before now it was always returning []byte which would skip HMACing. We
don't know what's in the data, so this is a "better safe than sorry"
issue. If different behavior is needed, backends can always manually
populate the data instead of relying on the helper function.
* We now check unwrapped data after unwrapping to see if there were raw
flags. If so, we try to detect whether the value can be unbase64'd. The
reason is that if it can it was probably originally a []byte and
shouldn't be audit HMAC'd; if not, it was probably originally a string
and should be. In either case, we then set the value as the raw body and
hit the flag indicating that it's already been JSON decoded so not to
try again before auditing. Doing it this way ensures the right typing.
* There is now a check to see if the data coming from unwrapping is
already JSON decoded and if so the decoding is skipped before setting
the audit response.
2018-05-10 19:40:03 +00:00
|
|
|
|
2018-04-23 22:16:10 +00:00
|
|
|
r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path)
|
2018-03-21 22:02:41 +00:00
|
|
|
resp, err := client.RawRequest(r)
|
|
|
|
if resp != nil {
|
|
|
|
defer resp.Body.Close()
|
|
|
|
}
|
|
|
|
if err != nil {
|
2018-04-24 22:49:06 +00:00
|
|
|
// If we get a 404 we are using an older version of vault, default to
|
|
|
|
// version 1
|
2022-04-27 23:35:18 +00:00
|
|
|
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
|
2023-02-21 15:12:45 +00:00
|
|
|
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)
|
2022-04-27 23:35:18 +00:00
|
|
|
}
|
2018-04-24 22:49:06 +00:00
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
return "", 0, err
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
secret, err := api.ParseSecret(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return "", 0, err
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
2019-05-03 16:10:41 +00:00
|
|
|
if secret == nil {
|
|
|
|
return "", 0, errors.New("nil response from pre-flight request")
|
|
|
|
}
|
2018-04-23 22:00:02 +00:00
|
|
|
var mountPath string
|
|
|
|
if mountPathRaw, ok := secret.Data["path"]; ok {
|
|
|
|
mountPath = mountPathRaw.(string)
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
2018-04-23 22:00:02 +00:00
|
|
|
options := secret.Data["options"]
|
|
|
|
if options == nil {
|
|
|
|
return mountPath, 1, nil
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
2018-04-23 22:00:02 +00:00
|
|
|
versionRaw := options.(map[string]interface{})["version"]
|
|
|
|
if versionRaw == nil {
|
|
|
|
return mountPath, 1, nil
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
2018-04-23 22:00:02 +00:00
|
|
|
version := versionRaw.(string)
|
|
|
|
switch version {
|
|
|
|
case "", "1":
|
|
|
|
return mountPath, 1, nil
|
|
|
|
case "2":
|
|
|
|
return mountPath, 2, nil
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
return mountPath, 1, nil
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
func isKVv2(path string, client *api.Client) (string, bool, error) {
|
|
|
|
mountPath, version, err := kvPreflightVersionRequest(client, path)
|
2018-03-21 22:02:41 +00:00
|
|
|
if err != nil {
|
2018-04-23 22:00:02 +00:00
|
|
|
return "", false, err
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2018-04-23 22:00:02 +00:00
|
|
|
return mountPath, version == 2, nil
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 17:10:55 +00:00
|
|
|
func addPrefixToKVPath(path, mountPath, apiPrefix string, skipIfExists bool) string {
|
|
|
|
if path == mountPath || path == strings.TrimSuffix(mountPath, "/") {
|
2023-05-02 19:23:43 +00:00
|
|
|
return paths.Join(mountPath, apiPrefix)
|
2018-06-08 17:45:47 +00:00
|
|
|
}
|
2021-10-22 02:35:13 +00:00
|
|
|
|
2023-05-03 17:10:55 +00:00
|
|
|
pathSuffix := strings.TrimPrefix(path, mountPath)
|
2021-10-22 02:35:13 +00:00
|
|
|
for {
|
|
|
|
// If the entire mountPath is included in the path, we are done
|
2023-05-03 17:10:55 +00:00
|
|
|
if pathSuffix != path {
|
2021-10-22 02:35:13 +00:00
|
|
|
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)
|
2022-01-27 18:06:34 +00:00
|
|
|
if len(partialMountPath) <= 1 || partialMountPath[1] == "" {
|
2021-10-22 02:35:13 +00:00
|
|
|
break
|
|
|
|
}
|
2021-10-22 21:47:16 +00:00
|
|
|
mountPath = strings.TrimSuffix(partialMountPath[1], "/")
|
2023-05-03 17:10:55 +00:00
|
|
|
pathSuffix = strings.TrimPrefix(pathSuffix, mountPath)
|
2021-10-22 02:35:13 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 17:10:55 +00:00
|
|
|
if skipIfExists {
|
|
|
|
if strings.HasPrefix(pathSuffix, apiPrefix) || strings.HasPrefix(pathSuffix, "/"+apiPrefix) {
|
|
|
|
return paths.Join(mountPath, pathSuffix)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return paths.Join(mountPath, apiPrefix, pathSuffix)
|
2018-03-21 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-03-08 21:17:27 +00:00
|
|
|
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 {
|
2018-03-21 22:02:41 +00:00
|
|
|
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))
|
|
|
|
}
|
2023-05-02 19:23:43 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|