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..e8d246bb3e8c 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-token"] = ctl.authToken + } return fmap }