From 9e4a1ed8a75f018eb773ac446f009061adf460a5 Mon Sep 17 00:00:00 2001 From: Matt Kulka Date: Sun, 21 Feb 2021 13:00:51 -0700 Subject: [PATCH] add an add command this command is basically `vault kv patch` but it's nice to have inside vsh when in interactive mode. you can simply add a key/value to a path. this can save you time if you're doing other operations inside vsh. i was also able to eliminate another duplication when creating new commands by making fetching the Command object from the Commands struct dynamic. --- CHANGELOG.md | 4 ++ README.md | 1 + cli/add.go | 101 ++++++++++++++++++++++++++++++++++ cli/command.go | 13 +++++ client/util.go | 7 ++- doc/commands/add.md | 23 ++++++++ main.go | 44 ++++----------- test/suites/commands/add.bats | 37 +++++++++++++ 8 files changed, 195 insertions(+), 35 deletions(-) create mode 100644 cli/add.go create mode 100644 doc/commands/add.md create mode 100644 test/suites/commands/add.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e466d1..94097cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master - unreleased +ENHANCEMENTS: + +* Add `add` command for single key insertion ([#86](https://github.com/fishi0x01/vsh/pull/87)) + BUG FIXES: * Don't show error on empty line enter in interactive mode ([#85](https://github.com/fishi0x01/vsh/pull/85)) diff --git a/README.md b/README.md index b6ebd281..a3cedc51 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Download latest static binaries from [release page](https://github.com/fishi0x01 ## Supported commands +- [add](doc/commands/add.md) adds a single key and value to a path - [append](doc/commands/append.md) merges secrets with different strategies (allows recursive operation on paths) - [cat](doc/commands/cat.md) shows the key/value pairs of a path - [cd](doc/commands/cd.md) allows interactive navigation through the paths diff --git a/cli/add.go b/cli/add.go new file mode 100644 index 00000000..88ec48a5 --- /dev/null +++ b/cli/add.go @@ -0,0 +1,101 @@ +package cli + +import ( + "fmt" + + "github.com/fishi0x01/vsh/client" + "github.com/fishi0x01/vsh/log" +) + +// AddCommand container for all 'append' parameters +type AddCommand struct { + name string + args *AddCommandArgs + + client *client.Client +} + +// AddCommandArgs provides a struct for go-arg parsing +type AddCommandArgs struct { + Key string `arg:"positional,required"` + Value string `arg:"positional,required"` + Path string `arg:"positional,required"` + Force bool `arg:"-f,--force" help:"Overwrite key if exists"` +} + +// Description provides detail on what the command does +func (AddCommandArgs) Description() string { + return "adds a key with value to a path" +} + +// NewAddCommand creates a new AddCommand parameter container +func NewAddCommand(c *client.Client) *AddCommand { + return &AddCommand{ + name: "add", + client: c, + args: &AddCommandArgs{}, + } +} + +// GetName returns the AddCommand's name identifier +func (cmd *AddCommand) GetName() string { + return cmd.name +} + +// GetArgs provides the struct holding arguments for the command +func (cmd *AddCommand) GetArgs() interface{} { + return cmd.args +} + +// IsSane returns true if command is sane +func (cmd *AddCommand) IsSane() bool { + return cmd.args.Key != "" && cmd.args.Value != "" && cmd.args.Path != "" +} + +// PrintUsage print command usage +func (cmd *AddCommand) PrintUsage() { + fmt.Println(Help(cmd)) +} + +// Parse parses the arguments into the Command and Args structs +func (cmd *AddCommand) Parse(args []string) error { + _, err := parseCommandArgs(args, cmd) + if err != nil { + return err + } + + return nil +} + +// Run executes 'add' with given AddCommand's parameters +func (cmd *AddCommand) Run() int { + path := cmdPath(cmd.client.Pwd, cmd.args.Path) + + pathType := cmd.client.GetType(path) + if pathType != client.LEAF { + log.UserError("Not a valid path for operation: %s", path) + return 1 + } + + err := cmd.addKeyValue(cmd.args.Path, cmd.args.Key, cmd.args.Value) + if err != nil { + log.UserError("Add failed: " + err.Error()) + return 1 + } + + return 0 +} + +func (cmd *AddCommand) addKeyValue(path string, key string, value string) error { + secret, err := cmd.client.Read(path) + if err != nil { + return fmt.Errorf("Read failed: %s", err) + } + data := secret.GetData() + if _, ok := data[key]; ok && !cmd.args.Force { + return fmt.Errorf("Key already exists at path: %s", path) + } + data[key] = value + secret.SetData(data) + return cmd.client.Write(path, secret) +} diff --git a/cli/command.go b/cli/command.go index 783dcb7f..33a89778 100644 --- a/cli/command.go +++ b/cli/command.go @@ -4,6 +4,7 @@ import ( "path/filepath" "strings" + "github.com/fatih/structs" "github.com/fishi0x01/vsh/client" "github.com/fishi0x01/vsh/log" ) @@ -20,6 +21,7 @@ type Command interface { // Commands contains all available commands type Commands struct { + Add *AddCommand Append *AppendCommand Cat *CatCommand Cd *CdCommand @@ -31,9 +33,20 @@ type Commands struct { Rm *RemoveCommand } +// Get returns the Command that matches the string +func (cmds *Commands) Get(cmd string) Command { + for _, f := range structs.Fields(cmds) { + if c := f.Value().(Command); cmd == c.GetName() { + return c + } + } + return nil +} + // NewCommands returns a Commands struct with all available commands func NewCommands(client *client.Client) *Commands { return &Commands{ + Add: NewAddCommand(client), Append: NewAppendCommand(client), Cat: NewCatCommand(client), Cd: NewCdCommand(client), diff --git a/client/util.go b/client/util.go index e0bbc50a..028899fb 100644 --- a/client/util.go +++ b/client/util.go @@ -98,8 +98,11 @@ func transformToKV2Secret(secret api.Secret) *api.Secret { } func normalizedVaultPath(absolutePath string) string { - // remove trailing '/' - return absolutePath[1:] + // remove leading '/' + if absolutePath[0] == '/' { + return absolutePath[1:] + } + return absolutePath } func sliceContains(arr []string, search string) bool { diff --git a/doc/commands/add.md b/doc/commands/add.md new file mode 100644 index 00000000..b2e3c60e --- /dev/null +++ b/doc/commands/add.md @@ -0,0 +1,23 @@ +# add + +```text +add [-f|--force] KEY VALUE PATH +``` + +Add operation adds or overwrites a single key at path. + +By default, it will add the key if it does not already exist. To overwrite the key if it exists, use the `--force` flag. + +```bash +> cat secret/path + +value = 1 +other = thing + +> add fizz buzz secret/path +> cat /secret/to + +value = 1 +fizz = buzz +other = thing +``` diff --git a/main.go b/main.go index a8281574..60bb3921 100644 --- a/main.go +++ b/main.go @@ -74,11 +74,16 @@ func executor(in string) { os.Exit(0) default: cmd, err = getCommand(args[0], commands) + if err == nil { + err = cmd.Parse(args[0]) + } } - if err != nil && cmd != nil { + if err != nil { log.UserError("%v", err) - cmd.PrintUsage() + if cmd != nil { + cmd.PrintUsage() + } } if err != nil && !isInteractive { @@ -94,38 +99,11 @@ func executor(in string) { } func getCommand(args []string, commands *cli.Commands) (cmd cli.Command, err error) { - switch args[0] { - case commands.Ls.GetName(): - err = commands.Ls.Parse(args) - cmd = commands.Ls - case commands.Cd.GetName(): - err = commands.Cd.Parse(args) - cmd = commands.Cd - case commands.Mv.GetName(): - err = commands.Mv.Parse(args) - cmd = commands.Mv - case commands.Append.GetName(): - err = commands.Append.Parse(args) - cmd = commands.Append - case commands.Cp.GetName(): - err = commands.Cp.Parse(args) - cmd = commands.Cp - case commands.Rm.GetName(): - err = commands.Rm.Parse(args) - cmd = commands.Rm - case commands.Cat.GetName(): - err = commands.Cat.Parse(args) - cmd = commands.Cat - case commands.Grep.GetName(): - err = commands.Grep.Parse(args) - cmd = commands.Grep - case commands.Replace.GetName(): - err = commands.Replace.Parse(args) - cmd = commands.Replace - default: - log.UserError("Not a valid command: %s", args[0]) - return nil, fmt.Errorf("not a valid command") + cmd = commands.Get(args[0]) + if cmd == nil { + return nil, fmt.Errorf("not a valid command: %s", args[0]) } + return cmd, err } diff --git a/test/suites/commands/add.bats b/test/suites/commands/add.bats new file mode 100644 index 00000000..669e4cb2 --- /dev/null +++ b/test/suites/commands/add.bats @@ -0,0 +1,37 @@ +load ../../util/common +load ../../util/standard-setup +load ../../bin/plugins/bats-support/load +load ../../bin/plugins/bats-assert/load + +@test "vault-${VAULT_VERSION} ${KV_BACKEND} 'add'" { + ####################################### + echo "==== case: add value to non existing path ====" + run ${APP_BIN} -c "add test value ${KV_BACKEND}/fake/path" + assert_failure + + ####################################### + echo "==== case: add key to existing path ====" + run ${APP_BIN} -c "add test value ${KV_BACKEND}/src/a/foo" + assert_success + + echo "ensure the key was written to destination" + run get_vault_value "test" "${KV_BACKEND}/src/a/foo" + assert_success + assert_output "value" + + ####################################### + echo "==== case: add existing key to existing path ====" + run ${APP_BIN} -c "add value another ${KV_BACKEND}/src/a/foo" + assert_failure + assert_output --partial "Key already exists at path: ${KV_BACKEND}/src/a/foo" + + ####################################### + echo "==== case: overwrite existing key to existing path ====" + run ${APP_BIN} -c "add -f value another ${KV_BACKEND}/src/a/foo" + assert_success + + echo "ensure the key was written to destination" + run get_vault_value "value" "${KV_BACKEND}/src/a/foo" + assert_success + assert_output "another" +}