diff --git a/clientv3/auth.go b/clientv3/auth.go index 4bc5eaf39..aeb106b8d 100644 --- a/clientv3/auth.go +++ b/clientv3/auth.go @@ -52,9 +52,6 @@ type Auth interface { // AuthDisable disables auth of an etcd cluster. AuthDisable(ctx context.Context) (*AuthDisableResponse, error) - // Authenticate does authenticate with given user name and password. - Authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) - // UserAdd adds a new user to an etcd cluster. UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error) @@ -100,11 +97,6 @@ func (auth *auth) AuthDisable(ctx context.Context) (*AuthDisableResponse, error) return (*AuthDisableResponse)(resp), rpctypes.Error(err) } -func (auth *auth) Authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) { - resp, err := auth.remote.Authenticate(ctx, &pb.AuthenticateRequest{Name: name, Password: password}) - return (*AuthenticateResponse)(resp), rpctypes.Error(err) -} - func (auth *auth) UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error) { resp, err := auth.remote.UserAdd(ctx, &pb.AuthUserAddRequest{Name: name, Password: password}) return (*AuthUserAddResponse)(resp), rpctypes.Error(err) @@ -146,3 +138,29 @@ func StrToPermissionType(s string) (PermissionType, error) { } return PermissionType(-1), fmt.Errorf("invalid permission type: %s", s) } + +type authenticator struct { + conn *grpc.ClientConn // conn in-use + remote pb.AuthClient +} + +func (auth *authenticator) authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) { + resp, err := auth.remote.Authenticate(ctx, &pb.AuthenticateRequest{Name: name, Password: password}) + return (*AuthenticateResponse)(resp), rpctypes.Error(err) +} + +func (auth *authenticator) close() { + auth.conn.Close() +} + +func newAuthenticator(endpoint string, opts []grpc.DialOption) (*authenticator, error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return nil, err + } + + return &authenticator{ + conn: conn, + remote: pb.NewAuthClient(conn), + }, nil +} diff --git a/clientv3/client.go b/clientv3/client.go index 3a7e69a49..559d33265 100644 --- a/clientv3/client.go +++ b/clientv3/client.go @@ -65,6 +65,11 @@ type Client struct { // newconnc is closed on successful connect and set to a fresh channel newconnc chan struct{} lastConnErr error + + // Username is a username of authentication + Username string + // Password is a password of authentication + Password string } // New creates a new etcdv3 client from a given configuration. @@ -132,6 +137,20 @@ func (c *Client) Errors() (errs []error) { return errs } +type authTokenCredential struct { + token string +} + +func (cred authTokenCredential) RequireTransportSecurity() bool { + return false +} + +func (cred authTokenCredential) GetRequestMetadata(ctx context.Context, s ...string) (map[string]string, error) { + return map[string]string{ + "token": cred.token, + }, nil +} + // Dial establishes a connection for a given endpoint using the client's config func (c *Client) Dial(endpoint string) (*grpc.ClientConn, error) { opts := []grpc.DialOption{ @@ -160,6 +179,21 @@ func (c *Client) Dial(endpoint string) (*grpc.ClientConn, error) { } opts = append(opts, grpc.WithDialer(f)) + if c.Username != "" && c.Password != "" { + auth, err := newAuthenticator(endpoint, opts) + if err != nil { + return nil, err + } + defer auth.close() + + resp, err := auth.authenticate(c.ctx, c.Username, c.Password) + if err != nil { + return nil, err + } + + opts = append(opts, grpc.WithPerRPCCredentials(authTokenCredential{token: resp.Token})) + } + conn, err := grpc.Dial(endpoint, opts...) if err != nil { return nil, err @@ -183,9 +217,10 @@ func newClient(cfg *Config) (*Client, error) { c := credentials.NewTLS(cfg.TLS) creds = &c } + // use a temporary skeleton client to bootstrap first connection ctx, cancel := context.WithCancel(context.TODO()) - conn, err := cfg.RetryDialer(&Client{cfg: *cfg, creds: creds, ctx: ctx}) + conn, err := cfg.RetryDialer(&Client{cfg: *cfg, creds: creds, ctx: ctx, Username: cfg.Username, Password: cfg.Password}) if err != nil { return nil, err } @@ -199,6 +234,11 @@ func newClient(cfg *Config) (*Client, error) { newconnc: make(chan struct{}), } + if cfg.Username != "" && cfg.Password != "" { + client.Username = cfg.Username + client.Password = cfg.Password + } + go client.connMonitor() client.Cluster = NewCluster(client) diff --git a/clientv3/config.go b/clientv3/config.go index 5b2060cc2..d2f73c390 100644 --- a/clientv3/config.go +++ b/clientv3/config.go @@ -43,6 +43,12 @@ type Config struct { // Logger is the logger used by client library. Logger Logger + + // Username is a username of authentication + Username string + + // Password is a password of authentication + Password string } type yamlConfig struct { diff --git a/clientv3/integration/auth_test.go b/clientv3/integration/auth_test.go deleted file mode 100644 index 23d78daac..000000000 --- a/clientv3/integration/auth_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2016 The etcd Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package integration - -import ( - "testing" - - "github.com/coreos/etcd/clientv3" - "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" - "github.com/coreos/etcd/integration" - "github.com/coreos/etcd/pkg/testutil" - "golang.org/x/net/context" -) - -func TestAuthError(t *testing.T) { - defer testutil.AfterTest(t) - - clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1}) - defer clus.Terminate(t) - - authapi := clientv3.NewAuth(clus.RandClient()) - - _, err := authapi.UserAdd(context.TODO(), "foo", "bar") - if err != nil { - t.Fatal(err) - } - - _, err = authapi.Authenticate(context.TODO(), "foo", "bar111") - if err != rpctypes.ErrAuthFailed { - t.Fatalf("expected %v, got %v", rpctypes.ErrAuthFailed, err) - } -} diff --git a/etcdctl/ctlv3/command/ep_command.go b/etcdctl/ctlv3/command/ep_command.go index 118e49b56..15f68a874 100644 --- a/etcdctl/ctlv3/command/ep_command.go +++ b/etcdctl/ctlv3/command/ep_command.go @@ -70,7 +70,7 @@ func epHealthCommandFunc(cmd *cobra.Command, args []string) { dt := dialTimeoutFromCmd(cmd) cfgs := []*v3.Config{} for _, ep := range endpoints { - cfg, err := newClientCfg([]string{ep}, dt, sec) + cfg, err := newClientCfg([]string{ep}, dt, sec, nil) if err != nil { ExitWithError(ExitBadArgs, err) } diff --git a/etcdctl/ctlv3/command/global.go b/etcdctl/ctlv3/command/global.go index dfe519dbf..861763c76 100644 --- a/etcdctl/ctlv3/command/global.go +++ b/etcdctl/ctlv3/command/global.go @@ -19,8 +19,10 @@ import ( "errors" "io" "io/ioutil" + "strings" "time" + "github.com/bgentry/speakeasy" "github.com/coreos/etcd/clientv3" "github.com/coreos/etcd/pkg/flags" "github.com/coreos/etcd/pkg/transport" @@ -40,6 +42,8 @@ type GlobalFlags struct { OutputFormat string IsHex bool + + User string } type secureCfg struct { @@ -51,6 +55,11 @@ type secureCfg struct { insecureSkipVerify bool } +type authCfg struct { + username string + password string +} + var display printer = &simplePrinter{} func initDisplayFromCmd(cmd *cobra.Command) { @@ -76,14 +85,15 @@ func mustClientFromCmd(cmd *cobra.Command) *clientv3.Client { } dialTimeout := dialTimeoutFromCmd(cmd) sec := secureCfgFromCmd(cmd) + auth := authCfgFromCmd(cmd) initDisplayFromCmd(cmd) - return mustClient(endpoints, dialTimeout, sec) + return mustClient(endpoints, dialTimeout, sec, auth) } -func mustClient(endpoints []string, dialTimeout time.Duration, scfg *secureCfg) *clientv3.Client { - cfg, err := newClientCfg(endpoints, dialTimeout, scfg) +func mustClient(endpoints []string, dialTimeout time.Duration, scfg *secureCfg, acfg *authCfg) *clientv3.Client { + cfg, err := newClientCfg(endpoints, dialTimeout, scfg, acfg) if err != nil { ExitWithError(ExitBadArgs, err) } @@ -96,7 +106,7 @@ func mustClient(endpoints []string, dialTimeout time.Duration, scfg *secureCfg) return client } -func newClientCfg(endpoints []string, dialTimeout time.Duration, scfg *secureCfg) (*clientv3.Config, error) { +func newClientCfg(endpoints []string, dialTimeout time.Duration, scfg *secureCfg, acfg *authCfg) (*clientv3.Config, error) { // set tls if any one tls option set var cfgtls *transport.TLSInfo tlsinfo := transport.TLSInfo{} @@ -138,6 +148,12 @@ func newClientCfg(endpoints []string, dialTimeout time.Duration, scfg *secureCfg if scfg.insecureSkipVerify && cfg.TLS != nil { cfg.TLS.InsecureSkipVerify = true } + + if acfg != nil { + cfg.Username = acfg.username + cfg.Password = acfg.password + } + return cfg, nil } @@ -213,3 +229,30 @@ func keyAndCertFromCmd(cmd *cobra.Command) (cert, key, cacert string) { return cert, key, cacert } + +func authCfgFromCmd(cmd *cobra.Command) *authCfg { + userFlag, err := cmd.Flags().GetString("user") + if err != nil { + ExitWithError(ExitBadArgs, err) + } + + if userFlag == "" { + return nil + } + + var cfg authCfg + + splitted := strings.SplitN(userFlag, ":", 2) + if len(splitted) == 0 { + cfg.username = userFlag + cfg.password, err = speakeasy.Ask("Password: ") + if err != nil { + ExitWithError(ExitError, err) + } + } else { + cfg.username = splitted[0] + cfg.password = splitted[1] + } + + return &cfg +} diff --git a/etcdctl/ctlv3/command/make_mirror_command.go b/etcdctl/ctlv3/command/make_mirror_command.go index 17474e070..44c848385 100644 --- a/etcdctl/ctlv3/command/make_mirror_command.go +++ b/etcdctl/ctlv3/command/make_mirror_command.go @@ -68,7 +68,7 @@ func makeMirrorCommandFunc(cmd *cobra.Command, args []string) { insecureTransport: mminsecureTr, } - dc := mustClient([]string{args[0]}, dialTimeout, sec) + dc := mustClient([]string{args[0]}, dialTimeout, sec, nil) c := mustClientFromCmd(cmd) err := makeMirror(context.TODO(), c, dc) diff --git a/etcdctl/ctlv3/ctl.go b/etcdctl/ctlv3/ctl.go index 7fadae4d4..09da7120d 100644 --- a/etcdctl/ctlv3/ctl.go +++ b/etcdctl/ctlv3/ctl.go @@ -57,6 +57,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.CertFile, "cert", "", "identify secure client using this TLS certificate file") rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.KeyFile, "key", "", "identify secure client using this TLS key file") rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.CAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle") + rootCmd.PersistentFlags().StringVar(&globalFlags.User, "user", "", "username[:password] for authentication (prompt if password is not supplied)") rootCmd.AddCommand( command.NewGetCommand(),