From 3c4aaa6ff8577f9646072ce5e5f3403382a2d13e Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Tue, 23 Jul 2024 10:53:23 -0700 Subject: [PATCH] test: validate direct JWT passing and acceptance Signed-off-by: Mike Crute --- tests/common/auth_test.go | 25 ++++++++++++++++- tests/common/auth_util.go | 49 ++++++++++++++++++++++++++++++++++ tests/framework/e2e/etcdctl.go | 15 +++++++++++ tests/go.mod | 2 +- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go index 0c34b800a3d7..e298fe0569ee 100644 --- a/tests/common/auth_test.go +++ b/tests/common/auth_test.go @@ -25,12 +25,16 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/tests/v3/framework/config" + "go.etcd.io/etcd/tests/v3/framework/e2e" "go.etcd.io/etcd/tests/v3/framework/testutils" ) 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 +762,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, testUserName, authRev) + require.NoErrorf(t, err, "failed to create test user JWT") + + testUserAuthClient := testutils.MustClient(clus.Client(e2e.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) diff --git a/tests/common/auth_util.go b/tests/common/auth_util.go index 313bfb46d40e..d5da3caabfec 100644 --- a/tests/common/auth_util.go +++ b/tests/common/auth_util.go @@ -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, username string, authRevision uint64) (string, error) { + signMethod := jwt.GetSigningMethod("RS256") + + 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)) diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index 81d57c088d5c..384130694c4b 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -36,6 +36,7 @@ type EtcdctlV3 struct { cfg ClientConfig endpoints []string authConfig clientv3.AuthConfig + authToken string } func NewEtcdctl(cfg ClientConfig, endpoints []string, opts ...config.ClientOption) (*EtcdctlV3, error) { @@ -73,6 +74,17 @@ func WithAuth(userName, password string) config.ClientOption { } } +func WithAuthToken(token string) config.ClientOption { + return func(c any) { + switch c := c.(type) { + case *EtcdctlV3: + c.authToken = token + case *clientv3.Config: + c.Token = token + } + } +} + func WithEndpoints(endpoints []string) config.ClientOption { return func(c any) { ctl := c.(*EtcdctlV3) @@ -347,6 +359,9 @@ func (ctl *EtcdctlV3) flags() map[string]string { if !ctl.authConfig.Empty() { fmap["user"] = ctl.authConfig.Username + ":" + ctl.authConfig.Password } + if ctl.authToken != "" { + fmap["auth-jwt-token"] = ctl.authToken + } return fmap } diff --git a/tests/go.mod b/tests/go.mod index eeb1783b6fff..8d3505ccb25a 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -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.2 // indirect github.com/google/uuid v1.6.0 // indirect