mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge 4f46fb4465b8bae610fe2e9c4c3c5d4d2f64ccf4 into c86c93ca2951338115159dcdd20711603044e1f1
This commit is contained in:
commit
3aee8ffb9c
@ -69,7 +69,10 @@ type Client struct {
|
||||
// Username is a user name for authentication.
|
||||
Username string
|
||||
// Password is a password for authentication.
|
||||
Password string
|
||||
Password string
|
||||
// Token is a JWT used for authentication instead of a password.
|
||||
Token string
|
||||
|
||||
authTokenBundle credentials.PerRPCCredentialsBundle
|
||||
|
||||
callOpts []grpc.CallOption
|
||||
@ -288,6 +291,11 @@ func (c *Client) Dial(ep string) (*grpc.ClientConn, error) {
|
||||
func (c *Client) getToken(ctx context.Context) error {
|
||||
var err error // return last error in a case of fail
|
||||
|
||||
if c.Token != "" {
|
||||
c.authTokenBundle.UpdateAuthToken(c.Token)
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Username == "" || c.Password == "" {
|
||||
return nil
|
||||
}
|
||||
@ -376,6 +384,10 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
creds = credentials.NewTransportCredential(cfg.TLS)
|
||||
}
|
||||
|
||||
if cfg.Token != "" && (cfg.Username != "" || cfg.Password != "") {
|
||||
return nil, errors.New("Username/Password and Token configurations are mutually exclusive")
|
||||
}
|
||||
|
||||
// use a temporary skeleton client to bootstrap first connection
|
||||
baseCtx := context.TODO()
|
||||
if cfg.Context != nil {
|
||||
@ -414,6 +426,12 @@ func newClient(cfg *Config) (*Client, error) {
|
||||
client.Password = cfg.Password
|
||||
client.authTokenBundle = credentials.NewPerRPCCredentialBundle()
|
||||
}
|
||||
|
||||
if cfg.Token != "" {
|
||||
client.Token = cfg.Token
|
||||
client.authTokenBundle = credentials.NewPerRPCCredentialBundle()
|
||||
}
|
||||
|
||||
if cfg.MaxCallSendMsgSize > 0 || cfg.MaxCallRecvMsgSize > 0 {
|
||||
if cfg.MaxCallRecvMsgSize > 0 && cfg.MaxCallSendMsgSize > cfg.MaxCallRecvMsgSize {
|
||||
return nil, fmt.Errorf("gRPC message recv limit (%d bytes) must be greater than send limit (%d bytes)", cfg.MaxCallRecvMsgSize, cfg.MaxCallSendMsgSize)
|
||||
|
@ -337,6 +337,75 @@ func TestAuthTokenBundleNoOverwrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithOnlyJWT(t *testing.T) {
|
||||
// This call in particular changes working directory to the tmp dir of
|
||||
// the test. The `etcd-auth-test:1` can be created in local directory,
|
||||
// not exceeding the longest allowed path on OsX.
|
||||
testutil.BeforeTest(t)
|
||||
|
||||
// Create a mock AuthServer to handle Authenticate RPCs.
|
||||
lis, err := net.Listen("unix", "etcd-auth-test:1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lis.Close()
|
||||
addr := "unix://" + lis.Addr().String()
|
||||
srv := grpc.NewServer()
|
||||
// Having a token removes the need to ever call Authenticate on the
|
||||
// server. If that happens then this will cause a connection failure.
|
||||
etcdserverpb.RegisterAuthServer(srv, mockFailingAuthServer{})
|
||||
go srv.Serve(lis)
|
||||
defer srv.Stop()
|
||||
|
||||
c, err := NewClient(t, Config{
|
||||
DialTimeout: 5 * time.Second,
|
||||
Endpoints: []string{addr},
|
||||
Token: "foo",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
meta, err := c.authTokenBundle.PerRPCCredentials().GetRequestMetadata(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Errorf("Error building request metadata: %s", err)
|
||||
}
|
||||
|
||||
if tok, ok := meta[rpctypes.TokenFieldNameGRPC]; !ok {
|
||||
t.Error("Token was not successfuly set in the auth bundle")
|
||||
} else if tok != "foo" {
|
||||
t.Errorf("Incorrect token set in auth bundle, got '%s', expected 'foo'", tok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOnlyJWTExclusivity(t *testing.T) {
|
||||
testutil.BeforeTest(t)
|
||||
|
||||
// Create a mock AuthServer to handle Authenticate RPCs.
|
||||
lis, err := net.Listen("unix", "etcd-auth-test:1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lis.Close()
|
||||
addr := "unix://" + lis.Addr().String()
|
||||
srv := grpc.NewServer()
|
||||
// Having a token removes the need to ever call Authenticate on the
|
||||
// server. If that happens then this will cause a connection failure.
|
||||
etcdserverpb.RegisterAuthServer(srv, mockFailingAuthServer{})
|
||||
go srv.Serve(lis)
|
||||
defer srv.Stop()
|
||||
|
||||
_, err = NewClient(t, Config{
|
||||
DialTimeout: 5 * time.Second,
|
||||
Endpoints: []string{addr},
|
||||
Token: "foo",
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
})
|
||||
require.Error(t, err, "Username/Password and Token configurations are mutually exclusive")
|
||||
}
|
||||
|
||||
func TestSyncFiltersMembers(t *testing.T) {
|
||||
c, _ := NewClient(t, Config{Endpoints: []string{"http://254.0.0.1:12345"}})
|
||||
defer c.Close()
|
||||
@ -498,6 +567,14 @@ func (mm mockMaintenance) Downgrade(ctx context.Context, action DowngradeAction,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockFailingAuthServer struct {
|
||||
*etcdserverpb.UnimplementedAuthServer
|
||||
}
|
||||
|
||||
func (mockFailingAuthServer) Authenticate(context.Context, *etcdserverpb.AuthenticateRequest) (*etcdserverpb.AuthenticateResponse, error) {
|
||||
return nil, errors.New("this auth server always fails")
|
||||
}
|
||||
|
||||
type mockAuthServer struct {
|
||||
*etcdserverpb.UnimplementedAuthServer
|
||||
}
|
||||
|
@ -66,6 +66,9 @@ type Config struct {
|
||||
// Password is a password for authentication.
|
||||
Password string `json:"password"`
|
||||
|
||||
// Token is a JWT used for authentication instead of a password.
|
||||
Token string `json:"token"`
|
||||
|
||||
// RejectOldCluster when set will refuse to create a client against an outdated cluster.
|
||||
RejectOldCluster bool `json:"reject-old-cluster"`
|
||||
|
||||
@ -128,10 +131,11 @@ type SecureConfig struct {
|
||||
type AuthConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (cfg AuthConfig) Empty() bool {
|
||||
return cfg.Username == "" && cfg.Password == ""
|
||||
return cfg.Username == "" && cfg.Password == "" && cfg.Token == ""
|
||||
}
|
||||
|
||||
// NewClientConfig creates a Config based on the provided ConfigSpec.
|
||||
@ -152,6 +156,7 @@ func NewClientConfig(confSpec *ConfigSpec, lg *zap.Logger) (*Config, error) {
|
||||
if confSpec.Auth != nil {
|
||||
cfg.Username = confSpec.Auth.Username
|
||||
cfg.Password = confSpec.Auth.Password
|
||||
cfg.Token = confSpec.Auth.Token
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
@ -68,6 +68,25 @@ func TestNewClientConfig(t *testing.T) {
|
||||
Password: "changeme",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JWT specified",
|
||||
spec: ConfigSpec{
|
||||
Endpoints: []string{"http://192.168.0.12:2379"},
|
||||
DialTimeout: 1 * time.Second,
|
||||
KeepAliveTime: 4 * time.Second,
|
||||
KeepAliveTimeout: 6 * time.Second,
|
||||
Auth: &AuthConfig{
|
||||
Token: "test",
|
||||
},
|
||||
},
|
||||
expectedConf: Config{
|
||||
Endpoints: []string{"http://192.168.0.12:2379"},
|
||||
DialTimeout: 1 * time.Second,
|
||||
DialKeepAliveTime: 4 * time.Second,
|
||||
DialKeepAliveTimeout: 6 * time.Second,
|
||||
Token: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default secure transport",
|
||||
spec: ConfigSpec{
|
||||
|
@ -56,6 +56,7 @@ type GlobalFlags struct {
|
||||
|
||||
User string
|
||||
Password string
|
||||
Token string
|
||||
|
||||
Debug bool
|
||||
}
|
||||
@ -270,13 +271,22 @@ func authCfgFromCmd(cmd *cobra.Command) *clientv3.AuthConfig {
|
||||
if err != nil {
|
||||
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
||||
}
|
||||
tokenFlag, err := cmd.Flags().GetString("auth-jwt-token")
|
||||
if err != nil {
|
||||
cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
|
||||
}
|
||||
|
||||
if userFlag == "" {
|
||||
if userFlag == "" && tokenFlag == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cfg clientv3.AuthConfig
|
||||
|
||||
if tokenFlag != "" {
|
||||
cfg.Token = tokenFlag
|
||||
return &cfg
|
||||
}
|
||||
|
||||
if passwordFlag == "" {
|
||||
splitted := strings.SplitN(userFlag, ":", 2)
|
||||
if len(splitted) < 2 {
|
||||
|
@ -70,6 +70,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.TrustedCAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle")
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.Token, "auth-jwt-token", "", "JWT token used for authentication (if this option is used, --user and --password should not be set)")
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.User, "user", "", "username[:password] for authentication (prompt if password is not supplied)")
|
||||
rootCmd.PersistentFlags().StringVar(&globalFlags.Password, "password", "", "password for authentication (if this option is used, --user option shouldn't include password)")
|
||||
rootCmd.PersistentFlags().StringVarP(&globalFlags.TLS.ServerName, "discovery-srv", "d", "", "domain name to query for SRV records describing cluster endpoints")
|
||||
|
@ -29,8 +29,11 @@ import (
|
||||
)
|
||||
|
||||
var tokenTTL = time.Second
|
||||
var defaultKeyPath = mustAbsPath("../fixtures/server.key.insecure")
|
||||
var defaultAuthToken = fmt.Sprintf("jwt,pub-key=%s,priv-key=%s,sign-method=RS256,ttl=%s",
|
||||
mustAbsPath("../fixtures/server.crt"), mustAbsPath("../fixtures/server.key.insecure"), tokenTTL)
|
||||
mustAbsPath("../fixtures/server.crt"), defaultKeyPath, tokenTTL)
|
||||
var verifyJWTOnlyAuth = fmt.Sprintf("jwt,pub-key=%s,sign-method=RS256,ttl=%s",
|
||||
mustAbsPath("../fixtures/server.crt"), tokenTTL)
|
||||
|
||||
const (
|
||||
PermissionDenied = "etcdserver: permission denied"
|
||||
@ -758,6 +761,25 @@ func TestAuthJWTExpire(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthJWTOnly(t *testing.T) {
|
||||
testRunner.BeforeTest(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: verifyJWTOnlyAuth}))
|
||||
defer clus.Close()
|
||||
cc := testutils.MustClient(clus.Client())
|
||||
testutils.ExecuteUntil(ctx, t, func() {
|
||||
authRev, err := setupAuthAndGetRevision(cc, []authRole{testRole}, []authUser{rootUser, testUser})
|
||||
require.NoErrorf(t, err, "failed to enable auth")
|
||||
|
||||
token, err := createSignedJWT(defaultKeyPath, "RS256", testUserName, authRev)
|
||||
require.NoErrorf(t, err, "failed to create test user JWT")
|
||||
|
||||
testUserAuthClient := testutils.MustClient(clus.Client(WithAuthToken(token)))
|
||||
require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{}))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAuthRevisionConsistency ensures auth revision is the same after member restarts
|
||||
func TestAuthRevisionConsistency(t *testing.T) {
|
||||
testRunner.BeforeTest(t)
|
||||
|
@ -17,8 +17,11 @@ package common
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.etcd.io/etcd/api/v3/authpb"
|
||||
@ -93,6 +96,29 @@ func createUsers(c interfaces.Client, users []authUser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSignedJWT(keyPath, alg, username string, authRevision uint64) (string, error) {
|
||||
signMethod := jwt.GetSigningMethod(alg)
|
||||
|
||||
keyBytes, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tk := jwt.NewWithClaims(signMethod,
|
||||
jwt.MapClaims{
|
||||
"username": username,
|
||||
"revision": authRevision,
|
||||
"exp": time.Now().Add(time.Minute).Unix(),
|
||||
})
|
||||
|
||||
return tk.SignedString(key)
|
||||
}
|
||||
|
||||
func setupAuth(c interfaces.Client, roles []authRole, users []authUser) error {
|
||||
// create roles
|
||||
if err := createRoles(c, roles); err != nil {
|
||||
@ -107,6 +133,29 @@ func setupAuth(c interfaces.Client, roles []authRole, users []authUser) error {
|
||||
return c.AuthEnable(context.TODO())
|
||||
}
|
||||
|
||||
func setupAuthAndGetRevision(c interfaces.Client, roles []authRole, users []authUser) (uint64, error) {
|
||||
// create roles
|
||||
if err := createRoles(c, roles); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := createUsers(c, users); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// This needs to happen before enabling auth for the TestAuthJWTOnly
|
||||
// test case because once auth is enabled we can no longer mint a valid
|
||||
// auth token without the revision, which we won't be able to obtain
|
||||
// without a valid auth token.
|
||||
authrev, err := c.AuthStatus(context.TODO())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// enable auth
|
||||
return authrev.AuthRevision, c.AuthEnable(context.TODO())
|
||||
}
|
||||
|
||||
func requireRolePermissionEqual(t *testing.T, expectRole authRole, actual []*authpb.Permission) {
|
||||
require.Equal(t, 1, len(actual))
|
||||
require.Equal(t, expectRole.permission, clientv3.PermissionType(actual[0].PermType))
|
||||
|
@ -78,6 +78,10 @@ func WithAuth(userName, password string) config.ClientOption {
|
||||
return e2e.WithAuth(userName, password)
|
||||
}
|
||||
|
||||
func WithAuthToken(token string) config.ClientOption {
|
||||
return e2e.WithAuthToken(token)
|
||||
}
|
||||
|
||||
func WithEndpoints(endpoints []string) config.ClientOption {
|
||||
return e2e.WithEndpoints(endpoints)
|
||||
}
|
||||
|
@ -56,6 +56,10 @@ func WithAuth(userName, password string) config.ClientOption {
|
||||
return integration.WithAuth(userName, password)
|
||||
}
|
||||
|
||||
func WithAuthToken(token string) config.ClientOption {
|
||||
return integration.WithAuthToken(token)
|
||||
}
|
||||
|
||||
func WithEndpoints(endpoints []string) config.ClientOption {
|
||||
return integration.WithEndpoints(endpoints)
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ func WithAuth(userName, password string) config.ClientOption {
|
||||
return func(any) {}
|
||||
}
|
||||
|
||||
func WithAuthToken(token string) config.ClientOption {
|
||||
return func(any) {}
|
||||
}
|
||||
|
||||
func WithEndpoints(endpoints []string) config.ClientOption {
|
||||
return func(any) {}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ func NewEtcdctl(cfg ClientConfig, endpoints []string, opts ...config.ClientOptio
|
||||
DialOptions: []grpc.DialOption{grpc.WithBlock()},
|
||||
Username: ctl.authConfig.Username,
|
||||
Password: ctl.authConfig.Password,
|
||||
Token: ctl.authConfig.Token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -73,6 +74,13 @@ func WithAuth(userName, password string) config.ClientOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAuthToken(token string) config.ClientOption {
|
||||
return func(c any) {
|
||||
ctl := c.(*EtcdctlV3)
|
||||
ctl.authConfig.Token = token
|
||||
}
|
||||
}
|
||||
|
||||
func WithEndpoints(endpoints []string) config.ClientOption {
|
||||
return func(c any) {
|
||||
ctl := c.(*EtcdctlV3)
|
||||
@ -344,7 +352,9 @@ func (ctl *EtcdctlV3) flags() map[string]string {
|
||||
}
|
||||
}
|
||||
fmap["endpoints"] = strings.Join(ctl.endpoints, ",")
|
||||
if !ctl.authConfig.Empty() {
|
||||
if ctl.authConfig.Token != "" {
|
||||
fmap["auth-jwt-token"] = ctl.authConfig.Token
|
||||
} else if !ctl.authConfig.Empty() {
|
||||
fmap["user"] = ctl.authConfig.Username + ":" + ctl.authConfig.Password
|
||||
}
|
||||
return fmap
|
||||
|
@ -1478,6 +1478,13 @@ func WithAuth(userName, password string) framecfg.ClientOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAuthToken(token string) framecfg.ClientOption {
|
||||
return func(c any) {
|
||||
cfg := c.(*clientv3.Config)
|
||||
cfg.Token = token
|
||||
}
|
||||
}
|
||||
|
||||
func WithEndpoints(endpoints []string) framecfg.ClientOption {
|
||||
return func(c any) {
|
||||
cfg := c.(*clientv3.Config)
|
||||
|
@ -18,6 +18,7 @@ replace (
|
||||
require (
|
||||
github.com/anishathalye/porcupine v0.1.4
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
@ -65,7 +66,6 @@ require (
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
|
Loading…
x
Reference in New Issue
Block a user