diff --git a/command/auth_help.go b/command/auth_help.go new file mode 100644 index 0000000000..7fbd5e1443 --- /dev/null +++ b/command/auth_help.go @@ -0,0 +1,126 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuthHelpCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthHelpCommand)(nil) + +// AuthHelpCommand is a Command that prints help output for a given auth +// provider +type AuthHelpCommand struct { + *BaseCommand + + Handlers map[string]AuthHandler +} + +func (c *AuthHelpCommand) Synopsis() string { + return "Prints usage for an auth provider" +} + +func (c *AuthHelpCommand) Help() string { + helpText := ` +Usage: vault path-help [options] TYPE | PATH + + Prints usage and help for an authentication provider. If provided a TYPE, + this command retrieves the default help for the given authentication + provider of that type. If given a PATH, this command returns the help + output for the authentication provider mounted at that path. If given a + PATH argument, the path must exist and be mounted. + + Get usage instructions for the userpass authentication provider: + + $ vault auth-help userpass + + Print usage for the authentication provider mounted at my-provider/ + + $ vault auth-help my-provider/: + + Each authentication provider produces its own help output. For additional + information, please view the online documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthHelpCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *AuthHelpCommand) AutocompleteArgs() complete.Predictor { + handlers := make([]string, 0, len(c.Handlers)) + for k := range c.Handlers { + handlers = append(handlers, k) + } + return complete.PredictSet(handlers...) +} + +func (c *AuthHelpCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthHelpCommand) 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 + } + + // Start with the assumption that we have an auth type, not a path. + authType := strings.TrimSpace(args[0]) + + authHandler, ok := c.Handlers[authType] + if !ok { + // There was no auth type by that name, see if it's a mount + auths, err := client.Sys().ListAuth() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing authentication providers: %s", err)) + return 2 + } + + authPath := ensureTrailingSlash(sanitizePath(args[0])) + auth, ok := auths[authPath] + if !ok { + c.UI.Error(fmt.Sprintf( + "Error retrieving help: unknown authentication provider: %s", args[0])) + return 1 + } + + authHandler, ok = c.Handlers[auth.Type] + if !ok { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "INTERNAL ERROR! Found an authentication provider mounted at %s, but "+ + "its type %q is not registered in Vault. This is a bug and should "+ + "be reported. Please open an issue at github.com/hashicorp/vault.", + authPath, authType))) + return 2 + } + } + + c.UI.Output(authHandler.Help()) + return 0 +} diff --git a/command/auth_help_test.go b/command/auth_help_test.go new file mode 100644 index 0000000000..b157552019 --- /dev/null +++ b/command/auth_help_test.go @@ -0,0 +1,152 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" + + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" +) + +func testAuthHelpCommand(tb testing.TB) (*cli.MockUi, *AuthHelpCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{ + DefaultMount: "userpass", + }, + }, + } +} + +func TestAuthHelpCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ui, cmd := testAuthHelpCommand(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) + } + }) + } + + t.Run("path", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("foo", "userpass", ""); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Usage: vault auth -method=userpass" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("type", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + // No mounted auth backends + + ui, cmd := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "userpass", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Usage: vault auth -method=userpass" + 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 := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "sys/mounts", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing authentication providers: " + 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 := testAuthHelpCommand(t) + assertNoTabs(t, cmd) + }) +}