diff --git a/command/commands.go b/command/commands.go index a0b3d7ef7..3e3daabfe 100644 --- a/command/commands.go +++ b/command/commands.go @@ -675,6 +675,90 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, }, nil }, + "kv": func() (cli.Command, error) { + return &KVCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv put": func() (cli.Command, error) { + return &KVPutCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv get": func() (cli.Command, error) { + return &KVGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv delete": func() (cli.Command, error) { + return &KVDeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv list": func() (cli.Command, error) { + return &KVListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv destroy": func() (cli.Command, error) { + return &KVDestroyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv undelete": func() (cli.Command, error) { + return &KVUndeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv enable-versioning": func() (cli.Command, error) { + return &KVEnableVersioningCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv metadata": func() (cli.Command, error) { + return &KVMetadataCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv metadata put": func() (cli.Command, error) { + return &KVMetadataPutCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv metadata get": func() (cli.Command, error) { + return &KVMetadataGetCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "kv metadata delete": func() (cli.Command, error) { + return &KVMetadataDeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, } // Deprecated commands diff --git a/command/format.go b/command/format.go index c547535eb..a258cd903 100644 --- a/command/format.go +++ b/command/format.go @@ -134,6 +134,9 @@ func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) return t.OutputList(ui, secret, data) case []string: return t.OutputList(ui, nil, data) + case map[string]interface{}: + t.OutputMap(ui, data.(map[string]interface{})) + return nil default: return errors.New("Cannot use the table formatter for this type") } @@ -261,6 +264,34 @@ func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error { return nil } +func (t TableFormatter) OutputMap(ui cli.Ui, data map[string]interface{}) { + out := make([]string, 0, len(data)+1) + if len(data) > 0 { + keys := make([]string, 0, len(data)) + for k := range data { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, data[k])) + } + } + + // If we got this far and still don't have any data, there's nothing to print, + // sorry. + if len(out) == 0 { + return + } + + // Prepend the header + out = append([]string{"Key" + hopeDelim + "Value"}, out...) + + ui.Output(tableOutput(out, &columnize.Config{ + Delim: hopeDelim, + })) +} + // OutputSealStatus will print *api.SealStatusResponse in the CLI according to the format provided func OutputSealStatus(ui cli.Ui, client *api.Client, status *api.SealStatusResponse) int { switch Format(ui) { diff --git a/command/kv.go b/command/kv.go new file mode 100644 index 000000000..3fa91c8c5 --- /dev/null +++ b/command/kv.go @@ -0,0 +1,52 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*KVCommand)(nil) + +type KVCommand struct { + *BaseCommand +} + +func (c *KVCommand) Synopsis() string { + return "Interact with Vault's Key-Value storage" +} + +func (c *KVCommand) Help() string { + helpText := ` +Usage: vault kv [options] [args] + + This command has subcommands for interacting with Vault's key-value + store. Here are some simple examples, and more detailed examples are + available in the subcommands or the documentation. + + Create or update the key named "foo" in the "secret" mount with the value + "bar=baz": + + $ vault kv put secret/foo bar=baz + + Read this value back: + + $ vault kv get secret/foo + + Get metadata for the key: + + $ vault kv metadata get secret/foo + + Get a specific version of the key: + + $ vault kv get -version=1 secret/foo + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *KVCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/kv_delete.go b/command/kv_delete.go new file mode 100644 index 000000000..a6bef6416 --- /dev/null +++ b/command/kv_delete.go @@ -0,0 +1,141 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVDeleteCommand)(nil) +var _ cli.CommandAutocomplete = (*KVDeleteCommand)(nil) + +type KVDeleteCommand struct { + *BaseCommand + + flagVersions []string +} + +func (c *KVDeleteCommand) Synopsis() string { + return "Deletes versions in the KV store" +} + +func (c *KVDeleteCommand) Help() string { + helpText := ` +Usage: vault kv delete [options] PATH + + Deletes the data for the provided version and path in the key-value store. The + versioned data will not be fully removed, but marked as deleted and will no + longer be returned in normal get requests. + + To delete the latest version of the key "foo": + + $ vault kv delete secret/foo + + To delete version 3 of key foo: + + $ vault kv delete -versions=3 secret/foo + + To delete all versions and metadata, see the "vault kv metadata" subcommand. + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *KVDeleteCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + // Common Options + f := set.NewFlagSet("Common Options") + + f.StringSliceVar(&StringSliceVar{ + Name: "versions", + Target: &c.flagVersions, + Default: nil, + Usage: `Specifies the version numbers to delete.`, + }) + + return set +} + +func (c *KVDeleteCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *KVDeleteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVDeleteCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + path := sanitizePath(args[0]) + var err error + if len(c.flagVersions) > 0 { + err = c.deleteVersions(path, c.flagVersions) + } else { + err = c.deleteLatest(path) + } + if err != nil { + c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) + return 2 + } + + c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path)) + return 0 +} + +func (c *KVDeleteCommand) deleteLatest(path string) 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) + + return err +} + +func (c *KVDeleteCommand) deleteVersions(path string, versions []string) error { + var err error + path, err = addPrefixToVKVPath(path, "delete") + if err != nil { + return err + } + + data := map[string]interface{}{ + "versions": c.flagVersions, + } + + client, err := c.Client() + if err != nil { + return err + } + + _, err = kvWriteRequest(client, path, data) + return err +} diff --git a/command/kv_destroy.go b/command/kv_destroy.go new file mode 100644 index 000000000..a84adbffc --- /dev/null +++ b/command/kv_destroy.go @@ -0,0 +1,119 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVDestroyCommand)(nil) +var _ cli.CommandAutocomplete = (*KVDestroyCommand)(nil) + +type KVDestroyCommand struct { + *BaseCommand + + flagVersions []string +} + +func (c *KVDestroyCommand) Synopsis() string { + return "Permanently removes one or more versions in the KV store" +} + +func (c *KVDestroyCommand) Help() string { + helpText := ` +Usage: vault kv destroy [options] KEY + + Permanently removes the specified versions' data from the key-value store. If + no key exists at the path, no action is taken. + + To destroy version 3 of key foo: + + $ vault kv destroy -versions=3 secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVDestroyCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.StringSliceVar(&StringSliceVar{ + Name: "versions", + Target: &c.flagVersions, + Default: nil, + Usage: `Specifies the version numbers to destroy.`, + }) + + return set +} + +func (c *KVDestroyCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVDestroyCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVDestroyCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + if len(c.flagVersions) == 0 { + c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to destroy.") + return 1 + } + var err error + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "destroy") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + data := map[string]interface{}{ + "versions": c.flagVersions, + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := kvWriteRequest(client, path, data) + 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 + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/kv_enable_versioning.go b/command/kv_enable_versioning.go new file mode 100644 index 000000000..7ebd0218c --- /dev/null +++ b/command/kv_enable_versioning.go @@ -0,0 +1,89 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVEnableVersioningCommand)(nil) +var _ cli.CommandAutocomplete = (*KVEnableVersioningCommand)(nil) + +type KVEnableVersioningCommand struct { + *BaseCommand +} + +func (c *KVEnableVersioningCommand) Synopsis() string { + return "Turns on versioning for a KV store" +} + +func (c *KVEnableVersioningCommand) Help() string { + helpText := ` +Usage: vault kv enable-versions [options] KEY + + This command turns on versioning for the backend at the provided path. + + $ vault kv enable-versions secret + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVEnableVersioningCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + return set +} + +func (c *KVEnableVersioningCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVEnableVersioningCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVEnableVersioningCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Append a trailing slash to indicate it's a path in output + mountPath := ensureTrailingSlash(sanitizePath(args[0])) + + if err := client.Sys().TuneMount(mountPath, api.MountConfigInput{ + Options: map[string]string{ + "versioned": "true", + }, + }); err != nil { + c.UI.Error(fmt.Sprintf("Error tuning secrets engine %s: %s", mountPath, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Tuned the secrets engine at: %s", mountPath)) + return 0 +} diff --git a/command/kv_get.go b/command/kv_get.go new file mode 100644 index 000000000..88819951b --- /dev/null +++ b/command/kv_get.go @@ -0,0 +1,137 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVGetCommand)(nil) +var _ cli.CommandAutocomplete = (*KVGetCommand)(nil) + +type KVGetCommand struct { + *BaseCommand + + flagVersion int +} + +func (c *KVGetCommand) Synopsis() string { + return "Retrieves data from the KV store" +} + +func (c *KVGetCommand) Help() string { + helpText := ` +Usage: vault kv get [options] KEY + + Retrieves the value from Vault's key-value store at the given key name. If no + key exists with that name, an error is returned. If a key exists with that + name but has no data, nothing is returned. + + $ vault kv get secret/foo + + To view the given key name at a specific version in time, specify the "-version" + flag: + + $ vault kv get -version=1 secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVGetCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.IntVar(&IntVar{ + Name: "version", + Target: &c.flagVersion, + Default: 0, + Usage: `If passed, the value at the version number will be returned.`, + }) + + return set +} + +func (c *KVGetCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVGetCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVGetCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "data") + 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), + } + } + + secret, err := kvReadRequest(client, path, versionParam) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) + return 2 + } + if secret == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + // If we have wrap info print the secret normally. + if secret.WrapInfo != nil || c.flagFormat != "table" { + return OutputSecret(c.UI, secret) + } + + if metadata, ok := secret.Data["metadata"]; ok && metadata != nil { + c.UI.Info(getHeaderForMap("Metadata", metadata.(map[string]interface{}))) + 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{}))) + OutputData(c.UI, data) + } + + return 0 +} diff --git a/command/kv_helpers.go b/command/kv_helpers.go new file mode 100644 index 000000000..28f000304 --- /dev/null +++ b/command/kv_helpers.go @@ -0,0 +1,143 @@ +package command + +import ( + "errors" + "fmt" + "net/http" + "path" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/consts" +) + +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, "v1") + + 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 { + return nil, nil + } + if err != nil { + return nil, err + } + + 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, "v1") + + // 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") + resp, err := client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + return nil, nil + } + if err != nil { + return nil, 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{} + } + r.Headers.Add(consts.VaultKVCLIClientHeader, "v1") + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + if resp.StatusCode == 200 { + return api.ParseSecret(resp.Body) + } + + return nil, 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, "v1") + resp, err := client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + if resp.StatusCode == 200 { + return api.ParseSecret(resp.Body) + } + + return nil, 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 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 + + 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)) +} diff --git a/command/kv_list.go b/command/kv_list.go new file mode 100644 index 000000000..50613d506 --- /dev/null +++ b/command/kv_list.go @@ -0,0 +1,104 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVListCommand)(nil) +var _ cli.CommandAutocomplete = (*KVListCommand)(nil) + +type KVListCommand struct { + *BaseCommand +} + +func (c *KVListCommand) Synopsis() string { + return "List data or secrets" +} + +func (c *KVListCommand) Help() string { + helpText := ` + +Usage: vault kv list [options] PATH + + Lists data from Vault's key-value store at the given path. + + List values under the "my-app" folder of the key-value store: + + $ vault kv list secret/my-app/ + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *KVListCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP | FlagSetOutputFormat) +} + +func (c *KVListCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFolders() +} + +func (c *KVListCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVListCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := ensureTrailingSlash(sanitizePath(args[0])) + path, err = addPrefixToVKVPath(path, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := kvListRequest(client, path) + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) + return 2 + } + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + // If the secret is wrapped, return the wrapped response. + if secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 { + return OutputSecret(c.UI, secret) + } + + if _, ok := extractListData(secret); !ok { + c.UI.Error(fmt.Sprintf("No entries found at %s", path)) + return 2 + } + + return OutputList(c.UI, secret) +} diff --git a/command/kv_metadata.go b/command/kv_metadata.go new file mode 100644 index 000000000..ee4dca938 --- /dev/null +++ b/command/kv_metadata.go @@ -0,0 +1,48 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*KVMetadataCommand)(nil) + +type KVMetadataCommand struct { + *BaseCommand +} + +func (c *KVMetadataCommand) Synopsis() string { + return "Interact with Vault's Key-Value storage" +} + +func (c *KVMetadataCommand) Help() string { + helpText := ` +Usage: vault kv metadata [options] [args] + + This command has subcommands for interacting with the metadata endpoint in + Vault's key-value store. Here are some simple examples, and more detailed + examples are available in the subcommands or the documentation. + + Create or update a metadata entry for a key: + + $ vault kv metadata put -max-versions=5 secret/foo + + Get the metadata for a key, this provides information about each existing + version: + + $ vault kv metadata get secret/foo + + Delete a key and all existing versions: + + $ vault kv metadata delete secret/foo + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *KVMetadataCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/kv_metadata_delete.go b/command/kv_metadata_delete.go new file mode 100644 index 000000000..8a764bb7b --- /dev/null +++ b/command/kv_metadata_delete.go @@ -0,0 +1,87 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVMetadataDeleteCommand)(nil) +var _ cli.CommandAutocomplete = (*KVMetadataDeleteCommand)(nil) + +type KVMetadataDeleteCommand struct { + *BaseCommand +} + +func (c *KVMetadataDeleteCommand) Synopsis() string { + return "Deletes all versions and metadata for a key in the KV store" +} + +func (c *KVMetadataDeleteCommand) Help() string { + helpText := ` +Usage: vault kv metadata delete [options] PATH + + Deletes all versions and metadata for the provided key. + + $ vault kv metadata delete secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *KVMetadataDeleteCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *KVMetadataDeleteCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *KVMetadataDeleteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVMetadataDeleteCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if _, err := kvDeleteRequest(client, path); err != nil { + c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) + return 2 + } + + c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path)) + return 0 +} diff --git a/command/kv_metadata_get.go b/command/kv_metadata_get.go new file mode 100644 index 000000000..11855a275 --- /dev/null +++ b/command/kv_metadata_get.go @@ -0,0 +1,129 @@ +package command + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVMetadataGetCommand)(nil) +var _ cli.CommandAutocomplete = (*KVMetadataGetCommand)(nil) + +type KVMetadataGetCommand struct { + *BaseCommand +} + +func (c *KVMetadataGetCommand) Synopsis() string { + return "Retrieves key metadata from the KV store" +} + +func (c *KVMetadataGetCommand) Help() string { + helpText := ` +Usage: vault kv metadata get [options] KEY + + Retrieves the metadata from Vault's key-value store at the given key name. If no + key exists with that name, an error is returned. + + $ vault kv metadata get secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVMetadataGetCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + return set +} + +func (c *KVMetadataGetCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVMetadataGetCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVMetadataGetCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := kvReadRequest(client, path, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) + return 2 + } + if secret == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + // If we have wrap info print the secret normally. + if secret.WrapInfo != nil || c.flagFormat != "table" { + return OutputSecret(c.UI, secret) + } + + versions := secret.Data["versions"].(map[string]interface{}) + + delete(secret.Data, "versions") + + c.UI.Info(getHeaderForMap("Metadata", secret.Data)) + OutputSecret(c.UI, secret) + + versionKeys := []int{} + for k := range versions { + i, err := strconv.Atoi(k) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing version %s", k)) + return 2 + } + + versionKeys = append(versionKeys, i) + } + + sort.Ints(versionKeys) + + for _, v := range versionKeys { + c.UI.Info("\n" + getHeaderForMap(fmt.Sprintf("Version %d", v), versions[strconv.Itoa(v)].(map[string]interface{}))) + OutputData(c.UI, versions[strconv.Itoa(v)]) + } + + return 0 +} diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go new file mode 100644 index 000000000..95a6d6f5c --- /dev/null +++ b/command/kv_metadata_put.go @@ -0,0 +1,135 @@ +package command + +import ( + "fmt" + "io" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVMetadataPutCommand)(nil) +var _ cli.CommandAutocomplete = (*KVMetadataPutCommand)(nil) + +type KVMetadataPutCommand struct { + *BaseCommand + + flagMaxVersions int + flagCASRequired bool + testStdin io.Reader // for tests +} + +func (c *KVMetadataPutCommand) Synopsis() string { + return "Sets or updates key settings in the KV store" +} + +func (c *KVMetadataPutCommand) Help() string { + helpText := ` +Usage: vault metadata kv put [options] KEY [DATA] + + This command can be used to create a blank key in the key-value store or to + update key configuration for a specified key. + + Create a key in the key-value store with no data: + + $ vault kv metadata put secret/foo + + Set a max versions setting on the key: + + $ vault kv metadata put -max-versions=5 secret/foo + + Require Check-and-Set for this key: + + $ vault kv metadata put -require-cas secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVMetadataPutCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.IntVar(&IntVar{ + Name: "max-versions", + Target: &c.flagMaxVersions, + Default: 0, + Usage: `The number of versions to keep. If not set, the backend’s configured max version is used.`, + }) + + f.BoolVar(&BoolVar{ + Name: "cas-required", + Target: &c.flagCASRequired, + Default: false, + Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backend’s configuration will be used.`, + }) + + return set +} + +func (c *KVMetadataPutCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVMetadataPutCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVMetadataPutCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + var err error + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "metadata") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + 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) + 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 + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/kv_put.go b/command/kv_put.go new file mode 100644 index 000000000..5d925aed2 --- /dev/null +++ b/command/kv_put.go @@ -0,0 +1,143 @@ +package command + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVPutCommand)(nil) +var _ cli.CommandAutocomplete = (*KVPutCommand)(nil) + +type KVPutCommand struct { + *BaseCommand + + flagCAS int + testStdin io.Reader // for tests +} + +func (c *KVPutCommand) Synopsis() string { + return "Sets or updates data in the KV store" +} + +func (c *KVPutCommand) Help() string { + helpText := ` +Usage: vault kv put [options] KEY [DATA] + + Writes the data to the given path in the key-value store. The data can be of + any type. + + $ vault kv put secret/foo bar=baz + + The data can also be consumed from a file on disk by prefixing with the "@" + symbol. For example: + + $ vault kv put secret/foo @data.json + + Or it can be read from stdin using the "-" symbol: + + $ echo "abcd1234" | vault kv put secret/foo bar=- + + To perform a Check-And-Set operation, specify the -cas flag with the + appropriate version number corresponding to the key you want to perform + the CAS operation on: + + $ vault kv put -cas=1 secret/foo bar=baz + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVPutCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.IntVar(&IntVar{ + Name: "cas", + Target: &c.flagCAS, + Default: -1, + Usage: `Specifies to use a Check-And-Set operation. If not set the write + will be allowed. If set to 0 a write will only be allowed if the key + doesn’t exist. If the index is non-zero the write will only be allowed + if the key’s current version matches the version specified in the cas + parameter.`, + }) + + return set +} + +func (c *KVPutCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVPutCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVPutCommand) 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 + } + + var err error + path := sanitizePath(args[0]) + path, err = addPrefixToVKVPath(path, "data") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + data, err := parseArgsData(stdin, args[1:]) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) + 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() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := kvWriteRequest(client, path, data) + 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 + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/kv_undelete.go b/command/kv_undelete.go new file mode 100644 index 000000000..30349f44e --- /dev/null +++ b/command/kv_undelete.go @@ -0,0 +1,119 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*KVUndeleteCommand)(nil) +var _ cli.CommandAutocomplete = (*KVUndeleteCommand)(nil) + +type KVUndeleteCommand struct { + *BaseCommand + + flagVersions []string +} + +func (c *KVUndeleteCommand) Synopsis() string { + return "Undeletes versions in the KV store" +} + +func (c *KVUndeleteCommand) Help() string { + helpText := ` +Usage: vault kv undelete [options] KEY + + Undeletes the data for the provided version and path in the key-value store. + This restores the data, allowing it to be returned on get requests. + + To undelete version 3 of key "foo": + + $ vault kv undelete -versions=3 secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVUndeleteCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.StringSliceVar(&StringSliceVar{ + Name: "versions", + Target: &c.flagVersions, + Default: nil, + Usage: `Specifies the version numbers to undelete.`, + }) + + return set +} + +func (c *KVUndeleteCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVUndeleteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVUndeleteCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + if len(c.flagVersions) == 0 { + 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") + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + data := map[string]interface{}{ + "versions": c.flagVersions, + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := kvWriteRequest(client, path, data) + 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 + } + + return OutputSecret(c.UI, secret) +} diff --git a/helper/keysutil/encrypted_key_storage.go b/helper/keysutil/encrypted_key_storage.go index 91e7d5eaf..9490e9dee 100644 --- a/helper/keysutil/encrypted_key_storage.go +++ b/helper/keysutil/encrypted_key_storage.go @@ -134,6 +134,13 @@ type encryptedKeyStorage struct { prefix string } +func ensureTailingSlash(path string) string { + if !strings.HasSuffix(path, "/") { + return path + "/" + } + return path +} + // List implements the logical.Storage List method, and decrypts all the items // in a path prefix. This can only operate on full folder structures so the // prefix should end in a "/". @@ -143,7 +150,7 @@ func (s *encryptedKeyStorage) List(ctx context.Context, prefix string) ([]string return nil, err } - keys, err := s.s.List(ctx, encPrefix+"/") + keys, err := s.s.List(ctx, ensureTailingSlash(encPrefix)) if err != nil { return keys, err } @@ -244,6 +251,10 @@ func (s *encryptedKeyStorage) Delete(ctx context.Context, path string) error { // by "/") with the object's key policy. The context for each encryption is the // plaintext path prefix for the key. func (s *encryptedKeyStorage) encryptPath(path string) (string, error) { + if path == "" || path == "/" { + return s.prefix, nil + } + path = paths.Clean(path) // Trim the prefix if it starts with a "/" @@ -252,7 +263,7 @@ func (s *encryptedKeyStorage) encryptPath(path string) (string, error) { parts := strings.Split(path, "/") encPath := s.prefix - context := s.prefix + context := strings.TrimSuffix(s.prefix, "/") for _, p := range parts { encoded := base64.StdEncoding.EncodeToString([]byte(p)) ciphertext, err := s.policy.Encrypt(0, []byte(context), nil, encoded) diff --git a/helper/keysutil/encrypted_key_storage_test.go b/helper/keysutil/encrypted_key_storage_test.go index 3e14442e0..7e035d266 100644 --- a/helper/keysutil/encrypted_key_storage_test.go +++ b/helper/keysutil/encrypted_key_storage_test.go @@ -160,7 +160,91 @@ func TestEncrytedKeysStorage_BadPolicy(t *testing.T) { } } -func TestEncrytedKeysStorage_CRUD(t *testing.T) { +func TestEncryptedKeysStorage_List(t *testing.T) { + s := &logical.InmemStorage{} + policy := &Policy{ + Name: "metadata", + Type: KeyType_AES256_GCM96, + Derived: true, + KDF: Kdf_hkdf_sha256, + ConvergentEncryption: true, + ConvergentVersion: 2, + VersionTemplate: EncryptedKeyPolicyVersionTpl, + versionPrefixCache: &sync.Map{}, + } + + ctx := context.Background() + + err := policy.Rotate(ctx, s) + if err != nil { + t.Fatal(err) + } + + es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{ + Policy: policy, + Prefix: "prefix", + }) + if err != nil { + t.Fatal(err) + } + + err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ + Key: "test", + Value: []byte("test"), + }) + if err != nil { + t.Fatal(err) + } + + err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ + Key: "test/foo", + Value: []byte("test"), + }) + if err != nil { + t.Fatal(err) + } + + err = es.Wrap(s).Put(ctx, &logical.StorageEntry{ + Key: "test/foo1/test", + Value: []byte("test"), + }) + if err != nil { + t.Fatal(err) + } + + keys, err := es.Wrap(s).List(ctx, "test/") + if err != nil { + t.Fatal(err) + } + + // Test prefixed with "/" + keys, err = es.Wrap(s).List(ctx, "/test/") + if err != nil { + t.Fatal(err) + } + + if len(keys) != 2 || keys[0] != "foo1/" || keys[1] != "foo" { + t.Fatalf("bad keys: %#v", keys) + } + + keys, err = es.Wrap(s).List(ctx, "/") + if err != nil { + t.Fatal(err) + } + if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" { + t.Fatalf("bad keys: %#v", keys) + } + + keys, err = es.Wrap(s).List(ctx, "") + if err != nil { + t.Fatal(err) + } + if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" { + t.Fatalf("bad keys: %#v", keys) + } +} + +func TestEncryptedKeysStorage_CRUD(t *testing.T) { s := &logical.InmemStorage{} policy := &Policy{ Name: "metadata", diff --git a/vault/cors.go b/vault/cors.go index a842db825..63a95b528 100644 --- a/vault/cors.go +++ b/vault/cors.go @@ -7,6 +7,7 @@ import ( "sync" "sync/atomic" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" ) @@ -26,6 +27,7 @@ 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.