client/v3: do not overwrite authTokenBundle on dial

`Client.dial` can be called multiple times. For example, from regular
`NewClient` and [from the `Maintenance`
wrapper](6d451ab61d/client/v3/maintenance.go (L81-L84)).

This ends up creating two (if `Client.dial` is called twice) independent
`credentials.Bundle` instances:
- the first one is wired into `PerRPCCredentials` callback in gRPC
  client.
- the second one is in `Client.authTokenBundle` and receives the latest
  auth token updates.

When using username/password authentication and the server is configured
to issue JWTs, token rotation logic breaks because of the above
`credentials.Bundle` confusion.
dependabot/go_modules/go.uber.org/atomic-1.10.0
Andrew Lytvynov 2021-05-17 17:06:38 -07:00
parent 6d451ab61d
commit ab8e5a4f8e
2 changed files with 54 additions and 3 deletions

View File

@ -283,8 +283,7 @@ func (c *Client) dial(creds grpccredentials.TransportCredentials, dopts ...grpc.
if err != nil {
return nil, fmt.Errorf("failed to configure dialer: %v", err)
}
if c.Username != "" && c.Password != "" {
c.authTokenBundle = credentials.NewBundle(credentials.Config{})
if c.authTokenBundle != nil {
opts = append(opts, grpc.WithPerRPCCredentials(c.authTokenBundle.PerRPCCredentials()))
}
@ -365,6 +364,7 @@ func newClient(cfg *Config) (*Client, error) {
if cfg.Username != "" && cfg.Password != "" {
client.Username = cfg.Username
client.Password = cfg.Password
client.authTokenBundle = credentials.NewBundle(credentials.Config{})
}
if cfg.MaxCallSendMsgSize > 0 || cfg.MaxCallRecvMsgSize > 0 {
if cfg.MaxCallRecvMsgSize > 0 && cfg.MaxCallSendMsgSize > cfg.MaxCallRecvMsgSize {

View File

@ -17,13 +17,15 @@ package clientv3
import (
"context"
"fmt"
"go.uber.org/zap"
"net"
"path/filepath"
"testing"
"time"
"go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
"go.etcd.io/etcd/client/pkg/v3/testutil"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"google.golang.org/grpc"
@ -198,3 +200,52 @@ func TestZapWithLogger(t *testing.T) {
t.Errorf("WithZapLogger should modify *zap.Logger")
}
}
func TestAuthTokenBundleNoOverwrite(t *testing.T) {
// Create a mock AuthServer to handle Authenticate RPCs.
lis, err := net.Listen("unix", filepath.Join(t.TempDir(), "etcd-auth-test:0"))
if err != nil {
t.Fatal(err)
}
defer lis.Close()
addr := "unix://" + lis.Addr().String()
srv := grpc.NewServer()
etcdserverpb.RegisterAuthServer(srv, mockAuthServer{})
go srv.Serve(lis)
defer srv.Stop()
// Create a client, which should call Authenticate on the mock server to
// exchange username/password for an auth token.
c, err := NewClient(t, Config{
DialTimeout: 5 * time.Second,
Endpoints: []string{addr},
Username: "foo",
Password: "bar",
})
if err != nil {
t.Fatal(err)
}
defer c.Close()
oldTokenBundle := c.authTokenBundle
// Call the public Dial again, which should preserve the original
// authTokenBundle.
gc, err := c.Dial(addr)
if err != nil {
t.Fatal(err)
}
defer gc.Close()
newTokenBundle := c.authTokenBundle
if oldTokenBundle != newTokenBundle {
t.Error("Client.authTokenBundle has been overwritten during Client.Dial")
}
}
type mockAuthServer struct {
*etcdserverpb.UnimplementedAuthServer
}
func (mockAuthServer) Authenticate(context.Context, *etcdserverpb.AuthenticateRequest) (*etcdserverpb.AuthenticateResponse, error) {
return &etcdserverpb.AuthenticateResponse{Token: "mock-token"}, nil
}