From fd5ba4c5edbad09f878017af1de38e2ed6c8f6f2 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:00:00 -0400 Subject: [PATCH] Update capabilities command --- command/capabilities.go | 109 ++++++++++++--------- command/capabilities_test.go | 181 +++++++++++++++++++++++++++++------ 2 files changed, 215 insertions(+), 75 deletions(-) diff --git a/command/capabilities.go b/command/capabilities.go index bb60bd4ea..6a49ebe39 100644 --- a/command/capabilities.go +++ b/command/capabilities.go @@ -2,49 +2,86 @@ package command import ( "fmt" + "sort" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*CapabilitiesCommand)(nil) +var _ cli.CommandAutocomplete = (*CapabilitiesCommand)(nil) + // CapabilitiesCommand is a Command that enables a new endpoint. type CapabilitiesCommand struct { - meta.Meta + *BaseCommand +} + +func (c *CapabilitiesCommand) Synopsis() string { + return "Fetchs the capabilities of a token" +} + +func (c *CapabilitiesCommand) Help() string { + helpText := ` +Usage: vault capabilities [options] [TOKEN] PATH + + Fetches the capabilities of a token for a given path. If a TOKEN is provided + as an argument, the "/sys/capabilities" endpoint and permission is used. If + no TOKEN is provided, the "/sys/capabilities-self" endpoint and permission + is used with the locally authenticated token. + + List capabilities for the local token on the "secret/foo" path: + + $ vault capabilities secret/foo + + List capabilities for a token on the "cubbyhole/foo" path: + + $ vault capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 cubbyhole/foo + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *CapabilitiesCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *CapabilitiesCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *CapabilitiesCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *CapabilitiesCommand) Run(args []string) int { - flags := c.Meta.FlagSet("capabilities", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) > 2 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ncapabilities expects at most two arguments")) - return 1 - } - - var token string - var path string + token := "" + path := "" + args = f.Args() switch { case len(args) == 1: path = args[0] case len(args) == 2: - token = args[0] - path = args[1] + token, path = args[0], args[1] default: - flags.Usage() - c.Ui.Error(fmt.Sprintf("\ncapabilities expects at least one argument")) + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1-2, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } @@ -55,33 +92,11 @@ func (c *CapabilitiesCommand) Run(args []string) int { capabilities, err = client.Sys().Capabilities(token, path) } if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error retrieving capabilities: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error listing capabilities: %s", err)) + return 2 } - c.Ui.Output(fmt.Sprintf("Capabilities: %s", capabilities)) + sort.Strings(capabilities) + c.UI.Output(strings.Join(capabilities, ", ")) return 0 } - -func (c *CapabilitiesCommand) Synopsis() string { - return "Fetch the capabilities of a token on a given path" -} - -func (c *CapabilitiesCommand) Help() string { - helpText := ` -Usage: vault capabilities [options] [token] path - - Fetch the capabilities of a token on a given path. - If a token is provided as an argument, the '/sys/capabilities' endpoint will be invoked - with the given token; otherwise the '/sys/capabilities-self' endpoint will be invoked - with the client token. - - If a token does not have any capability on a given path, or if any of the policies - belonging to the token explicitly have ["deny"] capability, or if the argument path - is invalid, this command will respond with a ["deny"]. - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) -} diff --git a/command/capabilities_test.go b/command/capabilities_test.go index 5d106a14e..4ca2b1d28 100644 --- a/command/capabilities_test.go +++ b/command/capabilities_test.go @@ -1,45 +1,170 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestCapabilities_Basic(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - ui := new(cli.MockUi) - c := &CapabilitiesCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, +func testCapabilitiesCommand(tb testing.TB) (*cli.MockUi, *CapabilitiesCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &CapabilitiesCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestCapabilitiesCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar", "zip"}, + "Too many arguments", + 1, }, } - var args []string + for _, tc := range cases { + tc := tc - args = []string{"-address", addr} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure due to no args") + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ui, cmd := testCapabilitiesCommand(t) + + 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) + } + }) } - args = []string{"-address", addr, "testpath"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("token", func(t *testing.T) { + t.Parallel() - args = []string{"-address", addr, token, "test"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - args = []string{"-address", addr, "invalidtoken", "test"} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure due to invalid token") - } + policy := `path "secret/foo" { capabilities = ["read"] }` + if err := client.Sys().PutPolicy("policy", policy); err != nil { + t.Error(err) + } + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"policy"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("missing auth data: %#v", secret) + } + token := secret.Auth.ClientToken + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + token, "secret/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "read" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("local", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/foo" { capabilities = ["read"] }` + if err := client.Sys().PutPolicy("policy", policy); err != nil { + t.Error(err) + } + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"policy"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("missing auth data: %#v", secret) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "read" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo", "bar", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing capabilities: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testCapabilitiesCommand(t) + assertNoTabs(t, cmd) + }) }