diff --git a/command/auth.go b/command/auth.go index 00b21ce414..8ebf021bd1 100644 --- a/command/auth.go +++ b/command/auth.go @@ -1,22 +1,13 @@ package command import ( - "bufio" - "encoding/json" "fmt" "io" "os" - "sort" - "strconv" "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/kv-builder" - "github.com/hashicorp/vault/helper/password" - "github.com/hashicorp/vault/meta" - "github.com/mitchellh/mapstructure" "github.com/posener/complete" - "github.com/ryanuber/columnize" ) // AuthHandler is the interface that any auth handlers must implement @@ -28,39 +19,318 @@ type AuthHandler interface { // AuthCommand is a Command that handles authentication. type AuthCommand struct { - meta.Meta + *BaseCommand Handlers map[string]AuthHandler - // The fields below can be overwritten for tests - testStdin io.Reader + flagMethod string + flagPath string + flagNoVerify bool + flagNoStore bool + flagOnlyToken bool + + // Deprecations + // TODO: remove in 0.9.0 + flagTokenOnly bool + flagMethods bool + flagMethodHelp bool + + testStdin io.Reader // for tests +} + +func (c *AuthCommand) Synopsis() string { + return "Authenticates users or machines" +} + +func (c *AuthCommand) Help() string { + helpText := ` +Usage: vault auth [options] [AUTH K=V...] + + Authenticates users or machines to Vault using the provided arguments. By + default, the authentication method is "token". If not supplied via the CLI, + Vault will prompt for input. If argument is "-", the configuration options + are read from stdin. + + The -method flag allows alternative authentication providers to be used, + such as userpass, github, or cert. For these, additional "key=value" pairs + may be required. For example, to authenticate to the userpass auth backend: + + $ vault auth -method=userpass username=my-username + + Use "vault auth-help TYPE" for more information about the list of + configuration parameters and examples for a particular provider. Use the + "vault auth-list" command to see a list of enabled authentication providers. + + If an authentication provider is mounted at a different path, the -method + flag should by the canonical type, and the -path flag should be set to the + mount path. If a github authentication provider was mounted at "github-ent", + you would authenticate to this backend like this: + + $ vault auth -method=github -path=github-prod + + If the authentication is requested with response wrapping (via -wrap-ttl), + the returned token is automatically unwrapped unless: + + - The -only-token flag is used, in which case this command will output + the wrapping token + + - The -no-store flag is used, in which case this command will output + the details of the wrapping token. + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "method", + Target: &c.flagMethod, + Default: "token", + Completion: c.PredictVaultAvailableAuths(), + Usage: "Type of authentication to use such as \"userpass\" or " + + "\"ldap\". Note this corresponds to the TYPE, not the mount path. Use " + + "-path to specify the path where the authentication is mounted.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", + Completion: c.PredictVaultAuths(), + Usage: "Mount point where the auth backend is enabled. This defaults to " + + "the TYPE of backend (e.g. userpass -> userpass/).", + }) + + f.BoolVar(&BoolVar{ + Name: "no-verify", + Target: &c.flagNoVerify, + Default: false, + Usage: "Do not verify the token after authentication. By default, Vault " + + "issue a request to get more metdata about the token. This request " + + "against the use-limit of the token. Set this to true to disable the " + + "post-authenication lookup.", + }) + + f.BoolVar(&BoolVar{ + Name: "no-store", + Target: &c.flagNoStore, + Default: false, + Usage: "Do not persist the token to the token helper (usually the " + + "local filesystem) after authentication for use in future requests. " + + "The token will only be displayed in the command output.", + }) + + f.BoolVar(&BoolVar{ + Name: "only-token", + Target: &c.flagOnlyToken, + Default: false, + Usage: "Output only the token with no verification. This flag is a " + + "shortcut for \"-field=token -no-store -no-verify\". Setting those " + + "flags to other values will have no affect.", + }) + + // Deprecations + // TODO: remove in Vault 0.9.0 + + f.BoolVar(&BoolVar{ + Name: "token-only", // Prefer only-token + Target: &c.flagTokenOnly, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "methods", // Prefer auth-list + Target: &c.flagMethods, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "method-help", // Prefer auth-help + Target: &c.flagMethodHelp, + Default: false, + Hidden: true, + }) + + return set +} + +func (c *AuthCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *AuthCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *AuthCommand) Run(args []string) int { - var method, authPath string - var methods, methodHelp, noVerify, noStore, tokenOnly bool - flags := c.Meta.FlagSet("auth", meta.FlagSetDefault) - flags.BoolVar(&methods, "methods", false, "") - flags.BoolVar(&methodHelp, "method-help", false, "") - flags.BoolVar(&noVerify, "no-verify", false, "") - flags.BoolVar(&noStore, "no-store", false, "") - flags.BoolVar(&tokenOnly, "token-only", false, "") - flags.StringVar(&method, "method", "", "method") - flags.StringVar(&authPath, "path", "", "") - 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 } - if methods { - return c.listMethods() + args = f.Args() + + // Deprecations - do this before any argument validations + // TODO: remove in 0.9.0 + switch { + case c.flagMethods: + c.UI.Warn(wrapAtLength( + "WARNING! The -methods flag is deprecated. Please use " + + "\"vault auth-list\". This flag will be removed in the next major " + + "release of Vault.")) + cmd := &AuthListCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + } + return cmd.Run(nil) + case c.flagMethodHelp: + c.UI.Warn(wrapAtLength( + "WARNING! The -method-help flag is deprecated. Please use " + + "\"vault auth-help\". This flag will be removed in the next major " + + "release of Vault.")) + cmd := &AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + Handlers: c.Handlers, + } + return cmd.Run([]string{c.flagMethod}) } - args = flags.Args() + // TODO: remove in 0.9.0 + if c.flagTokenOnly { + c.UI.Warn(wrapAtLength( + "WARNING! The -token-only flag is deprecated. Plase use -only-token " + + "instead. This flag will be removed in the next major release of " + + "Vault.")) + c.flagOnlyToken = c.flagTokenOnly + } + + // Set the right flags if the user requested only-token - this overrides + // any previously configured values, as documented. + if c.flagOnlyToken { + c.flagNoStore = true + c.flagNoVerify = true + c.flagField = "token" + } + + // Get the auth method + authMethod := sanitizePath(c.flagMethod) + if authMethod == "" { + authMethod = "token" + } + + // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend + authPath := c.flagPath + if authPath == "" { + authPath = ensureTrailingSlash(authMethod) + } + + // Get the handler function + authHandler, ok := c.Handlers[authMethod] + if !ok { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Unknown authentication method: %s. Use \"vault auth-list\" to see the "+ + "complete list of authentication providers. Additionally, some "+ + "authentication providers are only available via the HTTP API.", + authMethod))) + return 1 + } + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + // If the user provided a token, pass it along to the auth provier. + if authMethod == "token" && len(args) == 1 { + args = []string{"token=" + args[0]} + } + + config, err := parseArgsDataString(stdin, args) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing configuration: %s", err)) + return 1 + } + + // If the user did not specify a mount path, use the provided mount path. + if config["mount"] == "" && authPath != "" { + config["mount"] = authPath + } + + // Create the client + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Authenticate delegation to the auth handler + secret, err := authHandler.Auth(client, config) + if err != nil { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Error authenticating: %s", err))) + return 2 + } + + // Unset any previous token wrapping functionality. If the original request + // was for a wrapped token, we don't want future requests to be wrapped. + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + // Recursively extract the token, handling wrapping + unwrap := !c.flagOnlyToken && !c.flagNoStore + secret, isWrapped, err := c.extractToken(client, secret, unwrap) + if err != nil { + c.UI.Error(fmt.Sprintf("Error extracting token: %s", err)) + return 2 + } + if secret == nil { + c.UI.Error("Vault returned an empty secret") + return 2 + } + + // Handle special cases if the token was wrapped + if isWrapped { + if c.flagOnlyToken { + return PrintRawField(c.UI, secret, "wrapping_token") + } + if c.flagNoStore { + return OutputSecret(c.UI, c.flagFormat, secret) + } + } + + // If we got this far, verify we have authentication data before continuing + if secret.Auth == nil { + c.UI.Error(wrapAtLength( + "Vault returned a secret, but the secret has no authentication " + + "information attached. This should never happen and is likely a " + + "bug.")) + return 2 + } + + // Pull the token itself out, since we don't need the rest of the auth + // information anymore/. + token := secret.Auth.ClientToken tokenHelper, err := c.TokenHelper() if err != nil { - c.Ui.Error(fmt.Sprintf( + c.UI.Error(fmt.Sprintf( "Error initializing token helper: %s\n\n"+ "Please verify that the token helper is available and properly\n"+ "configured for your system. Please refer to the documentation\n"+ @@ -69,489 +339,114 @@ func (c *AuthCommand) Run(args []string) int { return 1 } - // token is where the final token will go - handler := c.Handlers[method] + if !c.flagNoVerify { + // Verify the token and pull it's list of policies + client.SetToken(token) + client.SetWrappingLookupFunc(func(string, string) string { return "" }) - // Read token from stdin if first arg is exactly "-" - var stdin io.Reader = os.Stdin - if c.testStdin != nil { - stdin = c.testStdin - } - - if len(args) > 0 && args[0] == "-" { - stdinR := bufio.NewReader(stdin) - args[0], err = stdinR.ReadString('\n') - if err != nil && err != io.EOF { - c.Ui.Error(fmt.Sprintf("Error reading from stdin: %s", err)) - return 1 + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + c.UI.Error(fmt.Sprintf("Error verifying token: %s", err)) + return 2 } - args[0] = strings.TrimSpace(args[0]) - } - - if method == "" { - token := "" - if len(args) > 0 { - token = args[0] - } - - handler = &tokenAuthHandler{Token: token} - args = nil - - switch authPath { - case "", "auth/token": - default: - c.Ui.Error("Token authentication does not support custom paths") - return 1 + if secret == nil { + c.UI.Error("Empty response from lookup-self") + return 2 } } - if handler == nil { - methods := make([]string, 0, len(c.Handlers)) - for k := range c.Handlers { - methods = append(methods, k) - } - sort.Strings(methods) - - c.Ui.Error(fmt.Sprintf( - "Unknown authentication method: %s\n\n"+ - "Please use a supported authentication method. The list of supported\n"+ - "authentication methods is shown below. Note that this list may not\n"+ - "be exhaustive: Vault may support other auth methods. For auth methods\n"+ - "unsupported by the CLI, please use the HTTP API.\n\n"+ - "%s", - method, - strings.Join(methods, ", "))) - return 1 - } - - if methodHelp { - c.Ui.Output(handler.Help()) - return 0 - } - - // Warn if the VAULT_TOKEN environment variable is set, as that will take - // precedence. Don't output on token-only since we're likely piping output. - if os.Getenv("VAULT_TOKEN") != "" && !tokenOnly { - c.Ui.Output("==> WARNING: VAULT_TOKEN environment variable set!\n") - c.Ui.Output(" The environment variable takes precedence over the value") - c.Ui.Output(" set by the auth command. Either update the value of the") - c.Ui.Output(" environment variable or unset it to use the new token.\n") - } - - var vars map[string]string - if len(args) > 0 { - builder := kvbuilder.Builder{Stdin: os.Stdin} - if err := builder.Add(args...); err != nil { - c.Ui.Error(err.Error()) - return 1 + if !c.flagNoStore { + // Store the token in the local client + if err := tokenHelper.Store(token); err != nil { + c.UI.Error(fmt.Sprintf("Error storing token: %s", err)) + c.UI.Error(wrapAtLength( + "Authentication was successful, but the token was not persisted. The " + + "resulting token is shown below for your records.")) + OutputSecret(c.UI, c.flagFormat, secret) + return 2 } - if err := mapstructure.Decode(builder.Map(), &vars); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing options: %s", err)) - return 1 + // Warn if the VAULT_TOKEN environment variable is set, as that will take + // precedence. Don't output on token-only since we're likely piping output. + if c.flagField == "" && c.flagFormat == "table" { + if os.Getenv("VAULT_TOKEN") != "" { + c.UI.Warn(wrapAtLength("WARNING! The VAULT_TOKEN environment variable " + + "is set! This takes precedence over the value set by this command. To " + + "use the value set by this command, unset the VAULT_TOKEN environment " + + "variable or set it to the token displayed below.")) + } } + } + + // If the user requested a particular field, print that out now since we + // are likely piping to another process. + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + // Output the secret as json or yaml if requested. We have to maintain + // backwards compatiability + if c.flagFormat != "table" { + return OutputSecret(c.UI, c.flagFormat, secret) + } + + output := "Success! You are now authenticated. " + if c.flagNoVerify { + output += "The token was not verified for validity. " + } + if c.flagNoStore { + output += "The token was not stored in the token helper. " } else { - vars = make(map[string]string) + output += "The token information displayed below is already stored in " + + "the token helper. You do NOT need to run \"vault auth\" again." + } + c.UI.Output(wrapAtLength(output) + "\n") + + // TODO make this consistent with other printed token secrets. + c.UI.Output(fmt.Sprintf("token: %s", secret.TokenID())) + c.UI.Output(fmt.Sprintf("accessor: %s", secret.TokenAccessor())) + + if ttl := secret.TokenTTL(); ttl != 0 { + c.UI.Output(fmt.Sprintf("duration: %s", ttl)) } - // Build the client so we can auth - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client to auth: %s", err)) - return 1 + c.UI.Output(fmt.Sprintf("renewable: %t", secret.TokenIsRenewable())) + + if policies := secret.TokenPolicies(); len(policies) > 0 { + c.UI.Output(fmt.Sprintf("policies: %s", policies)) } - if authPath != "" { - vars["mount"] = authPath - } + return 0 +} - // Authenticate - secret, err := handler.Auth(client, vars) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if secret == nil { - c.Ui.Error("Empty response from auth helper") - return 1 - } - - // If we had requested a wrapped token, we want to unset that request - // before performing further functions - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - -CHECK_TOKEN: - var token string +// extractToken extracts the token from the given secret, automatically +// unwrapping responses and handling error conditions if unwrap is true. The +// result also returns whether it was a wrapped resonse that was not unwrapped. +func (c *AuthCommand) extractToken(client *api.Client, secret *api.Secret, unwrap bool) (*api.Secret, bool, error) { switch { case secret == nil: - c.Ui.Error("Empty response from auth helper") - return 1 + return nil, false, fmt.Errorf("empty response from auth helper") case secret.Auth != nil: - token = secret.Auth.ClientToken + return secret, false, nil case secret.WrapInfo != nil: if secret.WrapInfo.WrappedAccessor == "" { - c.Ui.Error("Got a wrapped response from Vault but wrapped reply does not seem to contain a token") - return 1 + return nil, false, fmt.Errorf("wrapped response does not contain a token") } - if tokenOnly { - c.Ui.Output(secret.WrapInfo.Token) - return 0 - } - if noStore { - return OutputSecret(c.Ui, "table", secret) + + if !unwrap { + return secret, true, nil } + client.SetToken(secret.WrapInfo.Token) - secret, err = client.Logical().Unwrap("") - goto CHECK_TOKEN + secret, err := client.Logical().Unwrap("") + if err != nil { + return nil, false, err + } + return c.extractToken(client, secret, unwrap) default: - c.Ui.Error("No auth or wrapping info in auth helper response") - return 1 - } - - // Cache the previous token so that it can be restored if authentication fails - var previousToken string - if previousToken, err = tokenHelper.Get(); err != nil { - c.Ui.Error(fmt.Sprintf("Error caching the previous token: %s\n\n", err)) - return 1 - } - - if tokenOnly { - c.Ui.Output(token) - return 0 - } - - // Store the token! - if !noStore { - if err := tokenHelper.Store(token); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error storing token: %s\n\n"+ - "Authentication was not successful and did not persist.\n"+ - "Please reauthenticate, or fix the issue above if possible.", - err)) - return 1 - } - } - - if noVerify { - c.Ui.Output(fmt.Sprintf( - "Authenticated - no token verification has been performed.", - )) - - if noStore { - if err := tokenHelper.Erase(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error removing prior token: %s\n\n"+ - "Authentication was successful, but unable to remove the\n"+ - "previous token.", - err)) - return 1 - } - } - return 0 - } - - // Build the client again so it can read the token we just wrote - client, err = c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client to verify the token: %s", err)) - if !noStore { - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - } - return 1 - } - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - - // If in no-store mode it won't have read the token from a token-helper (or - // will read an old one) so set it explicitly - if noStore { - client.SetToken(token) - } - - // Verify the token - secret, err = client.Auth().Token().LookupSelf() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error validating token: %s", err)) - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - return 1 - } - if secret == nil && !noStore { - c.Ui.Error(fmt.Sprintf("Error: Invalid token")) - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - return 1 - } - - if noStore { - if err := tokenHelper.Erase(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error removing prior token: %s\n\n"+ - "Authentication was successful, but unable to remove the\n"+ - "previous token.", - err)) - return 1 - } - } - - // Get the policies we have - policiesRaw, ok := secret.Data["policies"] - if !ok || policiesRaw == nil { - policiesRaw = []interface{}{"unknown"} - } - var policies []string - for _, v := range policiesRaw.([]interface{}) { - policies = append(policies, v.(string)) - } - - output := "Successfully authenticated! You are now logged in." - if noStore { - output += "\nThe token has not been stored to the configured token helper." - } - if method != "" { - output += "\nThe token below is already saved in the session. You do not" - output += "\nneed to \"vault auth\" again with the token." - } - output += fmt.Sprintf("\ntoken: %s", secret.Data["id"]) - output += fmt.Sprintf("\ntoken_duration: %s", secret.Data["ttl"].(json.Number).String()) - if len(policies) > 0 { - output += fmt.Sprintf("\ntoken_policies: %v", policies) - } - - c.Ui.Output(output) - - return 0 - -} - -func (c *AuthCommand) getMethods() (map[string]*api.AuthMount, error) { - client, err := c.Client() - if err != nil { - return nil, err - } - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - - auth, err := client.Sys().ListAuth() - if err != nil { - return nil, err - } - - return auth, nil -} - -func (c *AuthCommand) listMethods() int { - auth, err := c.getMethods() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading auth table: %s", err)) - return 1 - } - - paths := make([]string, 0, len(auth)) - for path := range auth { - paths = append(paths, path) - } - sort.Strings(paths) - - columns := []string{"Path | Type | Accessor | Default TTL | Max TTL | Replication Behavior | Description"} - for _, path := range paths { - auth := auth[path] - defTTL := "system" - if auth.Config.DefaultLeaseTTL != 0 { - defTTL = strconv.Itoa(auth.Config.DefaultLeaseTTL) - } - maxTTL := "system" - if auth.Config.MaxLeaseTTL != 0 { - maxTTL = strconv.Itoa(auth.Config.MaxLeaseTTL) - } - replicatedBehavior := "replicated" - if auth.Local { - replicatedBehavior = "local" - } - columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s | %s | %s", path, auth.Type, auth.Accessor, defTTL, maxTTL, replicatedBehavior, auth.Description)) - } - - c.Ui.Output(columnize.SimpleFormat(columns)) - return 0 -} - -func (c *AuthCommand) Synopsis() string { - return "Prints information about how to authenticate with Vault" -} - -func (c *AuthCommand) Help() string { - helpText := ` -Usage: vault auth [options] [auth-information] - - Authenticate with Vault using the given token or via any supported - authentication backend. - - By default, the -method is assumed to be token. If not supplied via the - command-line, a prompt for input will be shown. If the authentication - information is "-", it will be read from stdin. - - The -method option allows alternative authentication methods to be used, - such as userpass, GitHub, or TLS certificates. For these, additional - values as "key=value" pairs may be required. For example, to authenticate - to the userpass auth backend: - - $ vault auth -method=userpass username=my-username - - Use "-method-help" to get help for a specific method. - - If an auth backend is enabled at a different path, the "-method" flag - should still point to the canonical name, and the "-path" flag should be - used. If a GitHub auth backend was mounted as "github-private", one would - authenticate to this backend via: - - $ vault auth -method=github -path=github-private - - The value of the "-path" flag is supplied to auth providers as the "mount" - option in the payload to specify the mount point. - - If response wrapping is used (via -wrap-ttl), the returned token will be - automatically unwrapped unless: - * -token-only is used, in which case the wrapping token will be output - * -no-store is used, in which case the details of the wrapping token - will be printed - -General Options: - - ` + meta.GeneralOptionsUsage() + ` - -Auth Options: - - -method=name Use the method given here, which is a type of backend, not - the path. If this authentication method is not available, - exit with code 1. - - -method-help If set, the help for the selected method will be shown. - - -methods List the available auth methods. - - -no-verify Do not verify the token after creation; avoids a use count - decrement. - - -no-store Do not store the token after creation; it will only be - displayed in the command output. - - -token-only Output only the token to stdout. This implies -no-verify - and -no-store. - - -path The path at which the auth backend is enabled. If an auth - backend is mounted at multiple paths, this option can be - used to authenticate against specific paths. -` - return strings.TrimSpace(helpText) -} - -// tokenAuthHandler handles retrieving the token from the command-line. -type tokenAuthHandler struct { - Token string -} - -func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (*api.Secret, error) { - token := h.Token - if token == "" { - var err error - - // No arguments given, read the token from user input - fmt.Printf("Token (will be hidden): ") - token, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - return nil, fmt.Errorf( - "Error attempting to ask for token. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into auth. If you want to\n"+ - "pipe the token, please pass '-' as the token argument.\n\n"+ - "Raw error: %s", err) - } - } - - if token == "" { - return nil, fmt.Errorf( - "A token must be passed to auth. Please view the help\n" + - "for more information.") - } - - return &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: token, - }, - }, nil -} - -func (h *tokenAuthHandler) Help() string { - help := ` -No method selected with the "-method" flag, so the "auth" command assumes -you'll be using raw token authentication. For this, specify the token to -authenticate as the parameter to "vault auth". Example: - - vault auth 123456 - -The token used to authenticate must come from some other source. A root -token is created when Vault is first initialized. After that, subsequent -tokens are created via the API or command line interface (with the -"token"-prefixed commands). -` - - return strings.TrimSpace(help) -} - -func (c *AuthCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing -} - -func (c *AuthCommand) AutocompleteFlags() complete.Flags { - var predictFunc complete.PredictFunc = func(a complete.Args) []string { - auths, err := c.getMethods() - if err != nil { - return []string{} - } - - methods := make([]string, 0, len(auths)) - for _, auth := range auths { - if strings.HasPrefix(auth.Type, a.Last) { - methods = append(methods, auth.Type) - } - } - - return methods - } - - return complete.Flags{ - "-method": predictFunc, - "-methods": complete.PredictNothing, - "-method-help": complete.PredictNothing, - "-no-verify": complete.PredictNothing, - "-no-store": complete.PredictNothing, - "-token-only": complete.PredictNothing, - "-path": complete.PredictNothing, + return nil, false, fmt.Errorf("no auth or wrapping info in response") } } diff --git a/command/auth_test.go b/command/auth_test.go index 8243129083..ca1e916284 100644 --- a/command/auth_test.go +++ b/command/auth_test.go @@ -1,400 +1,538 @@ package command import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" "strings" "testing" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/logical" + "github.com/mitchellh/cli" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" + credToken "github.com/hashicorp/vault/builtin/credential/token" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/command/token" ) -func TestAuth_methods(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) { + tb.Helper() - testAuthInit(t) + ui := cli.NewMockUi() + return ui, &AuthCommand{ + BaseCommand: &BaseCommand{ + UI: ui, - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - TokenHelper: DefaultTokenHelper, + // Override to our own token helper + tokenHelper: token.NewTestingTokenHelper(), + }, + Handlers: map[string]AuthHandler{ + "token": &credToken.CLIHandler{}, + "userpass": &credUserpass.CLIHandler{}, }, - } - - args := []string{ - "-address", addr, - "-methods", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if !strings.Contains(output, "token") { - t.Fatalf("bad: %#v", output) } } -func TestAuth_token(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestAuthCommand_Run(t *testing.T) { + t.Parallel() - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, + deprecations := []struct { + name string + args []string + out string + code int + }{ + { + "methods", + []string{"-methods"}, + "token/", + 0, + }, + { + "method_help", + []string{"-method", "userpass", "-method-help"}, + "Usage: vault auth -method=userpass", + 0, }, } - args := []string{ - "-address", addr, - token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("deprecations", func(t *testing.T) { + t.Parallel() - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } + for _, tc := range deprecations { + tc := tc - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - if actual != token { - t.Fatalf("bad: %s", actual) - } -} + client, closer := testVaultServer(t) + defer closer() -func TestAuth_wrapping(t *testing.T) { - baseConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": credUserpass.Factory, - }, - } - cluster := vault.NewTestCluster(t, baseConfig, &vault.TestClusterOptions{ - HandlerFunc: http.Handler, - BaseListenAddress: "127.0.0.1:8200", + ui, cmd := testAuthCommand(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) + } + }) + } }) - cluster.Start() - defer cluster.Cleanup() - testAuthInit(t) + t.Run("custom_path", func(t *testing.T) { + t.Parallel() - client := cluster.Cores[0].Client - err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-method", "userpass", + "-path", "my-auth", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! You are now authenticated." + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to be %q", combined, expected) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if l, exp := len(storedToken), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken) + } }) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ - "password": "bar", - "policies": "zip,zap", + + t.Run("no_verify", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + NumUses: 1, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testAuthCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-no-verify", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + lookup, err := client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + // There was 1 use to start, make sure we didn't use it (verifying would + // use it). + uses := lookup.TokenRemainingUses() + if uses != 1 { + t.Errorf("expected %d to be %d", uses, 1) + } }) - if err != nil { - t.Fatal(err) - } - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + t.Run("no_store", func(t *testing.T) { + t.Parallel() - args := []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - // Test again with wrapping - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + _, cmd := testAuthCommand(t) + cmd.client = client - // Test again with no-store - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-no-store", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + // Ensure we have no token to start + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Errorf("expected token helper to be empty: %s: %q", err, storedToken) + } - // Test again with wrapping and token-only - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + code := cmd.Run([]string{ + "-no-store", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-token-only", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - token := strings.TrimSpace(ui.OutputWriter.String()) - if token == "" { - t.Fatal("expected to find token in output") - } - secret, err := client.Logical().Unwrap(token) - if err != nil { - t.Fatal(err) - } - if secret.Auth.ClientToken == "" { - t.Fatal("no client token found") - } + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if exp := ""; storedToken != exp { + t.Errorf("expected %q to be %q", storedToken, exp) + } + }) + + t.Run("stores", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if storedToken != token { + t.Errorf("expected %q to be %q", storedToken, token) + } + }) + + t.Run("only_token", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-only-token", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Verify only the token was printed + token := ui.OutputWriter.String() + ui.ErrorWriter.String() + if l, exp := len(token), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token) + } + + // Verify the token was not stored + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expted token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("failure_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "not-a-real-token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error verifying token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("wrap_auto_unwrap", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + _, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + token, err := tokenHelper.Get() + if err != nil || token == "" { + t.Fatalf("expected token from helper: %s: %q", err, token) + } + + // Ensure the resulting token is unwrapped + secret, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Error(err) + } + + if secret.WrapInfo != nil { + t.Errorf("expected to be unwrapped: %#v", secret) + } + }) + + t.Run("wrap_only_token", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-only-token", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + token := ui.OutputWriter.String() + if token == "" { + t.Errorf("expected %q to not be %q", token, "") + } + if strings.Contains(token, "\n") { + t.Errorf("expected %q to not contain %q", token, "\n") + } + + // Ensure the resulting token is, in fact, still wrapped. + client.SetToken(token) + secret, err := client.Logical().Unwrap("") + if err != nil { + t.Error(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("expected secret to have auth: %#v", secret) + } + }) + + t.Run("wrap_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-no-store", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + expected := "wrapping_token" + output := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(output, expected) { + t.Errorf("expected %q to contain %q", output, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error verifying token: " + 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 := testAuthCommand(t) + assertNoTabs(t, cmd) + }) } - -func TestAuth_token_nostore(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "-no-store", - token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual != "" { - t.Fatalf("bad: %s", actual) - } -} - -func TestAuth_stdin(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - stdinR, stdinW := io.Pipe() - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - testStdin: stdinR, - } - - go func() { - stdinW.Write([]byte(token)) - stdinW.Close() - }() - - args := []string{ - "-address", addr, - "-", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestAuth_badToken(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "not-a-valid-token", - } - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestAuth_method(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Handlers: map[string]AuthHandler{ - "test": &testAuthHandler{}, - }, - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "-method=test", - "foo=" + token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual != token { - t.Fatalf("bad: %s", actual) - } -} - -func testAuthInit(t *testing.T) { - td, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - - // Set the HOME env var so we get that right - os.Setenv("HOME", td) - - // Write a .vault config to use our custom token helper - config := fmt.Sprintf( - "token_helper = \"\"\n") - ioutil.WriteFile(filepath.Join(td, ".vault"), []byte(config), 0644) -} - -type testAuthHandler struct{} - -func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { - return &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: m["foo"], - }, - }, nil -} - -func (h *testAuthHandler) Help() string { return "" }