From 223c4fc3250f8a280550a7d3fed3c993efb13df1 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 31 Aug 2017 16:57:00 -0400 Subject: [PATCH] Change auth helper interface to api.Secret. (#3263) This allows us to properly handle wrapped responses. Fixes #3217 --- builtin/credential/aws/cli.go | 12 +-- builtin/credential/cert/cli.go | 10 +- builtin/credential/github/cli.go | 10 +- builtin/credential/ldap/cli.go | 12 +-- builtin/credential/okta/cli.go | 12 +-- builtin/credential/userpass/cli.go | 14 +-- command/auth.go | 81 ++++++++++++--- command/auth_test.go | 160 ++++++++++++++++++++++++++++- 8 files changed, 262 insertions(+), 49 deletions(-) diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 4a53f7f33..2842c24db 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -69,7 +69,7 @@ func GenerateLoginData(accessKey, secretKey, sessionToken, headerValue string) ( return loginData, nil } -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "aws" @@ -87,23 +87,23 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { loginData, err := GenerateLoginData(m["aws_access_key_id"], m["aws_secret_access_key"], m["aws_security_token"], headerValue) if err != nil { - return "", err + return nil, err } if loginData == nil { - return "", fmt.Errorf("got nil response from GenerateLoginData") + return nil, fmt.Errorf("got nil response from GenerateLoginData") } loginData["role"] = role path := fmt.Sprintf("auth/%s/login", mount) secret, err := c.Logical().Write(path, loginData) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 66809c2e3..a1071fcd3 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -10,13 +10,13 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { var data struct { Mount string `mapstructure:"mount"` Name string `mapstructure:"name"` } if err := mapstructure.WeakDecode(m, &data); err != nil { - return "", err + return nil, err } if data.Mount == "" { @@ -29,13 +29,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login", data.Mount) secret, err := c.Logical().Write(path, options) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/github/cli.go b/builtin/credential/github/cli.go index dda1dac44..557939b20 100644 --- a/builtin/credential/github/cli.go +++ b/builtin/credential/github/cli.go @@ -10,7 +10,7 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "github" @@ -19,7 +19,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { token, ok := m["token"] if !ok { if token = os.Getenv("VAULT_AUTH_GITHUB_TOKEN"); token == "" { - return "", fmt.Errorf("GitHub token should be provided either as 'value' for 'token' key,\nor via an env var VAULT_AUTH_GITHUB_TOKEN") + return nil, fmt.Errorf("GitHub token should be provided either as 'value' for 'token' key,\nor via an env var VAULT_AUTH_GITHUB_TOKEN") } } @@ -28,13 +28,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { "token": token, }) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/ldap/cli.go b/builtin/credential/ldap/cli.go index e4d151faf..262bc998e 100644 --- a/builtin/credential/ldap/cli.go +++ b/builtin/credential/ldap/cli.go @@ -11,7 +11,7 @@ import ( type CLIHandler struct{} -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "ldap" @@ -21,7 +21,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { if !ok { username = usernameFromEnv() if username == "" { - return "", fmt.Errorf("'username' not supplied and neither 'LOGNAME' nor 'USER' env vars set") + return nil, fmt.Errorf("'username' not supplied and neither 'LOGNAME' nor 'USER' env vars set") } } password, ok := m["password"] @@ -31,7 +31,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { password, err = pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } } @@ -51,13 +51,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", mount, username) secret, err := c.Logical().Write(path, data) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/builtin/credential/okta/cli.go b/builtin/credential/okta/cli.go index 355e8cb9b..f5f850209 100644 --- a/builtin/credential/okta/cli.go +++ b/builtin/credential/okta/cli.go @@ -13,7 +13,7 @@ import ( type CLIHandler struct{} // Auth cli method -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { mount, ok := m["mount"] if !ok { mount = "okta" @@ -21,7 +21,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { username, ok := m["username"] if !ok { - return "", fmt.Errorf("'username' var must be set") + return nil, fmt.Errorf("'username' var must be set") } password, ok := m["password"] if !ok { @@ -30,7 +30,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { password, err = pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } } @@ -41,13 +41,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", mount, username) secret, err := c.Logical().Write(path, data) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } // Help method for okta cli diff --git a/builtin/credential/userpass/cli.go b/builtin/credential/userpass/cli.go index 80b52e339..4433c0e70 100644 --- a/builtin/credential/userpass/cli.go +++ b/builtin/credential/userpass/cli.go @@ -14,7 +14,7 @@ type CLIHandler struct { DefaultMount string } -func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { var data struct { Username string `mapstructure:"username"` Password string `mapstructure:"password"` @@ -23,18 +23,18 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { Passcode string `mapstructure:"passcode"` } if err := mapstructure.WeakDecode(m, &data); err != nil { - return "", err + return nil, err } if data.Username == "" { - return "", fmt.Errorf("'username' must be specified") + return nil, fmt.Errorf("'username' must be specified") } if data.Password == "" { fmt.Printf("Password (will be hidden): ") password, err := pwd.Read(os.Stdin) fmt.Println() if err != nil { - return "", err + return nil, err } data.Password = password } @@ -55,13 +55,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { path := fmt.Sprintf("auth/%s/login/%s", data.Mount, data.Username) secret, err := c.Logical().Write(path, options) if err != nil { - return "", err + return nil, err } if secret == nil { - return "", fmt.Errorf("empty response from credential provider") + return nil, fmt.Errorf("empty response from credential provider") } - return secret.Auth.ClientToken, nil + return secret, nil } func (h *CLIHandler) Help() string { diff --git a/command/auth.go b/command/auth.go index 42754aad3..00b21ce41 100644 --- a/command/auth.go +++ b/command/auth.go @@ -22,7 +22,7 @@ import ( // AuthHandler is the interface that any auth handlers must implement // to enable auth via the CLI. type AuthHandler interface { - Auth(*api.Client, map[string]string) (string, error) + Auth(*api.Client, map[string]string) (*api.Secret, error) Help() string } @@ -167,11 +167,52 @@ func (c *AuthCommand) Run(args []string) int { } // Authenticate - token, err := handler.Auth(client, vars) + 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 + switch { + case secret == nil: + c.Ui.Error("Empty response from auth helper") + return 1 + + case secret.Auth != nil: + token = secret.Auth.ClientToken + + 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 + } + if tokenOnly { + c.Ui.Output(secret.WrapInfo.Token) + return 0 + } + if noStore { + return OutputSecret(c.Ui, "table", secret) + } + client.SetToken(secret.WrapInfo.Token) + secret, err = client.Logical().Unwrap("") + goto CHECK_TOKEN + + 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 @@ -230,6 +271,9 @@ func (c *AuthCommand) Run(args []string) int { } 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 @@ -238,7 +282,7 @@ func (c *AuthCommand) Run(args []string) int { } // Verify the token - secret, err := client.Auth().Token().LookupSelf() + secret, err = client.Auth().Token().LookupSelf() if err != nil { c.Ui.Error(fmt.Sprintf( "Error validating token: %s", err)) @@ -274,8 +318,8 @@ func (c *AuthCommand) Run(args []string) int { // Get the policies we have policiesRaw, ok := secret.Data["policies"] - if !ok { - policiesRaw = []string{"unknown"} + if !ok || policiesRaw == nil { + policiesRaw = []interface{}{"unknown"} } var policies []string for _, v := range policiesRaw.([]interface{}) { @@ -307,6 +351,9 @@ func (c *AuthCommand) getMethods() (map[string]*api.AuthMount, error) { if err != nil { return nil, err } + client.SetWrappingLookupFunc(func(string, string) string { + return "" + }) auth, err := client.Sys().ListAuth() if err != nil { @@ -387,15 +434,21 @@ Usage: vault auth [options] [auth-information] 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 Outputs help for the authentication method with the given - name for the remote server. If this authentication method - is not available, exit with code 1. + -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. @@ -422,7 +475,7 @@ type tokenAuthHandler struct { Token string } -func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) { +func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (*api.Secret, error) { token := h.Token if token == "" { var err error @@ -432,7 +485,7 @@ func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) token, err = password.Read(os.Stdin) fmt.Printf("\n") if err != nil { - return "", fmt.Errorf( + 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"+ @@ -442,12 +495,16 @@ func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (string, error) } if token == "" { - return "", fmt.Errorf( + return nil, fmt.Errorf( "A token must be passed to auth. Please view the help\n" + "for more information.") } - return token, nil + return &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: token, + }, + }, nil } func (h *tokenAuthHandler) Help() string { diff --git a/command/auth_test.go b/command/auth_test.go index 6071ea470..824312908 100644 --- a/command/auth_test.go +++ b/command/auth_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" @@ -84,6 +87,155 @@ func TestAuth_token(t *testing.T) { } } +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", + }) + cluster.Start() + defer cluster.Cleanup() + + testAuthInit(t) + + client := cluster.Cores[0].Client + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ + "password": "bar", + "policies": "zip,zap", + }) + 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"}, + }, + } + + 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()) + } + + // 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"}, + }, + } + + 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()) + } + + // 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"}, + }, + } + + 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()) + } + + // 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"}, + }, + } + + 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") + } +} + func TestAuth_token_nostore(t *testing.T) { core, _, token := vault.TestCoreUnsealed(t) ln, addr := http.TestServer(t, core) @@ -237,8 +389,12 @@ func testAuthInit(t *testing.T) { type testAuthHandler struct{} -func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (string, error) { - return m["foo"], nil +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 "" }