mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
*: attach auth token as a gRPC credential
This commit adds a functionality of attaching an auth token to gRPC connection as a per RPC credential. For doing this, this commit lets clientv3.Client.Dial() create a dedicated gRPC connection for doing authentication. With the dedicated connection, the client calls Authenticate() RPC and obtain its token. The token is attached to the main gRPC connection with grpc.WithPerRPCCredentials(). This commit also adds a new option --username to etcdctl (v3). With this option, etcdctl attaches its auth token to the main gRPC connection (currently it is not used at all).
This commit is contained in:
parent
327b01169c
commit
6259318521
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user