From 3201b9d9f1b89f881c292316460ee801c31ba843 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 --- tests/common/auth_test.go | 25 ++++++++++++++++- tests/common/auth_util.go | 49 ++++++++++++++++++++++++++++++++++ tests/framework/e2e/etcdctl.go | 11 ++++++++ tests/go.mod | 1 + tests/go.sum | 2 ++ 5 files changed, 87 insertions(+), 1 deletion(-) 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..9515c4747990 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" "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..d8f7c6368b81 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,13 @@ func WithAuth(userName, password string) config.ClientOption { } } +func WithAuthToken(token string) config.ClientOption { + return func(c any) { + ctl := c.(*EtcdctlV3) + ctl.authToken = token + } +} + func WithEndpoints(endpoints []string) config.ClientOption { return func(c any) { ctl := c.(*EtcdctlV3) @@ -347,6 +355,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-token"] = ctl.authToken + } return fmap } diff --git a/tests/go.mod b/tests/go.mod index eeb1783b6fff..c6fa235d6b90 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 v3.2.2+incompatible 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 diff --git a/tests/go.sum b/tests/go.sum index fe1cb5bc2263..86e59d36d1c4 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -48,6 +48,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=