diff --git a/command/command_test.go b/command/command_test.go index d91eb5c4e14f..77598a9a424c 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/hashicorp/go-hclog" + kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/builtin/logical/pki" @@ -41,6 +42,7 @@ var ( "pki": pki.Factory, "ssh": ssh.Factory, "transit": transit.Factory, + "kv": kv.Factory, } ) diff --git a/command/commands.go b/command/commands.go index eb3677c9917d..7b9e4a60e58c 100644 --- a/command/commands.go +++ b/command/commands.go @@ -695,6 +695,13 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, }, nil }, + "kv patch": func() (cli.Command, error) { + return &KVPatchCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, "kv get": func() (cli.Command, error) { return &KVGetCommand{ BaseCommand: &BaseCommand{ diff --git a/command/kv_delete.go b/command/kv_delete.go index 3f3fd0c1b910..9ce39976a9dd 100644 --- a/command/kv_delete.go +++ b/command/kv_delete.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -87,13 +88,25 @@ func (c *KVDeleteCommand) Run(args []string) int { return 1 } + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + path := sanitizePath(args[0]) - var err error - if len(c.flagVersions) > 0 { - err = c.deleteVersions(path, kvParseVersionsFlags(c.flagVersions)) + mountPath, v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if v2 { + err = c.deleteV2(path, mountPath, client) } else { - err = c.deleteLatest(path) + _, err = client.Logical().Delete(path) } + if err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 @@ -103,39 +116,29 @@ func (c *KVDeleteCommand) Run(args []string) int { return 0 } -func (c *KVDeleteCommand) deleteLatest(path string) error { +func (c *KVDeleteCommand) deleteV2(path, mountPath string, client *api.Client) error { var err error - path, err = addPrefixToVKVPath(path, "data") - if err != nil { - return err - } - - client, err := c.Client() - if err != nil { - return err - } - - _, err = kvDeleteRequest(client, path) + switch { + case len(c.flagVersions) > 0: + path = addPrefixToVKVPath(path, mountPath, "delete") + if err != nil { + return err + } - return err -} + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } -func (c *KVDeleteCommand) deleteVersions(path string, versions []string) error { - var err error - path, err = addPrefixToVKVPath(path, "delete") - if err != nil { - return err - } + _, err = client.Logical().Write(path, data) + default: - data := map[string]interface{}{ - "versions": versions, - } + path = addPrefixToVKVPath(path, mountPath, "data") + if err != nil { + return err + } - client, err := c.Client() - if err != nil { - return err + _, err = client.Logical().Delete(path) } - _, err = kvWriteRequest(client, path, data) return err } diff --git a/command/kv_destroy.go b/command/kv_destroy.go index 25c6e98efecf..f622e3f05f00 100644 --- a/command/kv_destroy.go +++ b/command/kv_destroy.go @@ -86,23 +86,33 @@ func (c *KVDestroyCommand) Run(args []string) int { } var err error path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "destroy") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 } - data := map[string]interface{}{ - "versions": kvParseVersionsFlags(c.flagVersions), + mountPath, v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 } - - client, err := c.Client() + if !v2 { + c.UI.Error("Destroy not supported on KV Version 1") + return 1 + } + path = addPrefixToVKVPath(path, mountPath, "destroy") if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/kv_get.go b/command/kv_get.go index 88819951b8b5..80f272ddde25 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -43,7 +43,7 @@ Usage: vault kv get [options] KEY } func (c *KVGetCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) // Common Options f := set.NewFlagSet("Common Options") @@ -91,16 +91,25 @@ func (c *KVGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "data") + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } var versionParam map[string]string - if c.flagVersion > 0 { - versionParam = map[string]string{ - "version": fmt.Sprintf("%d", c.flagVersion), + + if v2 { + path = addPrefixToVKVPath(path, mountPath, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if c.flagVersion > 0 { + versionParam = map[string]string{ + "version": fmt.Sprintf("%d", c.flagVersion), + } } } @@ -115,7 +124,17 @@ func (c *KVGetCommand) Run(args []string) int { } if c.flagField != "" { - return PrintRawField(c.UI, secret, c.flagField) + if v2 { + // This is a v2, pass in the data field + if data, ok := secret.Data["data"]; ok && data != nil { + return PrintRawField(c.UI, data, c.flagField) + } else { + c.UI.Error(fmt.Sprintf("No data found at %s", path)) + return 2 + } + } else { + return PrintRawField(c.UI, secret, c.flagField) + } } // If we have wrap info print the secret normally. @@ -128,8 +147,18 @@ func (c *KVGetCommand) Run(args []string) int { OutputData(c.UI, metadata) c.UI.Info("") } - if data, ok := secret.Data["data"]; ok && data != nil { - c.UI.Info(getHeaderForMap("Data", data.(map[string]interface{}))) + + data := secret.Data + if v2 && data != nil { + data = nil + dataRaw := secret.Data["data"] + if dataRaw != nil { + data = dataRaw.(map[string]interface{}) + } + } + + if data != nil { + c.UI.Info(getHeaderForMap("Data", data)) OutputData(c.UI, data) } diff --git a/command/kv_helpers.go b/command/kv_helpers.go index 488d966adbc3..f65117eded57 100644 --- a/command/kv_helpers.go +++ b/command/kv_helpers.go @@ -1,25 +1,17 @@ package command import ( - "errors" "fmt" "io" - "net/http" "path" "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" ) func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) { r := client.NewRequest("GET", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - for k, v := range params { r.Params.Set(k, v) } @@ -48,121 +40,55 @@ func kvReadRequest(client *api.Client, path string, params map[string]string) (* return api.ParseSecret(resp.Body) } -func kvListRequest(client *api.Client, path string) (*api.Secret, error) { - r := client.NewRequest("LIST", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - - // Set this for broader compatibility, but we use LIST above to be able to - // handle the wrapping lookup function - r.Method = "GET" - r.Params.Set("list", "true") +func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { + r := client.NewRequest("GET", "/v1/sys/internal/ui/mount/"+path) 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 "", 0, err } - return api.ParseSecret(resp.Body) -} - -func kvWriteRequest(client *api.Client, path string, data map[string]interface{}) (*api.Secret, error) { - r := client.NewRequest("PUT", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} + secret, err := api.ParseSecret(resp.Body) + if err != nil { + return "", 0, err } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - if err := r.SetJSONBody(data); err != nil { - return nil, err + var mountPath string + if mountPathRaw, ok := secret.Data["path"]; ok { + mountPath = mountPathRaw.(string) } - - resp, err := client.RawRequest(r) - if resp != nil { - defer resp.Body.Close() + options := secret.Data["options"] + if options == nil { + return mountPath, 1, nil } - 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, err - } - } - if err != nil { - return nil, err + versionRaw := options.(map[string]interface{})["version"] + if versionRaw == nil { + return mountPath, 1, nil } - - if resp.StatusCode == 200 { - return api.ParseSecret(resp.Body) + version := versionRaw.(string) + switch version { + case "", "1": + return mountPath, 1, nil + case "2": + return mountPath, 2, nil } - return nil, nil + return mountPath, 1, nil } -func kvDeleteRequest(client *api.Client, path string) (*api.Secret, error) { - r := client.NewRequest("DELETE", "/v1/"+path) - if r.Headers == nil { - r.Headers = http.Header{} - } - r.Headers.Add(consts.VaultKVCLIClientHeader, "v2") - 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, err - } - } +func isKVv2(path string, client *api.Client) (string, bool, error) { + mountPath, version, err := kvPreflightVersionRequest(client, path) if err != nil { - return nil, err + return "", false, err } - if resp.StatusCode == 200 { - return api.ParseSecret(resp.Body) - } - - return nil, nil + return mountPath, version == 2, nil } -func addPrefixToVKVPath(p, apiPrefix string) (string, error) { - parts := strings.SplitN(p, "/", 2) - if len(parts) != 2 { - return "", errors.New("invalid path") - } - - return path.Join(parts[0], apiPrefix, parts[1]), nil +func addPrefixToVKVPath(p, mountPath, apiPrefix string) string { + p = strings.TrimPrefix(p, mountPath) + return path.Join(mountPath, apiPrefix, p) } func getHeaderForMap(header string, data map[string]interface{}) string { diff --git a/command/kv_list.go b/command/kv_list.go index 50613d506c74..faedd6f3f429 100644 --- a/command/kv_list.go +++ b/command/kv_list.go @@ -74,13 +74,21 @@ func (c *KVListCommand) Run(args []string) int { } path := ensureTrailingSlash(sanitizePath(args[0])) - path, err = addPrefixToVKVPath(path, "metadata") + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvListRequest(client, path) + if v2 { + path = addPrefixToVKVPath(path, mountPath, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + } + + secret, err := client.Logical().List(path) if err != nil { c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) return 2 diff --git a/command/kv_metadata_delete.go b/command/kv_metadata_delete.go index 8a764bb7bfda..446533bdf307 100644 --- a/command/kv_metadata_delete.go +++ b/command/kv_metadata_delete.go @@ -71,13 +71,18 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int { } path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "metadata") + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 + } - if _, err := kvDeleteRequest(client, path); err != nil { + path = addPrefixToVKVPath(path, mountPath, "metadata") + if _, err := client.Logical().Delete(path); err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 } diff --git a/command/kv_metadata_get.go b/command/kv_metadata_get.go index 11855a275b3e..75c4cd22f7f8 100644 --- a/command/kv_metadata_get.go +++ b/command/kv_metadata_get.go @@ -75,13 +75,18 @@ func (c *KVMetadataGetCommand) Run(args []string) int { } path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "metadata") + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 + } - secret, err := kvReadRequest(client, path, nil) + path = addPrefixToVKVPath(path, mountPath, "metadata") + secret, err := client.Logical().Read(path) if err != nil { c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) return 2 diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go index 95a6d6f5c30b..32f8d9248152 100644 --- a/command/kv_metadata_put.go +++ b/command/kv_metadata_put.go @@ -99,26 +99,30 @@ func (c *KVMetadataPutCommand) Run(args []string) int { return 1 } - var err error + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "metadata") + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } + if !v2 { + c.UI.Error("Metadata not supported on KV Version 1") + return 1 + } + path = addPrefixToVKVPath(path, mountPath, "metadata") data := map[string]interface{}{ "max_versions": c.flagMaxVersions, "cas_required": c.flagCASRequired, } - client, err := c.Client() - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - - secret, err := kvWriteRequest(client, path, data) + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/kv_patch.go b/command/kv_patch.go new file mode 100644 index 000000000000..bbfc164e7e54 --- /dev/null +++ b/command/kv_patch.go @@ -0,0 +1,195 @@ +package command + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVPatchCommand)(nil) +var _ cli.CommandAutocomplete = (*KVPatchCommand)(nil) + +type KVPatchCommand struct { + *BaseCommand + + testStdin io.Reader // for tests +} + +func (c *KVPatchCommand) Synopsis() string { + return "Sets or updates data in the KV store without overwriting." +} + +func (c *KVPatchCommand) Help() string { + helpText := ` +Usage: vault kv put [options] KEY [DATA] + + *NOTE*: This is only supported for KV v2 engine mounts. + + Writes the data to the given path in the key-value store. The data can be of + any type. + + $ vault kv patch secret/foo bar=baz + + The data can also be consumed from a file on disk by prefixing with the "@" + symbol. For example: + + $ vault kv patch secret/foo @data.json + + Or it can be read from stdin using the "-" symbol: + + $ echo "abcd1234" | vault kv patch secret/foo bar=- + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVPatchCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + return set +} + +func (c *KVPatchCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVPatchCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVPatchCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected >1, got %d)", len(args))) + return 1 + case len(args) == 1: + c.UI.Error("Must supply data") + return 1 + } + + var err error + path := sanitizePath(args[0]) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + newData, err := parseArgsData(stdin, args[1:]) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) + return 1 + } + + mountPath, v2, err := isKVv2(path, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if !v2 { + c.UI.Error(fmt.Sprintf("K/V engine mount must be version 2 for patch support")) + return 2 + } + + path = addPrefixToVKVPath(path, mountPath, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // First, do a read + secret, err := kvReadRequest(client, path, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err)) + return 2 + } + + // Make sure a value already exists + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + // Verify metadata found + rawMeta, ok := secret.Data["metadata"] + if !ok || rawMeta == nil { + c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path)) + return 2 + } + meta, ok := rawMeta.(map[string]interface{}) + if !ok { + c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path)) + return 2 + } + if meta == nil { + c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path)) + return 2 + } + + // Verify old data found + rawData, ok := secret.Data["data"] + if !ok || rawData == nil { + c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path)) + return 2 + } + data, ok := rawData.(map[string]interface{}) + if !ok { + c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path)) + return 2 + } + if data == nil { + c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path)) + return 2 + } + + // Copy new data over + for k, v := range newData { + data[k] = v + } + + secret, err = client.Logical().Write(path, map[string]interface{}{ + "data": data, + "options": map[string]interface{}{ + "cas": meta["version"], + }, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) + return 2 + } + if secret == nil { + // Don't output anything unless using the "table" format + if Format(c.UI) == "table" { + c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path)) + } + return 0 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/kv_put.go b/command/kv_put.go index ca981f885822..a4524f9c8944 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -55,7 +55,7 @@ Usage: vault kv put [options] KEY [DATA] } func (c *KVPutCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) // Common Options f := set.NewFlagSet("Common Options") @@ -97,14 +97,19 @@ func (c *KVPutCommand) Run(args []string) int { stdin = c.testStdin } - if len(args) < 1 { - c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected >1, got %d)", len(args))) + return 1 + case len(args) == 1: + c.UI.Error("Must supply data") return 1 } var err error path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "data") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 @@ -116,22 +121,25 @@ func (c *KVPutCommand) Run(args []string) int { return 1 } - data = map[string]interface{}{ - "data": data, - "options": map[string]interface{}{}, - } - - if c.flagCAS > -1 { - data["options"].(map[string]interface{})["cas"] = c.flagCAS - } - - client, err := c.Client() + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } - secret, err := kvWriteRequest(client, path, data) + if v2 { + path = addPrefixToVKVPath(path, mountPath, "data") + data = map[string]interface{}{ + "data": data, + "options": map[string]interface{}{}, + } + + if c.flagCAS > -1 { + data["options"].(map[string]interface{})["cas"] = c.flagCAS + } + } + + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 @@ -144,5 +152,9 @@ func (c *KVPutCommand) Run(args []string) int { return 0 } + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + return OutputSecret(c.UI, secret) } diff --git a/command/kv_test.go b/command/kv_test.go new file mode 100644 index 000000000000..697a105bb6cf --- /dev/null +++ b/command/kv_test.go @@ -0,0 +1,529 @@ +package command + +import ( + "io" + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testKVPutCommand(tb testing.TB) (*cli.MockUi, *KVPutCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVPutCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVPutCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "empty_kvs", + []string{"secret/write/foo"}, + "Must supply data", + 1, + }, + { + "kvs_no_value", + []string{"secret/write/foo", "foo"}, + "Failed to parse K=V data", + 1, + }, + { + "single_value", + []string{"secret/write/foo", "foo=bar"}, + "Success!", + 0, + }, + { + "multi_value", + []string{"secret/write/foo", "foo=bar", "zip=zap"}, + "Success!", + 0, + }, + { + "v2_single_value", + []string{"kv/write/foo", "foo=bar"}, + "created_time", + 0, + }, + { + "v2_multi_value", + []string{"kv/write/foo", "foo=bar", "zip=zap"}, + "created_time", + 0, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + + t.Run("v2_cas", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-cas", "0", "kv/write/cas", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "created_time") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, cmd = testKVPutCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-cas", "1", "kv/write/cas", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "created_time") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, cmd = testKVPutCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-cas", "1", "kv/write/cas", "bar=baz", + }) + if code != 2 { + t.Fatalf("expected 2 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "check-and-set parameter did not match the current version") { + t.Errorf("expected %q to contain %q", combined, "check-and-set parameter did not match the current version") + } + + }) + + t.Run("v1_data", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/write/data", "bar=baz", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "Success!") { + t.Errorf("expected %q to contain %q", combined, "created_time") + } + + ui, rcmd := testReadCommand(t) + rcmd.client = client + code = rcmd.Run([]string{ + "secret/write/data", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + combined = ui.OutputWriter.String() + ui.ErrorWriter.String() + if strings.Contains(combined, "data") { + t.Errorf("expected %q not to contain %q", combined, "data") + } + }) + + t.Run("stdin_full", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(`{"foo":"bar"}`)) + stdinW.Close() + }() + + _, cmd := testKVPutCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "secret/write/stdin_full", "-", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/stdin_full") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("stdin_value", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte("bar")) + stdinW.Close() + }() + + _, cmd := testKVPutCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "secret/write/stdin_value", "foo=-", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/stdin_value") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + _, cmd := testKVPutCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/write/integration", "foo=bar", "zip=zap", + }) + if code != 0 { + t.Fatalf("expected 0 to be %d", code) + } + + secret, err := client.Logical().Read("secret/write/integration") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + if exp, act := "bar", secret.Data["foo"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + if exp, act := "zap", secret.Data["zip"].(string); exp != act { + t.Errorf("expected %q to be %q", act, exp) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVPutCommand(t) + assertNoTabs(t, cmd) + }) +} + +func testKVGetCommand(tb testing.TB) (*cli.MockUi, *KVGetCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVGetCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_found", + []string{"secret/nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/read/foo"}, + "foo", + 0, + }, + { + "v1_field", + []string{"-field", "foo", "secret/read/foo"}, + "bar", + 0, + }, + { + "v2_field", + []string{"-field", "foo", "kv/read/foo"}, + "bar", + 0, + }, + + { + "v2_not_found", + []string{"kv/nope/not/once/never"}, + "", + 2, + }, + + { + "v2_read", + []string{"kv/read/foo"}, + "foo", + 0, + }, + { + "v2_read", + []string{"kv/read/foo"}, + "version", + 0, + }, + { + "v2_read_version", + []string{"--version", "1", "kv/read/foo"}, + "foo", + 0, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("kv/data/read/foo", map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVGetCommand(t) + cmd.client = client + + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVGetCommand(t) + assertNoTabs(t, cmd) + }) +} + +func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVMetadataGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKVMetadataGetCommand(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "v1", + []string{"secret/foo"}, + "Metadata not supported on KV Version 1", + 1, + }, + { + "metadata_exists", + []string{"kv/foo"}, + "current_version", + 0, + }, + { + "versions_exist", + []string{"kv/foo"}, + "deletion_time", + 0, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("kv/data/foo", map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testKVMetadataGetCommand(t) + cmd.client = client + + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKVMetadataGetCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/kv_undelete.go b/command/kv_undelete.go index 048d6f6885d5..58eee93c0b9a 100644 --- a/command/kv_undelete.go +++ b/command/kv_undelete.go @@ -84,25 +84,30 @@ func (c *KVUndeleteCommand) Run(args []string) int { c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to undelete.") return 1 } - var err error - path := sanitizePath(args[0]) - path, err = addPrefixToVKVPath(path, "undelete") + + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) return 2 } - data := map[string]interface{}{ - "versions": kvParseVersionsFlags(c.flagVersions), - } - - client, err := c.Client() + path := sanitizePath(args[0]) + mountPath, v2, err := isKVv2(path, client) if err != nil { c.UI.Error(err.Error()) return 2 } + if !v2 { + c.UI.Error("Undelete not supported on KV Version 1") + return 1 + } + + path = addPrefixToVKVPath(path, mountPath, "undelete") + data := map[string]interface{}{ + "versions": kvParseVersionsFlags(c.flagVersions), + } - secret, err := kvWriteRequest(client, path, data) + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) return 2 diff --git a/command/util.go b/command/util.go index b418ce7fc8ec..d9718ffc8746 100644 --- a/command/util.go +++ b/command/util.go @@ -20,7 +20,7 @@ func DefaultTokenHelper() (token.TokenHelper, error) { // RawField extracts the raw field from the given data and returns it as a // string for printing purposes. -func RawField(secret *api.Secret, field string) (interface{}, bool) { +func RawField(secret *api.Secret, field string) interface{} { var val interface{} switch { case secret.Auth != nil: @@ -72,13 +72,20 @@ func RawField(secret *api.Secret, field string) (interface{}, bool) { } } - return val, val != nil + return val } // PrintRawField prints raw field from the secret. -func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { - val, ok := RawField(secret, field) - if !ok { +func PrintRawField(ui cli.Ui, data interface{}, field string) int { + var val interface{} + switch data.(type) { + case *api.Secret: + val = RawField(data.(*api.Secret), field) + case map[string]interface{}: + val = data.(map[string]interface{})[field] + } + + if val == nil { ui.Error(fmt.Sprintf("Field %q not present in secret", field)) return 1 } diff --git a/helper/consts/consts.go b/helper/consts/consts.go index 6c9558aa673e..eee59d9c999d 100644 --- a/helper/consts/consts.go +++ b/helper/consts/consts.go @@ -4,6 +4,4 @@ const ( // ExpirationRestoreWorkerCount specifies the number of workers to use while // restoring leases into the expiration manager ExpirationRestoreWorkerCount = 64 - - VaultKVCLIClientHeader = "X-Vault-Kv-Client" ) diff --git a/http/sys_internal_test.go b/http/sys_internal_test.go index dc6c5ac44fce..d3c066f70c84 100644 --- a/http/sys_internal_test.go +++ b/http/sys_internal_test.go @@ -65,12 +65,14 @@ func TestSysInternal_UIMounts(t *testing.T) { "secret/": map[string]interface{}{ "type": "kv", "description": "key/value secret storage", + "options": map[string]interface{}{"version": "1"}, }, }, "auth": map[string]interface{}{ "token/": map[string]interface{}{ "type": "token", "description": "token based credentials", + "options": interface{}(nil), }, }, }, diff --git a/vault/capabilities.go b/vault/capabilities.go index db64347aafb4..a2b9e3f26eb3 100644 --- a/vault/capabilities.go +++ b/vault/capabilities.go @@ -38,11 +38,15 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string, policies = append(policies, policy) } - _, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID) + entity, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID) if err != nil { return nil, err } + if entity != nil && entity.Disabled { + return nil, logical.ErrPermissionDenied + } + for _, item := range derivedPolicies { policy, err := c.policyStore.GetPolicy(ctx, item, PolicyTypeToken) if err != nil { diff --git a/vault/core_test.go b/vault/core_test.go index b63ba822a43d..bd3591d3d10c 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -2314,7 +2314,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) { Path: "foo/test", ClientToken: root, Headers: map[string][]string{ - "X-Vault-Kv-Client": []string{"foo"}, "Should-Passthrough": []string{"foo"}, "Should-Passthrough-Case-Insensitive": []string{"baz"}, "Should-Not-Passthrough": []string{"bar"}, @@ -2328,16 +2327,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) { // Check the headers headers := noop.Requests[0].Headers - // Test whitelisted values - if val, ok := headers["X-Vault-Kv-Client"]; ok { - expected := []string{"foo"} - if !reflect.DeepEqual(val, expected) { - t.Fatalf("expected: %v, got: %v", expected, val) - } - } else { - t.Fatalf("expected 'X-Vault-Kv-Client' to be present in the headers map") - } - // Test passthrough values if val, ok := headers["Should-Passthrough"]; ok { expected := []string{"foo"} diff --git a/vault/cors.go b/vault/cors.go index 4ec8f8ff9873..6b0920a73b6b 100644 --- a/vault/cors.go +++ b/vault/cors.go @@ -7,7 +7,6 @@ import ( "sync/atomic" "github.com/hashicorp/errwrap" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) @@ -27,7 +26,6 @@ var StdAllowedHeaders = []string{ "X-Vault-Wrap-Format", "X-Vault-Wrap-TTL", "X-Vault-Policy-Override", - consts.VaultKVCLIClientHeader, } // CORSConfig stores the state of the CORS configuration. diff --git a/vault/logical_system.go b/vault/logical_system.go index 661e047ab4f8..9287933c6a8c 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -22,7 +22,9 @@ import ( uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/compressutil" "github.com/hashicorp/vault/helper/consts" + "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/parseutil" + "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/wrapping" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -92,6 +94,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { "wrapping/pubkey", "replication/status", "internal/ui/mounts", + "internal/ui/mount/*", }, }, @@ -1075,6 +1078,20 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]), HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]), }, + &framework.Path{ + Pattern: "internal/ui/mount/(?P.+)", + Fields: map[string]*framework.FieldSchema{ + "path": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The path of the mount.", + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathInternalUIMountRead, + }, + HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]), + HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]), + }, &framework.Path{ Pattern: "internal/ui/resultant-acl", Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -1520,6 +1537,41 @@ func (b *SystemBackend) handleRekeyDeleteRecovery(ctx context.Context, req *logi return b.handleRekeyDelete(ctx, req, data, true) } +func mountInfo(entry *MountEntry) map[string]interface{} { + info := map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "accessor": entry.Accessor, + "local": entry.Local, + "seal_wrap": entry.SealWrap, + "options": entry.Options, + } + entryConfig := map[string]interface{}{ + "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), + "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), + "force_no_cache": entry.Config.ForceNoCache, + "plugin_name": entry.Config.PluginName, + } + if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { + entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) + } + if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { + entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string) + } + // Even though empty value is valid for ListingVisibility, we can ignore + // this case during mount since there's nothing to unset/hide. + if len(entry.Config.ListingVisibility) > 0 { + entryConfig["listing_visibility"] = entry.Config.ListingVisibility + } + if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + entryConfig["passthrough_request_headers"] = rawVal.([]string) + } + + info["config"] = entryConfig + + return info +} + // handleMountTable handles the "mounts" endpoint to provide the mount table func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { b.Core.mountsLock.RLock() @@ -1531,36 +1583,7 @@ func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Reque for _, entry := range b.Core.mounts.Entries { // Populate mount info - info := map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, - "accessor": entry.Accessor, - "local": entry.Local, - "seal_wrap": entry.SealWrap, - "options": entry.Options, - } - entryConfig := map[string]interface{}{ - "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), - "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), - "force_no_cache": entry.Config.ForceNoCache, - "plugin_name": entry.Config.PluginName, - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { - entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) - } - if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok { - entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string) - } - // Even though empty value is valid for ListingVisibility, we can ignore - // this case during mount since there's nothing to unset/hide. - if len(entry.Config.ListingVisibility) > 0 { - entryConfig["listing_visibility"] = entry.Config.ListingVisibility - } - if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok { - entryConfig["passthrough_request_headers"] = rawVal.([]string) - } - - info["config"] = entryConfig + info := mountInfo(entry) resp.Data[entry.Path] = info } @@ -3402,10 +3425,49 @@ func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Reques return resp, nil } -func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - b.Core.mountsLock.RLock() - defer b.Core.mountsLock.RUnlock() +func hasMountAccess(acl *ACL, path string) bool { + // If an ealier policy is giving us access to the mount path then we can do + // a fast return. + capabilities := acl.Capabilities(path) + if !strutil.StrListContains(capabilities, DenyCapability) { + return true + } + var aclCapabilitiesGiven bool + walkFn := func(s string, v interface{}) bool { + if v == nil { + return false + } + + perms := v.(*ACLPermissions) + + switch { + case perms.CapabilitiesBitmap&DenyCapabilityInt > 0: + return false + + case perms.CapabilitiesBitmap&CreateCapabilityInt > 0, + perms.CapabilitiesBitmap&DeleteCapabilityInt > 0, + perms.CapabilitiesBitmap&ListCapabilityInt > 0, + perms.CapabilitiesBitmap&ReadCapabilityInt > 0, + perms.CapabilitiesBitmap&SudoCapabilityInt > 0, + perms.CapabilitiesBitmap&UpdateCapabilityInt > 0: + + aclCapabilitiesGiven = true + return true + } + + return false + } + + acl.exactRules.WalkPrefix(path, walkFn) + if !aclCapabilitiesGiven { + acl.globRules.WalkPrefix(path, walkFn) + } + + return aclCapabilitiesGiven +} + +func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { resp := &logical.Response{ Data: make(map[string]interface{}), } @@ -3415,25 +3477,104 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic resp.Data["secret"] = secretMounts resp.Data["auth"] = authMounts + var acl *ACL + var isAuthed bool + var err error + if req.ClientToken != "" { + isAuthed = true + + var entity *identity.Entity + // Load the ACL policies so we can walk the prefix for this mount + acl, _, entity, err = b.Core.fetchACLTokenEntryAndEntity(req) + if err != nil { + return nil, err + } + if entity != nil && entity.Disabled { + return nil, logical.ErrPermissionDenied + } + + } + + hasAccess := func(me *MountEntry) bool { + if me.Config.ListingVisibility == ListingVisibilityUnauth { + return true + } + + if isAuthed { + return hasMountAccess(acl, me.Path) + } + + return false + } + + b.Core.mountsLock.RLock() for _, entry := range b.Core.mounts.Entries { - if entry.Config.ListingVisibility == ListingVisibilityUnauth { - info := map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, + if hasAccess(entry) { + if isAuthed { + // If this is an authed request return all the mount info + secretMounts[entry.Path] = mountInfo(entry) + } else { + secretMounts[entry.Path] = map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "options": entry.Options, + } } - secretMounts[entry.Path] = info } } + b.Core.mountsLock.RUnlock() + b.Core.authLock.RLock() for _, entry := range b.Core.auth.Entries { - if entry.Config.ListingVisibility == ListingVisibilityUnauth { - info := map[string]interface{}{ - "type": entry.Type, - "description": entry.Description, + if hasAccess(entry) { + if isAuthed { + // If this is an authed request return all the mount info + authMounts[entry.Path] = mountInfo(entry) + } else { + authMounts[entry.Path] = map[string]interface{}{ + "type": entry.Type, + "description": entry.Description, + "options": entry.Options, + } } - authMounts[entry.Path] = info } } + b.Core.authLock.RUnlock() + + return resp, nil +} + +func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + path := d.Get("path").(string) + if path == "" { + return logical.ErrorResponse("path not set"), logical.ErrInvalidRequest + } + path = sanitizeMountPath(path) + + me := b.Core.router.MatchingMountEntry(path) + if me == nil { + // Return a permission denied error here so this path cannot be used to + // brute force a list of mounts. + return nil, logical.ErrPermissionDenied + } + + resp := &logical.Response{ + Data: mountInfo(me), + } + resp.Data["path"] = me.Path + + // Load the ACL policies so we can walk the prefix for this mount + acl, _, entity, err := b.Core.fetchACLTokenEntryAndEntity(req) + if err != nil { + return nil, err + } + if entity != nil && entity.Disabled { + return nil, logical.ErrPermissionDenied + } + + if !hasMountAccess(acl, me.Path) { + return nil, logical.ErrPermissionDenied + } return resp, nil } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 129941a54b51..07dfb834c3e9 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -2218,7 +2218,7 @@ func TestSystemBackend_ToolsRandom(t *testing.T) { } func TestSystemBackend_InternalUIMounts(t *testing.T) { - b := testSystemBackend(t) + _, b, rootToken := testCoreSystemBackend(t) // Ensure no entries are in the endpoint as a starting point req := logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts") @@ -2235,6 +2235,95 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) } + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts") + req.ClientToken = rootToken + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("err: %v", err) + } + + exp = map[string]interface{}{ + "secret": map[string]interface{}{ + "secret/": map[string]interface{}{ + "type": "kv", + "description": "key/value secret storage", + "accessor": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string{ + "version": "1", + }, + }, + "sys/": map[string]interface{}{ + "type": "system", + "description": "system endpoints used for control, policy and debugging", + "accessor": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + }, + "cubbyhole/": map[string]interface{}{ + "description": "per-token private secret storage", + "type": "cubbyhole", + "accessor": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": true, + "seal_wrap": false, + "options": map[string]string(nil), + }, + "identity/": map[string]interface{}{ + "description": "identity store", + "type": "identity", + "accessor": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["accessor"], + "config": map[string]interface{}{ + "default_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), + "max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), + "plugin_name": "", + "force_no_cache": false, + }, + "local": false, + "seal_wrap": false, + "options": map[string]string(nil), + }, + }, + "auth": map[string]interface{}{ + "token/": map[string]interface{}{ + "options": map[string]string(nil), + "config": map[string]interface{}{ + "default_lease_ttl": int64(0), + "max_lease_ttl": int64(0), + "force_no_cache": false, + "plugin_name": "", + }, + "type": "token", + "description": "token based credentials", + "accessor": resp.Data["auth"].(map[string]interface{})["token/"].(map[string]interface{})["accessor"], + "local": false, + "seal_wrap": false, + }, + }, + } + if !reflect.DeepEqual(resp.Data, exp) { + t.Fatalf("got: %#v \n\n expect: %#v", resp.Data, exp) + } + // Mount-tune an auth mount req = logical.TestRequest(t, logical.UpdateOperation, "auth/token/tune") req.Data["listing_visibility"] = "unauth" @@ -2256,12 +2345,14 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { "secret/": map[string]interface{}{ "type": "kv", "description": "key/value secret storage", + "options": map[string]string{"version": "1"}, }, }, "auth": map[string]interface{}{ "token/": map[string]interface{}{ "type": "token", "description": "token based credentials", + "options": map[string]string(nil), }, }, } @@ -2269,3 +2360,75 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) } } + +func TestSystemBackend_InternalUIMount(t *testing.T) { + core, b, rootToken := testCoreSystemBackend(t) + + req := logical.TestRequest(t, logical.UpdateOperation, "policy/secret") + req.ClientToken = rootToken + req.Data = map[string]interface{}{ + "rules": `path "secret/foo/*" { + capabilities = ["create", "read", "update", "delete", "list"] +}`, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + + req = logical.TestRequest(t, logical.UpdateOperation, "mounts/kv") + req.ClientToken = rootToken + req.Data = map[string]interface{}{ + "type": "kv", + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv/bar") + req.ClientToken = rootToken + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "kv" { + t.Fatalf("Bad Response: %#v", resp) + } + + testMakeToken(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"}) + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != logical.ErrPermissionDenied { + t.Fatal("expected permission denied error") + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/secret") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "kv" { + t.Fatalf("Bad Response: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/sys") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("Bad %#v %#v", err, resp) + } + if resp.Data["type"] != "system" { + t.Fatalf("Bad Response: %#v", resp) + } + + req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/non-existent") + req.ClientToken = "tokenid" + resp, err = b.HandleRequest(context.Background(), req) + if err != logical.ErrPermissionDenied { + t.Fatal("expected permission denied error") + } +} diff --git a/vault/router.go b/vault/router.go index 32b036824975..d79cc16338bc 100644 --- a/vault/router.go +++ b/vault/router.go @@ -10,17 +10,10 @@ import ( "github.com/armon/go-metrics" "github.com/armon/go-radix" - "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/logical" ) -var ( - whitelistedHeaders = []string{ - consts.VaultKVCLIClientHeader, - } -) - // Router is used to do prefix based routing of a request to a logical backend type Router struct { l sync.RWMutex @@ -639,20 +632,6 @@ func pathsToRadix(paths []string) *radix.Tree { func filteredPassthroughHeaders(origHeaders map[string][]string, passthroughHeaders []string) map[string][]string { retHeaders := make(map[string][]string) - // Handle whitelisted values - for _, header := range whitelistedHeaders { - if val, ok := origHeaders[header]; ok { - retHeaders[header] = val - } else { - // Try to check if a lowercased version of the header exists in the - // originating request. The header key that gets used is the one from the - // whitelist. - if val, ok := origHeaders[strings.ToLower(header)]; ok { - retHeaders[header] = val - } - } - } - // Short-circuit if there's nothing to filter if len(passthroughHeaders) == 0 { return retHeaders diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go index 3aa4542df414..aa81126de32a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go @@ -60,6 +60,10 @@ type versionedKVBackend struct { // upgrading is an atomic value denoting if the backend is in the process of // upgrading its data. upgrading *uint32 + + // globalConfig is a cached value for fast lookup + globalConfig *Configuration + globalConfigLock *sync.RWMutex } // Factory will return a logical backend of type versionedKVBackend or @@ -85,7 +89,8 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, // Factory returns a new backend as logical.Backend. func VersionedKVFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { b := &versionedKVBackend{ - upgrading: new(uint32), + upgrading: new(uint32), + globalConfigLock: new(sync.RWMutex), } if conf.BackendUUID == "" { return nil, errors.New("could not initialize versioned K/V Store, no UUID was provided") @@ -207,6 +212,10 @@ func (b *versionedKVBackend) Invalidate(ctx context.Context, key string) { b.l.Lock() b.keyEncryptedWrapper = nil b.l.Unlock() + case path.Join(b.storagePrefix, configPath): + b.globalConfigLock.Lock() + b.globalConfig = nil + b.globalConfigLock.Unlock() } } @@ -301,19 +310,40 @@ func (b *versionedKVBackend) getKeyEncryptor(ctx context.Context, s logical.Stor // config takes a storage object and returns a configuration object func (b *versionedKVBackend) config(ctx context.Context, s logical.Storage) (*Configuration, error) { + b.globalConfigLock.RLock() + if b.globalConfig != nil { + defer b.globalConfigLock.RUnlock() + return &Configuration{ + CasRequired: b.globalConfig.CasRequired, + MaxVersions: b.globalConfig.MaxVersions, + }, nil + } + + b.globalConfigLock.RUnlock() + b.globalConfigLock.Lock() + defer b.globalConfigLock.Unlock() + + // Verify this hasn't already changed + if b.globalConfig != nil { + return &Configuration{ + CasRequired: b.globalConfig.CasRequired, + MaxVersions: b.globalConfig.MaxVersions, + }, nil + } + raw, err := s.Get(ctx, path.Join(b.storagePrefix, configPath)) if err != nil { return nil, err } conf := &Configuration{} - if raw == nil { - return conf, nil + if raw != nil { + if err := proto.Unmarshal(raw.Value, conf); err != nil { + return nil, err + } } - if err := proto.Unmarshal(raw.Value, conf); err != nil { - return nil, err - } + b.globalConfig = conf return conf, nil } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go index b6a9f03513b8..036e351e7c78 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go @@ -36,14 +36,10 @@ func LeasedPassthroughBackendFactory(ctx context.Context, conf *logical.BackendC // LeaseSwitchedPassthroughBackend returns a PassthroughBackend // with leases switched on or off func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendConfig, leases bool) (logical.Backend, error) { - passthroughBackend := &PassthroughBackend{ + b := &PassthroughBackend{ generateLeases: leases, } - var b Passthrough = &PassthroughDowngrader{ - next: passthroughBackend, - } - backend := &framework.Backend{ BackendType: logical.TypeLogical, Help: strings.TrimSpace(passthroughHelp), @@ -89,9 +85,9 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC return nil, fmt.Errorf("Configuation passed into backend is nil") } backend.Setup(ctx, conf) - passthroughBackend.Backend = backend + b.Backend = backend - return passthroughBackend, nil + return b, nil } // PassthroughBackend is used storing secrets directly into the physical diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go deleted file mode 100644 index 2f7096f87bec..000000000000 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go +++ /dev/null @@ -1,158 +0,0 @@ -package kv - -import ( - "context" - "net/http" - "strings" - - "github.com/hashicorp/vault/helper/consts" - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/framework" -) - -// PassthroughDowngrader wraps a normal passthrough backend and downgrades the -// request object from the newer Versioned API to the older Passthrough API. -// This allows us to use the new "vault kv" subcommand with a non-versioned -// instance of the kv store without doing a preflight API version check. The -// CLI will always use the new API definition and this object will make it -// compatible with the passthrough backend. The "X-Vault-Kv-Client" header is -// used to know the request originated from the CLI and uses the newer API. -type PassthroughDowngrader struct { - next Passthrough -} - -func (b *PassthroughDowngrader) handleExistenceCheck() framework.ExistenceFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { - if !b.shouldDowngrade(req) { - return b.next.handleExistenceCheck()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return false, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - return b.next.handleExistenceCheck()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleRead() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleRead()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - if _, ok := data.Raw["version"]; ok { - return logical.ErrorResponse("retrieving a version is not supported when versioning is disabled"), logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - resp, err := b.next.handleRead()(ctx, reqDown, data) - if resp != nil && resp.Data != nil { - resp.Data = map[string]interface{}{ - "data": resp.Data, - "metadata": nil, - } - } - - return resp, err - } -} - -func (b *PassthroughDowngrader) handleWrite() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleWrite()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - // Validate the data map is what we expect - switch req.Data["data"].(type) { - case map[string]interface{}: - default: - return logical.ErrorResponse("could not downgrade request, unexpected data format"), logical.ErrInvalidRequest - } - - // Move the data object up a level and ignore the options object. - reqDown.Data = req.Data["data"].(map[string]interface{}) - - return b.next.handleWrite()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleDelete() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleDelete()(ctx, req, data) - } - - respErr := b.invalidPath(req) - if respErr != nil { - return respErr, logical.ErrInvalidRequest - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "data/") - - return b.next.handleDelete()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) handleList() framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - if !b.shouldDowngrade(req) { - return b.next.handleList()(ctx, req, data) - } - - reqDown := &logical.Request{} - *reqDown = *req - reqDown.Path = strings.TrimPrefix(req.Path, "metadata/") - - return b.next.handleList()(ctx, reqDown, data) - } -} - -func (b *PassthroughDowngrader) shouldDowngrade(req *logical.Request) bool { - return http.Header(req.Headers).Get(consts.VaultKVCLIClientHeader) != "" -} - -// invalidPaths returns an error if we are trying to access an versioned only -// path on a non-versioned kv store. -func (b *PassthroughDowngrader) invalidPath(req *logical.Request) *logical.Response { - switch { - case req.Path == "config": - fallthrough - case strings.HasPrefix(req.Path, "metadata/"): - fallthrough - case strings.HasPrefix(req.Path, "archive/"): - fallthrough - case strings.HasPrefix(req.Path, "unarchive/"): - fallthrough - case strings.HasPrefix(req.Path, "destroy/"): - return logical.ErrorResponse("path is not supported when versioning is disabled") - } - - return nil -} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go index 80946a4119ec..6c9aa974151c 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go @@ -42,9 +42,6 @@ func (b *versionedKVBackend) pathConfigRead() framework.OperationFunc { if err != nil { return nil, err } - if config == nil { - return nil, nil - } return &logical.Response{ Data: map[string]interface{}{ @@ -91,6 +88,11 @@ func (b *versionedKVBackend) pathConfigWrite() framework.OperationFunc { return nil, err } + b.globalConfigLock.Lock() + defer b.globalConfigLock.Unlock() + + b.globalConfig = config + return nil, nil } } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go index 4a9706c45543..6e2a88c978b0 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go @@ -105,6 +105,7 @@ func (b *versionedKVBackend) pathMetadataRead() framework.OperationFunc { "created_time": ptypesTimestampToString(meta.CreatedTime), "updated_time": ptypesTimestampToString(meta.UpdatedTime), "max_versions": meta.MaxVersions, + "cas_required": meta.CasRequired, }, }, nil } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto index 275d7816b128..aaa3a821e14b 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto @@ -3,6 +3,7 @@ package kv; import "google/protobuf/timestamp.proto"; +// If values are added to this, be sure to update the config() function message Configuration { uint32 max_versions = 1; bool cas_required = 2; diff --git a/vendor/vendor.json b/vendor/vendor.json index e5df1e5b6e12..8924b4aaeb1b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1339,10 +1339,10 @@ "revisionTime": "2018-04-23T14:10:30Z" }, { - "checksumSHA1": "m3cQgQrCSuWHiPA339FaZU6LuHU=", + "checksumSHA1": "mawUYCTqiIcegkYTCG9fZChK4kQ=", "path": "github.com/hashicorp/vault-plugin-secrets-kv", - "revision": "bc6216eebacf73fab61fd5cc7535b5eda7a74c98", - "revisionTime": "2018-04-09T21:22:48Z" + "revision": "d5a07c3d99f7fa02dd23d6dbff98d24e0eedf06b", + "revisionTime": "2018-04-23T19:31:27Z" }, { "checksumSHA1": "vTfeYxi0Z1y176bjQaYh1/FpQ9s=",