diff --git a/server/auth/store.go b/server/auth/store.go index a27f9fdc2067..370629c29a28 100644 --- a/server/auth/store.go +++ b/server/auth/store.go @@ -832,6 +832,8 @@ func (as *authStore) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) ( "granted/updated a permission to a user", zap.String("user-name", r.Name), zap.String("permission-name", authpb.Permission_Type_name[int32(r.Perm.PermType)]), + zap.ByteString("key", r.Perm.Key), + zap.ByteString("range-end", r.Perm.RangeEnd), ) return &pb.AuthRoleGrantPermissionResponse{}, nil } diff --git a/tests/common/alarm_test.go b/tests/common/alarm_test.go index 9f0ace141716..4f48dbfffe6b 100644 --- a/tests/common/alarm_test.go +++ b/tests/common/alarm_test.go @@ -32,17 +32,18 @@ func TestAlarm(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1, QuotaBackendBytes: int64(13 * os.Getpagesize())}) defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { // test small put still works smallbuf := strings.Repeat("a", 64) - if err := clus.Client().Put("1st_test", smallbuf, config.PutOptions{}); err != nil { + if err := cc.Put("1st_test", smallbuf, config.PutOptions{}); err != nil { t.Fatalf("alarmTest: put kv error (%v)", err) } // write some chunks to fill up the database buf := strings.Repeat("b", os.Getpagesize()) for { - if err := clus.Client().Put("2nd_test", buf, config.PutOptions{}); err != nil { + if err := cc.Put("2nd_test", buf, config.PutOptions{}); err != nil { if !strings.Contains(err.Error(), "etcdserver: mvcc: database space exceeded") { t.Fatal(err) } @@ -51,20 +52,20 @@ func TestAlarm(t *testing.T) { } // quota alarm should now be on - alarmResp, err := clus.Client().AlarmList() + alarmResp, err := cc.AlarmList() if err != nil { t.Fatalf("alarmTest: Alarm error (%v)", err) } // check that Put is rejected when alarm is on - if err := clus.Client().Put("3rd_test", smallbuf, config.PutOptions{}); err != nil { + if err := cc.Put("3rd_test", smallbuf, config.PutOptions{}); err != nil { if !strings.Contains(err.Error(), "etcdserver: mvcc: database space exceeded") { t.Fatal(err) } } // get latest revision to compact - sresp, err := clus.Client().Status() + sresp, err := cc.Status() if err != nil { t.Fatalf("get endpoint status error: %v", err) } @@ -77,12 +78,12 @@ func TestAlarm(t *testing.T) { } // make some space - _, err = clus.Client().Compact(rvs, config.CompactOption{Physical: true, Timeout: 10 * time.Second}) + _, err = cc.Compact(rvs, config.CompactOption{Physical: true, Timeout: 10 * time.Second}) if err != nil { t.Fatalf("alarmTest: Compact error (%v)", err) } - if err = clus.Client().Defragment(config.DefragOption{Timeout: 10 * time.Second}); err != nil { + if err = cc.Defragment(config.DefragOption{Timeout: 10 * time.Second}); err != nil { t.Fatalf("alarmTest: defrag error (%v)", err) } @@ -92,14 +93,14 @@ func TestAlarm(t *testing.T) { MemberID: alarm.MemberID, Alarm: alarm.Alarm, } - _, err = clus.Client().AlarmDisarm(alarmMember) + _, err = cc.AlarmDisarm(alarmMember) if err != nil { t.Fatalf("alarmTest: Alarm error (%v)", err) } } // put one more key below quota - if err := clus.Client().Put("4th_test", smallbuf, config.PutOptions{}); err != nil { + if err := cc.Put("4th_test", smallbuf, config.PutOptions{}); err != nil { t.Fatal(err) } }) diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go new file mode 100644 index 000000000000..a9a9243d255a --- /dev/null +++ b/tests/common/auth_test.go @@ -0,0 +1,1129 @@ +package common + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + "go.etcd.io/etcd/client/pkg/v3/testutil" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/tests/v3/framework" + "go.etcd.io/etcd/tests/v3/framework/config" + "go.etcd.io/etcd/tests/v3/framework/testutils" +) + +var defaultAuthToken = fmt.Sprintf("jwt,pub-key=%s,priv-key=%s,sign-method=RS256,ttl=1s", + mustAbsPath("../fixtures/server.crt"), mustAbsPath("../fixtures/server.key.insecure")) + +func TestAuthEnable(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, clus.MustClient(config.ClientOption{})) + }) +} + +func TestAuthDisable(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + err := cc.Put("hoo", "a", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + authEnable(t, cc) + + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + // test-user doesn't have the permission, it must fail + err = testUserAuthClient.Put("hoo", "bar", config.PutOptions{}) + if err == nil { + t.Fatalf("want error but got nil") + } + _, err = rootAuthClient.AuthDisable() + if err != nil { + t.Fatalf("failed to auth disable %v", err) + } + // now ErrAuthNotEnabled of Authenticate() is simply ignored + err = testUserAuthClient.Put("hoo", "bar", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + // now the key can be accessed + err = cc.Put("hoo", "bar", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := cc.Get("hoo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "hoo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'hoo', 'bar' but got %+v", resp.Kvs) + } + }) +} + +func TestAuthStatus(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + resp, err := cc.AuthStatus() + if err != nil { + t.Fatal(err) + } + if resp.Enabled { + t.Fatal("want not enabled but enabled") + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + resp, err = rootAuthClient.AuthStatus() + if err != nil { + t.Fatal(err) + } + if !resp.Enabled { + t.Fatalf("want enabled but got not enabled") + } + }) +} + +func TestAuthRoleUpdate(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // try put to not granted key + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + putFailPerm(t, testUserAuthClient, "hoo", "bar") + // grant a new key + if _, err := rootAuthClient.RoleGrantPermission("test-role", "hoo", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // try a newly granted key + if err := testUserAuthClient.Put("hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get("hoo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "hoo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'hoo' 'bar' but got %+v", resp.Kvs) + } + // revoke the newly granted key + if _, err = rootAuthClient.RoleRevokePermission("test-role", "hoo", ""); err != nil { + t.Fatal(err) + } + // try put to the revoked key + putFailPerm(t, testUserAuthClient, "hoo", "bar") + // confirm a key still granted can be accessed + resp, err = testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'foo' 'bar' but got %+v", resp.Kvs) + } + }) +} + +func TestAuthUserDeleteDuringOps(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + + // create a key + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + if err := testUserAuthClient.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'foo' 'bar' but got %+v", resp.Kvs) + } + // delete the user + if _, err = rootAuthClient.UserDelete("test-user"); err != nil { + t.Fatal(err) + } + // check the user is deleted + err = testUserAuthClient.Put("foo", "baz", config.PutOptions{}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrAuthFailed.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrAuthFailed.Error(), err) + } + }) +} + +func TestAuthRoleRevokeDuringOps(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // create a key + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + if err := testUserAuthClient.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'foo' 'bar' but got %+v", resp.Kvs) + } + // create a new role + if _, err = rootAuthClient.RoleAdd("test-role2"); err != nil { + t.Fatal(err) + } + // grant a new key to the new role + if _, err = rootAuthClient.RoleGrantPermission("test-role2", "hoo", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // grant the new role to the user + if _, err = rootAuthClient.UserGrantRole("test-user", "test-role2"); err != nil { + t.Fatal(err) + } + + // try a newly granted key + if err := testUserAuthClient.Put("hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get("hoo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "hoo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'hoo' 'bar' but got %+v", resp.Kvs) + } + // revoke a role from the user + if _, err = rootAuthClient.UserRevokeRole("test-user", "test-role"); err != nil { + t.Fatal(err) + } + // check the role is revoked and permission is lost from the user + putFailPerm(t, testUserAuthClient, "foo", "baz") + + // try a key that can be accessed from the remaining role + if err := testUserAuthClient.Put("hoo", "bar2", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get("hoo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "hoo" || string(resp.Kvs[0].Value) != "bar2" { + t.Fatalf("want key value pair 'hoo' 'bar2' but got %+v", resp.Kvs) + } + }) +} + +func TestAuthWriteKey(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put("foo", "a", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + + // confirm root role can access to all keys + if err := rootAuthClient.Put("foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + resp, err := rootAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'foo' 'bar' but got %+v", resp.Kvs) + } + // try invalid user + _, err = clus.Client(config.ClientOption{UserName: "a", Password: "b"}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrAuthFailed.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrAuthFailed.Error(), err) + } + // confirm put failed + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + resp, err = testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar" { + t.Fatalf("want key value pair 'foo' 'bar' but got %+v", resp.Kvs) + } + // try good user + if err = testUserAuthClient.Put("foo", "bar2", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar2" { + t.Fatalf("want key value pair 'foo' 'bar2' but got %+v", resp.Kvs) + } + // try bad password + _, err = clus.Client(config.ClientOption{UserName: "test-user", Password: "badpass"}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrAuthFailed.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrAuthFailed.Error(), err) + } + // confirm put failed + resp, err = testUserAuthClient.Get("foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(resp.Kvs) != 1 || string(resp.Kvs[0].Key) != "foo" || string(resp.Kvs[0].Value) != "bar2" { + t.Fatalf("want key value pair 'foo' 'bar2' but got %+v", resp.Kvs) + } + }) +} + +// TestAuthEmptyUserGet ensures that a get with an empty user will return an empty user error. +func TestAuthEmptyUserGet(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + _, err := cc.Get("abc", config.GetOptions{}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrUserEmpty.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrUserEmpty.Error(), err) + } + }) +} + +// TestAuthEmptyUserPut ensures that a put with an empty user will return an empty user error, +// and the consistent_index should be moved forward even the apply-->Put fails. +func TestAuthEmptyUserPut(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1, SnapshotCount: 3}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + // The SnapshotCount is 3, so there must be at least 3 new snapshot files being created. + // The VERIFY logic will check whether the consistent_index >= last snapshot index on + // cluster terminating. + for i := 0; i < 10; i++ { + err := cc.Put("foo", "bar", config.PutOptions{}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrUserEmpty.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrUserEmpty.Error(), err) + } + } + }) +} + +// TestAuthTokenWithDisable tests that auth won't crash if +// given a valid token when authentication is disabled +func TestAuthTokenWithDisable(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + rctx, cancel := context.WithCancel(context.TODO()) + donec := make(chan struct{}) + go func() { + defer close(donec) + for rctx.Err() == nil { + rootAuthClient.Put("abc", "def", config.PutOptions{}) + } + }() + time.Sleep(10 * time.Millisecond) + if _, err := rootAuthClient.AuthDisable(); err != nil { + t.Fatal(err) + } + time.Sleep(10 * time.Millisecond) + cancel() + <-donec + }) +} + +func TestAuthTxn(t *testing.T) { + tcs := []struct { + name string + cfg config.ClusterConfig + }{ + { + "NoJWT", + config.ClusterConfig{ClusterSize: 1}, + }, + { + "JWT", + config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}, + }, + } + + reqs := []txnReq{ + { + compare: []string{`version("c2") = "1"`}, + ifSucess: []string{"get s2"}, + ifFail: []string{"get f2"}, + results: []string{"SUCCESS", "s2", "v"}, + }, + // a key of compare case isn't granted + { + compare: []string{`version("c1") = "1"`}, + ifSucess: []string{"get s2"}, + ifFail: []string{"get f2"}, + results: []string{"etcdserver: permission denied"}, + }, + // a key of success case isn't granted + { + compare: []string{`version("c2") = "1"`}, + ifSucess: []string{"get s1"}, + ifFail: []string{"get f2"}, + results: []string{"etcdserver: permission denied"}, + }, + // a key of failure case isn't granted + { + compare: []string{`version("c2") = "1"`}, + ifSucess: []string{"get s2"}, + ifFail: []string{"get f1"}, + results: []string{"etcdserver: permission denied"}, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, tc.cfg) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + // keys with 1 suffix aren't granted to test-user + // keys with 2 suffix are granted to test-user + + keys := []string{"c1", "s1", "f1"} + grantedKeys := []string{"c2", "s2", "f2"} + for _, key := range keys { + if err := cc.Put(key, "v", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + for _, key := range grantedKeys { + if err := cc.Put(key, "v", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // grant keys to test-user + for _, key := range grantedKeys { + if _, err := rootAuthClient.RoleGrantPermission("test-role", key, "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + } + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + for _, req := range reqs { + resp, err := testUserAuthClient.Txn(req.compare, req.ifSucess, req.ifFail, config.TxnOptions{ + Interactive: true, + }) + if strings.Contains(req.results[0], "denied") { + assert.Contains(t, err.Error(), req.results[0]) + } else { + assert.NoError(t, err) + assert.Equal(t, req.results, getRespValues(resp)) + } + } + }) + }) + } +} + +func TestAuthPrefixPerm(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + prefix := "/prefix/" // directory like prefix + // grant keys to test-user + if _, err := rootAuthClient.RoleGrantPermission("test-role", prefix, clientv3.GetPrefixRangeEnd(prefix), clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // try a prefix granted permission + testUserClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + for i := 0; i < 10; i++ { + key := fmt.Sprintf("%s%d", prefix, i) + if err := testUserClient.Put(key, "val", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + putFailPerm(t, testUserClient, clientv3.GetPrefixRangeEnd(prefix), "baz") + // grant the prefix2 keys to test-user + prefix2 := "/prefix2/" + if _, err := rootAuthClient.RoleGrantPermission("test-role", prefix2, clientv3.GetPrefixRangeEnd(prefix2), clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + for i := 0; i < 10; i++ { + key := fmt.Sprintf("%s%d", prefix2, i) + if err := testUserClient.Put(key, "val", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + }) +} + +func TestAuthRevokeWithDelete(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // create a new role + if _, err := rootAuthClient.RoleAdd("test-role2"); err != nil { + t.Fatal(err) + } + // grant the new role to the user + if _, err := rootAuthClient.UserGrantRole("test-user", "test-role2"); err != nil { + t.Fatal(err) + } + // check the result + resp, err := rootAuthClient.UserGet("test-user") + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, resp.Roles, []string{"test-role", "test-role2"}) + // delete the role, test-role2 must be revoked from test-user + if _, err := rootAuthClient.RoleDelete("test-role2"); err != nil { + t.Fatal(err) + } + // check the result + resp, err = rootAuthClient.UserGet("test-user") + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, resp.Roles, []string{"test-role"}) + }) +} + +func TestAuthInvalidMgmt(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + _, err := rootAuthClient.RoleDelete("root") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrInvalidAuthMgmt.Error()) { + t.Fatalf("want %v error but got %v error", rpctypes.ErrInvalidAuthMgmt, err) + } + _, err = rootAuthClient.UserRevokeRole("root", "root") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrInvalidAuthMgmt.Error()) { + t.Fatalf("want %v error but got %v error", rpctypes.ErrInvalidAuthMgmt, err) + } + }) +} + +func TestAuthLeaseTestKeepAlive(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, clus.MustClient(config.ClientOption{})) + rootUserAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootUserAuthClient) + resp, err := rootUserAuthClient.Grant(10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootUserAuthClient.Put("key", "value", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + if _, err = rootUserAuthClient.LeaseKeepAliveOnce(leaseID); err != nil { + t.Fatal(err) + } + gresp, err := rootUserAuthClient.Get("key", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(gresp.Kvs) != 1 || string(gresp.Kvs[0].Key) != "key" || string(gresp.Kvs[0].Value) != "value" { + t.Fatalf("want kv pair ('key', 'value') but got %v", gresp.Kvs) + } + }) +} + +func TestAuthLeaseTestTimeToLiveExpired(t *testing.T) { + tcs := []struct { + name string + JWTEnabled bool + }{ + { + name: "JWTEnabled", + JWTEnabled: true, + }, + { + name: "JWTDisabled", + JWTEnabled: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, clus.MustClient(config.ClientOption{})) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + resp, err := rootAuthClient.Grant(2) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootAuthClient.Put("key", "val", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + // eliminate false positive + time.Sleep(3 * time.Second) + tresp, err := rootAuthClient.TimeToLive(leaseID, config.LeaseOption{}) + if err != nil { + t.Fatal(err) + } + if tresp.TTL != -1 { + t.Fatalf("want leaseID %v expired but not", leaseID) + } + gresp, err := rootAuthClient.Get("key", config.GetOptions{}) + if err != nil || len(gresp.Kvs) != 0 { + t.Fatalf("want nil err and no kvs but got (%v) error and %d kvs", err, len(gresp.Kvs)) + } + }) + }) + } +} + +func TestAuthLeaseGrantLeases(t *testing.T) { + tcs := []struct { + name string + cfg config.ClusterConfig + }{ + { + "NoJWT", + config.ClusterConfig{ClusterSize: 1}, + }, + { + "JWT", + config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + resp, err := rootAuthClient.Grant(10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + lresp, err := rootAuthClient.LeaseList() + if err != nil { + t.Fatal(err) + } + if len(lresp.Leases) != 1 || lresp.Leases[0].ID != leaseID { + t.Fatalf("want %v leaseID but got %v leases", leaseID, lresp.Leases) + } + }) + }) + } +} + +func TestAuthLeaseAttach(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + users := []struct { + name string + password string + role string + key string + end string + }{ + { + name: "user1", + password: "user1-123", + role: "role1", + key: "k1", + end: "k3", + }, + { + name: "user2", + password: "user2-123", + role: "role2", + key: "k2", + end: "k4", + }, + } + for _, user := range users { + authSetupTestUser(t, cc, user.name, user.password, user.role, user.key, user.end) + } + authEnable(t, cc) + user1c := clus.MustClient(config.ClientOption{UserName: "user1", Password: "user1-123"}) + user2c := clus.MustClient(config.ClientOption{UserName: "user2", Password: "user2-123"}) + leaseResp, err := user1c.Grant(90) + testutil.AssertNil(t, err) + leaseID := leaseResp.ID + // permission of k2 is also granted to user2 + err = user1c.Put("k2", "val", config.PutOptions{LeaseID: leaseID}) + testutil.AssertNil(t, err) + _, err = user2c.LeaseRevoke(leaseID) + testutil.AssertNil(t, err) + + leaseResp, err = user1c.Grant(90) + testutil.AssertNil(t, err) + leaseID = leaseResp.ID + // permission of k1 isn't granted to user2 + err = user1c.Put("k1", "val", config.PutOptions{LeaseID: leaseID}) + testutil.AssertNil(t, err) + _, err = user2c.LeaseRevoke(leaseID) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrPermissionDenied.Error()) { + t.Fatalf("want %v error but got %v error", rpctypes.ErrPermissionDenied, err) + } + }) +} + +func TestAuthLeaseRevoke(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // put with TTL 10 seconds and revoke + resp, err := rootAuthClient.Grant(10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootAuthClient.Put("key", "val", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + if _, err = rootAuthClient.LeaseRevoke(leaseID); err != nil { + t.Fatal(err) + } + gresp, err := rootAuthClient.Get("key", config.GetOptions{}) + if err != nil || len(gresp.Kvs) != 0 { + t.Fatalf("want nil err and no kvs but got (%v) error and %d kvs", err, len(gresp.Kvs)) + } + }) +} + +func TestAuthRoleGet(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + if _, err := rootAuthClient.RoleGet("test-role"); err != nil { + t.Fatal(err) + } + // test-user can get the information of test-role because it belongs to the role + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + if _, err := testUserAuthClient.RoleGet("test-role"); err != nil { + t.Fatal(err) + } + // test-user cannot get the information of root because it doesn't belong to the role + _, err := testUserAuthClient.RoleGet("root") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrPermissionDenied.Error()) { + t.Fatalf("want %v error but got %v", rpctypes.ErrPermissionDenied, err) + } + }) +} + +func TestAuthUserGet(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + resp, err := rootAuthClient.UserGet("test-user") + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, resp.Roles, []string{"test-role"}) + // test-user can get the information of test-user itself + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + resp, err = testUserAuthClient.UserGet("test-user") + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, resp.Roles, []string{"test-role"}) + // test-user cannot get the information of root + _, err = testUserAuthClient.UserGet("root") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrPermissionDenied.Error()) { + t.Fatalf("want %v error but got %v", rpctypes.ErrPermissionDenied, err) + } + }) +} + +func TestAuthRoleList(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + resp, err := rootAuthClient.RoleList() + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, resp.Roles, []string{"test-role"}) + }) +} + +func TestAuthDefrag(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + var kvs = []testutils.KV{{Key: "key", Val: "val1"}, {Key: "key", Val: "val2"}, {Key: "key", Val: "val3"}} + for i := range kvs { + if err := cc.Put(kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { + t.Fatalf("TestAuthDefrag #%d: put kv error (%v)", i, err) + } + } + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + // ordinary user cannot defrag + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + if err := testUserAuthClient.Defragment(config.DefragOption{Timeout: 5 * time.Second}); err == nil { + t.Fatal("want error but got no error") + } + // root can defrag + if err := rootAuthClient.Defragment(config.DefragOption{Timeout: 5 * time.Second}); err != nil { + t.Fatal(err) + } + }) +} + +func TestAuthEndpointHealth(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootAuthClient) + testUserAuthClient := clus.MustClient(config.ClientOption{UserName: "test-user", Password: "pass"}) + + if err := rootAuthClient.Health(); err != nil { + t.Fatal(err) + } + // health checking with an ordinary user "succeeds" since permission denial goes through consensus + if err := testUserAuthClient.Health(); err != nil { + t.Fatal(err) + } + // succeed if permissions granted for ordinary user + if _, err := rootAuthClient.RoleGrantPermission("test-role", "health", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + if err := testUserAuthClient.Health(); err != nil { + t.Fatal(err) + } + }) +} + +func TestAuthJWTExpire(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootUserAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + authSetupDefaultTestUser(t, rootUserAuthClient) + // try a granted key + if err := rootUserAuthClient.Put("hoo", "bar", config.PutOptions{}); err != nil { + t.Error(err) + } + // wait an expiration of my JWT token + <-time.After(3 * time.Second) + if err := rootUserAuthClient.Put("hoo", "bar", config.PutOptions{}); err != nil { + t.Error(err) + } + }) +} + +// TestAuthRevisionConsistency ensures authRevision is the same after etcd restart +func TestAuthRevisionConsistency(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootUserAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + // add user + if _, err := rootUserAuthClient.UserAdd("test-user", "pass", config.UserAddOptions{}); err != nil { + t.Fatal(err) + } + // delete the same user + if _, err := rootUserAuthClient.UserDelete("test-user"); err != nil { + t.Fatal(err) + } + sresp, err := rootUserAuthClient.AuthStatus() + if err != nil { + t.Fatal(err) + } + oldAuthRevision := sresp.AuthRevision + // restart the node + m := clus.Members()[0] + m.Stop() + if err = m.Start(); err != nil { + t.Fatal(err) + } + sresp, err = rootUserAuthClient.AuthStatus() + if err != nil { + t.Fatal(err) + } + newAuthRevision := sresp.AuthRevision + // assert AuthRevision equal + if newAuthRevision != oldAuthRevision { + t.Fatalf("auth revison shouldn't change when restarting etcd, expected: %d, got: %d", oldAuthRevision, newAuthRevision) + } + }) +} + +// TestAuthKVRevision ensures kv revision is the same after auth mutating operations +func TestAuthKVRevision(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + err := cc.Put("foo", "bar", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + statusResp, err := cc.Status() + if err != nil { + t.Fatal(err) + } + rev := statusResp[0].Header.Revision + aresp, aerr := cc.UserAdd("root", "123", config.UserAddOptions{NoPassword: false}) + if aerr != nil { + t.Fatal(err) + } + if aresp.Header.Revision != rev { + t.Fatalf("revision want %d, got %d", rev, aresp.Header.Revision) + } + }) +} + +// TestAuthConcurrent ensures concurrent auth ops don't cause old authRevision errors +func TestAuthRevConcurrent(t *testing.T) { + testRunner.BeforeTest(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(t, cc) + rootUserAuthClient := clus.MustClient(config.ClientOption{UserName: "root", Password: "root"}) + var wg sync.WaitGroup + f := func(i int) { + defer wg.Done() + role, user := fmt.Sprintf("test-role-%d", i), fmt.Sprintf("test-user-%d", i) + _, err := rootUserAuthClient.RoleAdd(role) + testutil.AssertNil(t, err) + _, err = rootUserAuthClient.RoleGrantPermission(role, "", clientv3.GetPrefixRangeEnd(""), clientv3.PermissionType(clientv3.PermReadWrite)) + testutil.AssertNil(t, err) + _, err = rootUserAuthClient.UserAdd(user, "123", config.UserAddOptions{NoPassword: false}) + testutil.AssertNil(t, err) + err = rootUserAuthClient.Put("a", "b", config.PutOptions{}) + testutil.AssertNil(t, err) + } + // needs concurrency to trigger + numRoles := 2 + wg.Add(numRoles) + for i := 0; i < numRoles; i++ { + go f(i) + } + wg.Wait() + }) +} + +func authEnable(t *testing.T, cc framework.Client) { + // create root user with root role + _, err := cc.UserAdd("root", "root", config.UserAddOptions{}) + if err != nil { + t.Fatalf("failed to create root user %v", err) + } + _, err = cc.UserGrantRole("root", "root") + if err != nil { + t.Fatalf("failed to grant root user root role %v", err) + } + _, err = cc.AuthEnable() + if err != nil { + t.Fatalf("failed to enable auth %v", err) + } +} + +func authSetupDefaultTestUser(t *testing.T, cc framework.Client) { + authSetupTestUser(t, cc, "test-user", "pass", "test-role", "foo", "") +} + +func authSetupTestUser(t *testing.T, cc framework.Client, userName, password, roleName, key, end string) { + _, err := cc.UserAdd(userName, password, config.UserAddOptions{}) + if err != nil { + t.Fatalf("failed to create test-user %v", err) + } + _, err = cc.RoleAdd(roleName) + if err != nil { + t.Fatalf("failed to create test-role %v", err) + } + _, err = cc.UserGrantRole(userName, roleName) + if err != nil { + t.Fatalf("failed to grant role test-role to user test-user %v", err) + } + _, err = cc.RoleGrantPermission(roleName, key, end, clientv3.PermissionType(clientv3.PermReadWrite)) + if err != nil { + t.Fatalf("failed to grant role test-role readwrite permission to key foo %v", err) + } +} + +func putFailPerm(t *testing.T, cc framework.Client, key, val string) { + err := cc.Put(key, val, config.PutOptions{}) + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrPermissionDenied.Error()) { + t.Errorf("want error %s but got %v", rpctypes.ErrPermissionDenied.Error(), err) + } +} + +func mustAbsPath(path string) string { + abs, err := filepath.Abs(path) + if err != nil { + panic(err) + } + return abs +} diff --git a/tests/common/compact_test.go b/tests/common/compact_test.go index 2b1031daedab..62e07514d7d5 100644 --- a/tests/common/compact_test.go +++ b/tests/common/compact_test.go @@ -47,14 +47,15 @@ func TestCompact(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 3}) defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { var kvs = []testutils.KV{{Key: "key", Val: "val1"}, {Key: "key", Val: "val2"}, {Key: "key", Val: "val3"}} for i := range kvs { - if err := clus.Client().Put(kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { + if err := cc.Put(kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { t.Fatalf("compactTest #%d: put kv error (%v)", i, err) } } - get, err := clus.Client().Get("key", config.GetOptions{Revision: 3}) + get, err := cc.Get("key", config.GetOptions{Revision: 3}) if err != nil { t.Fatalf("compactTest: Get kv by revision error (%v)", err) } @@ -62,12 +63,12 @@ func TestCompact(t *testing.T) { getkvs := testutils.KeyValuesFromGetResponse(get) assert.Equal(t, kvs[1:2], getkvs) - _, err = clus.Client().Compact(4, tc.options) + _, err = cc.Compact(4, tc.options) if err != nil { t.Fatalf("compactTest: Compact error (%v)", err) } - get, err = clus.Client().Get("key", config.GetOptions{Revision: 3}) + get, err = cc.Get("key", config.GetOptions{Revision: 3}) if err != nil { if !strings.Contains(err.Error(), "required revision has been compacted") { t.Fatalf("compactTest: Get compact key error (%v)", err) @@ -76,7 +77,7 @@ func TestCompact(t *testing.T) { t.Fatalf("expected '...has been compacted' error, got ") } - _, err = clus.Client().Compact(2, tc.options) + _, err = cc.Compact(2, tc.options) if err != nil { if !strings.Contains(err.Error(), "required revision has been compacted") { t.Fatal(err) diff --git a/tests/common/defrag_test.go b/tests/common/defrag_test.go index 295149312e33..6e4ab9bf4a5b 100644 --- a/tests/common/defrag_test.go +++ b/tests/common/defrag_test.go @@ -29,20 +29,21 @@ func TestDefragOnline(t *testing.T) { defer cancel() options := config.DefragOption{Timeout: 10 * time.Second} clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 3}) + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { defer clus.Close() var kvs = []testutils.KV{{Key: "key", Val: "val1"}, {Key: "key", Val: "val2"}, {Key: "key", Val: "val3"}} for i := range kvs { - if err := clus.Client().Put(kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { + if err := cc.Put(kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { t.Fatalf("compactTest #%d: put kv error (%v)", i, err) } } - _, err := clus.Client().Compact(4, config.CompactOption{Physical: true, Timeout: 10 * time.Second}) + _, err := cc.Compact(4, config.CompactOption{Physical: true, Timeout: 10 * time.Second}) if err != nil { t.Fatalf("defrag_test: compact with revision error (%v)", err) } - if err = clus.Client().Defragment(options); err != nil { + if err = cc.Defragment(options); err != nil { t.Fatalf("defrag_test: defrag error (%v)", err) } }) diff --git a/tests/common/endpoint_test.go b/tests/common/endpoint_test.go index d1ba6da18b6d..b4427ef116fc 100644 --- a/tests/common/endpoint_test.go +++ b/tests/common/endpoint_test.go @@ -29,8 +29,9 @@ func TestEndpointStatus(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 3}) defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { - _, err := clus.Client().Status() + _, err := cc.Status() if err != nil { t.Fatalf("get endpoint status error: %v", err) } @@ -43,8 +44,9 @@ func TestEndpointHashKV(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 3}) defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { - _, err := clus.Client().HashKV(0) + _, err := cc.HashKV(0) if err != nil { t.Fatalf("get endpoint hashkv error: %v", err) } @@ -57,8 +59,9 @@ func TestEndpointHealth(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 3}) defer clus.Close() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { - if err := clus.Client().Health(); err != nil { + if err := cc.Health(); err != nil { t.Fatalf("get endpoint health error: %v", err) } }) diff --git a/tests/common/kv_test.go b/tests/common/kv_test.go index ef2540ceddbd..be32450be26a 100644 --- a/tests/common/kv_test.go +++ b/tests/common/kv_test.go @@ -33,7 +33,7 @@ func TestKVPut(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { key, value := "foo", "bar" @@ -67,7 +67,7 @@ func TestKVGet(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { var ( @@ -127,7 +127,7 @@ func TestKVDelete(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { kvs := []string{"a", "b", "c", "c/abc", "d"} tests := []struct { diff --git a/tests/common/lease_test.go b/tests/common/lease_test.go index 0281b3cfe385..3a3cb79153f6 100644 --- a/tests/common/lease_test.go +++ b/tests/common/lease_test.go @@ -59,7 +59,7 @@ func TestLeaseGrantTimeToLive(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { ttl := int64(10) @@ -103,7 +103,7 @@ func TestLeaseGrantAndList(t *testing.T) { t.Logf("Creating cluster...") clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) t.Logf("Created cluster and client") testutils.ExecuteUntil(ctx, t, func() { createdLeases := []clientv3.LeaseID{} @@ -150,7 +150,7 @@ func TestLeaseGrantTimeToLiveExpired(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { leaseResp, err := cc.Grant(2) @@ -187,7 +187,7 @@ func TestLeaseGrantKeepAliveOnce(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { leaseResp, err := cc.Grant(2) @@ -216,7 +216,7 @@ func TestLeaseGrantRevoke(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { leaseResp, err := cc.Grant(20) diff --git a/tests/common/member_test.go b/tests/common/member_test.go index f1b6be3aef03..fd559c861f85 100644 --- a/tests/common/member_test.go +++ b/tests/common/member_test.go @@ -16,9 +16,11 @@ package common import ( "context" - "go.etcd.io/etcd/tests/v3/framework/testutils" "testing" "time" + + "go.etcd.io/etcd/tests/v3/framework/config" + "go.etcd.io/etcd/tests/v3/framework/testutils" ) func TestMemberList(t *testing.T) { @@ -30,7 +32,7 @@ func TestMemberList(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { resp, err := cc.MemberList() diff --git a/tests/common/role_test.go b/tests/common/role_test.go index 6b1382aa2510..b3ed27a87aa1 100644 --- a/tests/common/role_test.go +++ b/tests/common/role_test.go @@ -34,7 +34,7 @@ func TestRoleAdd_Simple(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { _, err := cc.RoleAdd("root") @@ -52,7 +52,7 @@ func TestRoleAdd_Error(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { _, err := cc.RoleAdd("test-role") if err != nil { @@ -75,7 +75,7 @@ func TestRootRole(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { _, err := cc.RoleAdd("root") if err != nil { @@ -105,7 +105,7 @@ func TestRoleGrantRevokePermission(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { _, err := cc.RoleAdd("role1") if err != nil { @@ -140,7 +140,7 @@ func TestRoleDelete(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, config.ClusterConfig{ClusterSize: 1}) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { _, err := cc.RoleAdd("role1") if err != nil { diff --git a/tests/common/txn_test.go b/tests/common/txn_test.go index 79497224e335..70f57360c0e5 100644 --- a/tests/common/txn_test.go +++ b/tests/common/txn_test.go @@ -60,7 +60,7 @@ func TestTxnSucc(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, cfg.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { if err := cc.Put("key1", "value1", config.PutOptions{}); err != nil { t.Fatalf("could not create key:%s, value:%s", "key1", "value1") @@ -104,7 +104,7 @@ func TestTxnFail(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, cfg.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { if err := cc.Put("key1", "value1", config.PutOptions{}); err != nil { t.Fatalf("could not create key:%s, value:%s", "key1", "value1") diff --git a/tests/common/user_test.go b/tests/common/user_test.go index b48a8224ea01..f82ae1d4b0f6 100644 --- a/tests/common/user_test.go +++ b/tests/common/user_test.go @@ -68,7 +68,7 @@ func TestUserAdd_Simple(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { resp, err := cc.UserAdd(nc.username, nc.password, config.UserAddOptions{NoPassword: nc.noPassword}) @@ -102,7 +102,7 @@ func TestUserAdd_DuplicateUserNotAllowed(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { user := "barb" @@ -131,7 +131,7 @@ func TestUserList(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { // No Users Yet @@ -172,7 +172,7 @@ func TestUserDelete(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { user := "barb" @@ -224,7 +224,7 @@ func TestUserChangePassword(t *testing.T) { defer cancel() clus := testRunner.NewCluster(ctx, t, tc.config) defer clus.Close() - cc := clus.Client() + cc := clus.MustClient(config.ClientOption{}) testutils.ExecuteUntil(ctx, t, func() { user := "barb" diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index 8c9980049dfd..3d4e14d444a0 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -15,73 +15,36 @@ package e2e import ( - "context" "fmt" "os" "syscall" "testing" "time" - clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/tests/v3/framework/e2e" ) -func TestCtlV3AuthEnable(t *testing.T) { - testCtl(t, authEnableTest) -} -func TestCtlV3AuthDisable(t *testing.T) { testCtl(t, authDisableTest) } -func TestCtlV3AuthGracefulDisable(t *testing.T) { testCtl(t, authGracefulDisableTest) } -func TestCtlV3AuthStatus(t *testing.T) { testCtl(t, authStatusTest) } -func TestCtlV3AuthWriteKey(t *testing.T) { testCtl(t, authCredWriteKeyTest) } -func TestCtlV3AuthRoleUpdate(t *testing.T) { testCtl(t, authRoleUpdateTest) } -func TestCtlV3AuthUserDeleteDuringOps(t *testing.T) { testCtl(t, authUserDeleteDuringOpsTest) } -func TestCtlV3AuthRoleRevokeDuringOps(t *testing.T) { testCtl(t, authRoleRevokeDuringOpsTest) } -func TestCtlV3AuthTxn(t *testing.T) { testCtl(t, authTestTxn) } -func TestCtlV3AuthTxnJWT(t *testing.T) { testCtl(t, authTestTxn, withCfg(*e2e.NewConfigJWT())) } -func TestCtlV3AuthPrefixPerm(t *testing.T) { testCtl(t, authTestPrefixPerm) } -func TestCtlV3AuthMemberAdd(t *testing.T) { testCtl(t, authTestMemberAdd) } +// TestCtlV3AuthGracefulDisable ensure disable auth won't fail watch. TODO(@chaochn47) migrate to common once https://github.com/etcd-io/etcd/pull/13953 is merged. +func TestCtlV3AuthGracefulDisable(t *testing.T) { testCtl(t, authGracefulDisableTest) } + +// TODO(@chaochn47) migrate to common once member methods are created. +func TestCtlV3AuthMemberAdd(t *testing.T) { testCtl(t, authTestMemberAdd) } func TestCtlV3AuthMemberRemove(t *testing.T) { testCtl(t, authTestMemberRemove, withQuorum(), withNoStrictReconfig()) } -func TestCtlV3AuthMemberUpdate(t *testing.T) { testCtl(t, authTestMemberUpdate) } -func TestCtlV3AuthRevokeWithDelete(t *testing.T) { testCtl(t, authTestRevokeWithDelete) } -func TestCtlV3AuthInvalidMgmt(t *testing.T) { testCtl(t, authTestInvalidMgmt) } -func TestCtlV3AuthFromKeyPerm(t *testing.T) { testCtl(t, authTestFromKeyPerm) } -func TestCtlV3AuthAndWatch(t *testing.T) { testCtl(t, authTestWatch) } -func TestCtlV3AuthAndWatchJWT(t *testing.T) { testCtl(t, authTestWatch, withCfg(*e2e.NewConfigJWT())) } - -func TestCtlV3AuthLeaseTestKeepAlive(t *testing.T) { testCtl(t, authLeaseTestKeepAlive) } -func TestCtlV3AuthLeaseTestTimeToLiveExpired(t *testing.T) { - testCtl(t, authLeaseTestTimeToLiveExpired) -} -func TestCtlV3AuthLeaseGrantLeases(t *testing.T) { testCtl(t, authLeaseTestLeaseGrantLeases) } -func TestCtlV3AuthLeaseGrantLeasesJWT(t *testing.T) { - testCtl(t, authLeaseTestLeaseGrantLeases, withCfg(*e2e.NewConfigJWT())) -} -func TestCtlV3AuthLeaseRevoke(t *testing.T) { testCtl(t, authLeaseTestLeaseRevoke) } +func TestCtlV3AuthMemberUpdate(t *testing.T) { testCtl(t, authTestMemberUpdate) } -func TestCtlV3AuthRoleGet(t *testing.T) { testCtl(t, authTestRoleGet) } -func TestCtlV3AuthUserGet(t *testing.T) { testCtl(t, authTestUserGet) } -func TestCtlV3AuthRoleList(t *testing.T) { testCtl(t, authTestRoleList) } +func TestCtlV3AuthFromKeyPerm(t *testing.T) { testCtl(t, authTestFromKeyPerm) } -func TestCtlV3AuthDefrag(t *testing.T) { testCtl(t, authTestDefrag) } -func TestCtlV3AuthEndpointHealth(t *testing.T) { - testCtl(t, authTestEndpointHealth, withQuorum()) -} +// TODO(@chaochn47) migrate to common once https://github.com/etcd-io/etcd/pull/13953 is merged. +func TestCtlV3AuthAndWatch(t *testing.T) { testCtl(t, authTestWatch) } +func TestCtlV3AuthAndWatchJWT(t *testing.T) { testCtl(t, authTestWatch, withCfg(*e2e.NewConfigJWT())) } + +// TODO(@chaochn47) migrate to common once snapshot methods are created. ref. https://github.com/etcd-io/etcd/issues/13637#issuecomment-1123202080 func TestCtlV3AuthSnapshot(t *testing.T) { testCtl(t, authTestSnapshot) } func TestCtlV3AuthSnapshotJWT(t *testing.T) { testCtl(t, authTestSnapshot, withCfg(*e2e.NewConfigJWT())) } -func TestCtlV3AuthJWTExpire(t *testing.T) { - testCtl(t, authTestJWTExpire, withCfg(*e2e.NewConfigJWT())) -} -func TestCtlV3AuthRevisionConsistency(t *testing.T) { testCtl(t, authTestRevisionConsistency) } - -func authEnableTest(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } -} func authEnable(cx ctlCtx) error { // create root user with root role @@ -102,47 +65,6 @@ func ctlV3AuthEnable(cx ctlCtx) error { return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "Authentication Enabled") } -func authDisableTest(cx ctlCtx) { - // a key that isn't granted to test-user - if err := ctlV3Put(cx, "hoo", "a", ""); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // test-user doesn't have the permission, it must fail - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - if err := ctlV3AuthDisable(cx); err != nil { - cx.t.Fatalf("authDisableTest ctlV3AuthDisable error (%v)", err) - } - - // now ErrAuthNotEnabled of Authenticate() is simply ignored - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - - // now the key can be accessed - cx.user, cx.pass = "", "" - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"hoo"}, []kv{{"hoo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } -} - func authGracefulDisableTest(cx ctlCtx) { if err := authEnable(cx); err != nil { cx.t.Fatal(err) @@ -192,248 +114,6 @@ func ctlV3AuthDisable(cx ctlCtx) error { return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "Authentication Disabled") } -func authStatusTest(cx ctlCtx) { - cmdArgs := append(cx.PrefixArgs(), "auth", "status") - if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "Authentication Status: false", "AuthRevision:"); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - cmdArgs = append(cx.PrefixArgs(), "auth", "status") - - if err := e2e.SpawnWithExpects(cmdArgs, cx.envMap, "Authentication Status: true", "AuthRevision:"); err != nil { - cx.t.Fatal(err) - } - - cmdArgs = append(cx.PrefixArgs(), "auth", "status", "--write-out", "json") - if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "enabled"); err != nil { - cx.t.Fatal(err) - } - if err := e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, "authRevision"); err != nil { - cx.t.Fatal(err) - } -} - -func authCredWriteKeyTest(cx ctlCtx) { - // baseline key to check for failed puts - if err := ctlV3Put(cx, "foo", "a", ""); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // confirm root role can access to all keys - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // try invalid user - cx.user, cx.pass = "a", "b" - if err := ctlV3PutFailAuth(cx, "foo", "bar"); err != nil { - cx.t.Fatal(err) - } - // confirm put failed - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // try good user - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "foo", "bar2", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar2"}}...); err != nil { - cx.t.Fatal(err) - } - - // try bad password - cx.user, cx.pass = "test-user", "badpass" - if err := ctlV3PutFailAuth(cx, "foo", "baz"); err != nil { - cx.t.Fatal(err) - } - // confirm put failed - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar2"}}...); err != nil { - cx.t.Fatal(err) - } -} - -func authRoleUpdateTest(cx ctlCtx) { - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // try put to not granted key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { - cx.t.Fatal(err) - } - - // grant a new key - cx.user, cx.pass = "root", "root" - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "hoo", "", false}); err != nil { - cx.t.Fatal(err) - } - - // try a newly granted key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"hoo"}, []kv{{"hoo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // revoke the newly granted key - cx.user, cx.pass = "root", "root" - if err := ctlV3RoleRevokePermission(cx, "test-role", "hoo", "", false); err != nil { - cx.t.Fatal(err) - } - - // try put to the revoked key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3PutFailPerm(cx, "hoo", "bar"); err != nil { - cx.t.Fatal(err) - } - - // confirm a key still granted can be accessed - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } -} - -func authUserDeleteDuringOpsTest(cx ctlCtx) { - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // create a key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // delete the user - cx.user, cx.pass = "root", "root" - err := ctlV3User(cx, []string{"delete", "test-user"}, "User test-user deleted", []string{}) - if err != nil { - cx.t.Fatal(err) - } - - // check the user is deleted - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3PutFailAuth(cx, "foo", "baz"); err != nil { - cx.t.Fatal(err) - } -} - -func authRoleRevokeDuringOpsTest(cx ctlCtx) { - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // create a key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "foo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"foo"}, []kv{{"foo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // create a new role - cx.user, cx.pass = "root", "root" - if err := ctlV3Role(cx, []string{"add", "test-role2"}, "Role test-role2 created"); err != nil { - cx.t.Fatal(err) - } - // grant a new key to the new role - if err := ctlV3RoleGrantPermission(cx, "test-role2", grantingPerm{true, true, "hoo", "", false}); err != nil { - cx.t.Fatal(err) - } - // grant the new role to the user - if err := ctlV3User(cx, []string{"grant-role", "test-user", "test-role2"}, "Role test-role2 is granted to user test-user", nil); err != nil { - cx.t.Fatal(err) - } - - // try a newly granted key - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"hoo"}, []kv{{"hoo", "bar"}}...); err != nil { - cx.t.Fatal(err) - } - - // revoke a role from the user - cx.user, cx.pass = "root", "root" - err := ctlV3User(cx, []string{"revoke-role", "test-user", "test-role"}, "Role test-role is revoked from user test-user", []string{}) - if err != nil { - cx.t.Fatal(err) - } - - // check the role is revoked and permission is lost from the user - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3PutFailPerm(cx, "foo", "baz"); err != nil { - cx.t.Fatal(err) - } - - // try a key that can be accessed from the remaining role - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Put(cx, "hoo", "bar2", ""); err != nil { - cx.t.Fatal(err) - } - // confirm put succeeded - if err := ctlV3Get(cx, []string{"hoo"}, []kv{{"hoo", "bar2"}}...); err != nil { - cx.t.Fatal(err) - } -} - -func ctlV3PutFailAuth(cx ctlCtx, key, val string) error { - return e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "put", key, val), cx.envMap, "authentication failed") -} - func ctlV3PutFailPerm(cx ctlCtx, key, val string) error { return e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "put", key, val), cx.envMap, "permission denied") } @@ -454,131 +134,6 @@ func authSetupTestUser(cx ctlCtx) { } } -func authTestTxn(cx ctlCtx) { - // keys with 1 suffix aren't granted to test-user - // keys with 2 suffix are granted to test-user - - keys := []string{"c1", "s1", "f1"} - grantedKeys := []string{"c2", "s2", "f2"} - for _, key := range keys { - if err := ctlV3Put(cx, key, "v", ""); err != nil { - cx.t.Fatal(err) - } - } - - for _, key := range grantedKeys { - if err := ctlV3Put(cx, key, "v", ""); err != nil { - cx.t.Fatal(err) - } - } - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // grant keys to test-user - cx.user, cx.pass = "root", "root" - for _, key := range grantedKeys { - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, key, "", false}); err != nil { - cx.t.Fatal(err) - } - } - - // now test txn - cx.interactive = true - cx.user, cx.pass = "test-user", "pass" - - rqs := txnRequests{ - compare: []string{`version("c2") = "1"`}, - ifSucess: []string{"get s2"}, - ifFail: []string{"get f2"}, - results: []string{"SUCCESS", "s2", "v"}, - } - if err := ctlV3Txn(cx, rqs); err != nil { - cx.t.Fatal(err) - } - - // a key of compare case isn't granted - rqs = txnRequests{ - compare: []string{`version("c1") = "1"`}, - ifSucess: []string{"get s2"}, - ifFail: []string{"get f2"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs); err != nil { - cx.t.Fatal(err) - } - - // a key of success case isn't granted - rqs = txnRequests{ - compare: []string{`version("c2") = "1"`}, - ifSucess: []string{"get s1"}, - ifFail: []string{"get f2"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs); err != nil { - cx.t.Fatal(err) - } - - // a key of failure case isn't granted - rqs = txnRequests{ - compare: []string{`version("c2") = "1"`}, - ifSucess: []string{"get s2"}, - ifFail: []string{"get f1"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs); err != nil { - cx.t.Fatal(err) - } -} - -func authTestPrefixPerm(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - prefix := "/prefix/" // directory like prefix - // grant keys to test-user - cx.user, cx.pass = "root", "root" - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, prefix, "", true}); err != nil { - cx.t.Fatal(err) - } - - // try a prefix granted permission - cx.user, cx.pass = "test-user", "pass" - for i := 0; i < 10; i++ { - key := fmt.Sprintf("%s%d", prefix, i) - if err := ctlV3Put(cx, key, "val", ""); err != nil { - cx.t.Fatal(err) - } - } - - if err := ctlV3PutFailPerm(cx, clientv3.GetPrefixRangeEnd(prefix), "baz"); err != nil { - cx.t.Fatal(err) - } - - // grant the entire keys to test-user - cx.user, cx.pass = "root", "root" - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "", "", true}); err != nil { - cx.t.Fatal(err) - } - - prefix2 := "/prefix2/" - cx.user, cx.pass = "test-user", "pass" - for i := 0; i < 10; i++ { - key := fmt.Sprintf("%s%d", prefix2, i) - if err := ctlV3Put(cx, key, "val", ""); err != nil { - cx.t.Fatal(err) - } - } -} - func authTestMemberAdd(cx ctlCtx) { if err := authEnable(cx); err != nil { cx.t.Fatal(err) @@ -686,55 +241,6 @@ func authTestCertCN(cx ctlCtx) { } } -func authTestRevokeWithDelete(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // create a new role - cx.user, cx.pass = "root", "root" - if err := ctlV3Role(cx, []string{"add", "test-role2"}, "Role test-role2 created"); err != nil { - cx.t.Fatal(err) - } - - // grant the new role to the user - if err := ctlV3User(cx, []string{"grant-role", "test-user", "test-role2"}, "Role test-role2 is granted to user test-user", nil); err != nil { - cx.t.Fatal(err) - } - - // check the result - if err := ctlV3User(cx, []string{"get", "test-user"}, "Roles: test-role test-role2", nil); err != nil { - cx.t.Fatal(err) - } - - // delete the role, test-role2 must be revoked from test-user - if err := ctlV3Role(cx, []string{"delete", "test-role2"}, "Role test-role2 deleted"); err != nil { - cx.t.Fatal(err) - } - - // check the result - if err := ctlV3User(cx, []string{"get", "test-user"}, "Roles: test-role", nil); err != nil { - cx.t.Fatal(err) - } -} - -func authTestInvalidMgmt(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - if err := ctlV3Role(cx, []string{"delete", "root"}, "Error: etcdserver: invalid auth management"); err == nil { - cx.t.Fatal("deleting the role root must not be allowed") - } - - if err := ctlV3User(cx, []string{"revoke-role", "root", "root"}, "Error: etcdserver: invalid auth management", []string{}); err == nil { - cx.t.Fatal("revoking the role root from the user root must not be allowed") - } -} - func authTestFromKeyPerm(cx ctlCtx) { if err := authEnable(cx); err != nil { cx.t.Fatal(err) @@ -816,112 +322,6 @@ func authTestFromKeyPerm(cx ctlCtx) { } } -func authLeaseTestKeepAlive(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - // put with TTL 10 seconds and keep-alive - leaseID, err := ctlV3LeaseGrant(cx, 10) - if err != nil { - cx.t.Fatalf("leaseTestKeepAlive: ctlV3LeaseGrant error (%v)", err) - } - if err := ctlV3Put(cx, "key", "val", leaseID); err != nil { - cx.t.Fatalf("leaseTestKeepAlive: ctlV3Put error (%v)", err) - } - if err := ctlV3LeaseKeepAlive(cx, leaseID); err != nil { - cx.t.Fatalf("leaseTestKeepAlive: ctlV3LeaseKeepAlive error (%v)", err) - } - if err := ctlV3Get(cx, []string{"key"}, kv{"key", "val"}); err != nil { - cx.t.Fatalf("leaseTestKeepAlive: ctlV3Get error (%v)", err) - } -} - -func authLeaseTestTimeToLiveExpired(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - ttl := 3 - if err := leaseTestTimeToLiveExpire(cx, ttl); err != nil { - cx.t.Fatalf("leaseTestTimeToLiveExpire: error (%v)", err) - } -} - -func leaseTestTimeToLiveExpire(cx ctlCtx, ttl int) error { - leaseID, err := ctlV3LeaseGrant(cx, ttl) - if err != nil { - return fmt.Errorf("ctlV3LeaseGrant error (%v)", err) - } - - if err = ctlV3Put(cx, "key", "val", leaseID); err != nil { - return fmt.Errorf("ctlV3Put error (%v)", err) - } - // eliminate false positive - time.Sleep(time.Duration(ttl+1) * time.Second) - cmdArgs := append(cx.PrefixArgs(), "lease", "timetolive", leaseID) - exp := fmt.Sprintf("lease %s already expired", leaseID) - if err = e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, exp); err != nil { - return fmt.Errorf("lease not properly expired: (%v)", err) - } - if err := ctlV3Get(cx, []string{"key"}); err != nil { - return fmt.Errorf("ctlV3Get error (%v)", err) - } - return nil -} - -func authLeaseTestLeaseGrantLeases(cx ctlCtx) { - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - if err := leaseTestGrantLeasesList(cx); err != nil { - cx.t.Fatalf("authLeaseTestLeaseGrantLeases: error (%v)", err) - } -} - -func leaseTestGrantLeasesList(cx ctlCtx) error { - id, err := ctlV3LeaseGrant(cx, 10) - if err != nil { - return fmt.Errorf("ctlV3LeaseGrant error (%v)", err) - } - - cmdArgs := append(cx.PrefixArgs(), "lease", "list") - proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap) - if err != nil { - return fmt.Errorf("lease list failed (%v)", err) - } - _, err = proc.Expect(id) - if err != nil { - return fmt.Errorf("lease id not in returned list (%v)", err) - } - return proc.Close() -} - -func authLeaseTestLeaseRevoke(cx ctlCtx) { - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // put with TTL 10 seconds and revoke - leaseID, err := ctlV3LeaseGrant(cx, 10) - if err != nil { - cx.t.Fatalf("ctlV3LeaseGrant error (%v)", err) - } - if err := ctlV3Put(cx, "key", "val", leaseID); err != nil { - cx.t.Fatalf("ctlV3Put error (%v)", err) - } - if err := ctlV3LeaseRevoke(cx, leaseID); err != nil { - cx.t.Fatalf("ctlV3LeaseRevoke error (%v)", err) - } - if err := ctlV3GetWithErr(cx, []string{"key"}, []string{"retrying of unary invoker failed"}); err != nil { // expect errors - cx.t.Fatalf("ctlV3GetWithErr error (%v)", err) - } -} - func authTestWatch(cx ctlCtx) { if err := authEnable(cx); err != nil { cx.t.Fatal(err) @@ -999,102 +399,6 @@ func authTestWatch(cx ctlCtx) { } -func authTestRoleGet(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - expected := []string{ - "Role test-role", - "KV Read:", "foo", - "KV Write:", "foo", - } - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "test-role"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } - - // test-user can get the information of test-role because it belongs to the role - cx.user, cx.pass = "test-user", "pass" - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "test-role"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } - - // test-user cannot get the information of root because it doesn't belong to the role - expected = []string{ - "Error: etcdserver: permission denied", - } - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "root"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } -} - -func authTestUserGet(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - expected := []string{ - "User: test-user", - "Roles: test-role", - } - - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "test-user"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } - - // test-user can get the information of test-user itself - cx.user, cx.pass = "test-user", "pass" - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "test-user"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } - - // test-user cannot get the information of root - expected = []string{ - "Error: etcdserver: permission denied", - } - if err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "root"), cx.envMap, expected...); err != nil { - cx.t.Fatal(err) - } -} - -func authTestRoleList(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - if err := e2e.SpawnWithExpectWithEnv(append(cx.PrefixArgs(), "role", "list"), cx.envMap, "test-role"); err != nil { - cx.t.Fatal(err) - } -} - -func authTestDefrag(cx ctlCtx) { - maintenanceInitKeys(cx) - - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // ordinary user cannot defrag - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3OnlineDefrag(cx); err == nil { - cx.t.Fatal("ordinary user should not be able to issue a defrag request") - } - - // root can defrag - cx.user, cx.pass = "root", "root" - if err := ctlV3OnlineDefrag(cx); err != nil { - cx.t.Fatal(err) - } -} - func authTestSnapshot(cx ctlCtx) { maintenanceInitKeys(cx) @@ -1132,35 +436,6 @@ func authTestSnapshot(cx ctlCtx) { } } -func authTestEndpointHealth(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - if err := ctlV3EndpointHealth(cx); err != nil { - cx.t.Fatalf("endpointStatusTest ctlV3EndpointHealth error (%v)", err) - } - - // health checking with an ordinary user "succeeds" since permission denial goes through consensus - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3EndpointHealth(cx); err != nil { - cx.t.Fatalf("endpointStatusTest ctlV3EndpointHealth error (%v)", err) - } - - // succeed if permissions granted for ordinary user - cx.user, cx.pass = "root", "root" - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "health", "", false}); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3EndpointHealth(cx); err != nil { - cx.t.Fatalf("endpointStatusTest ctlV3EndpointHealth error (%v)", err) - } -} - func certCNAndUsername(cx ctlCtx, noPassword bool) { if err := authEnable(cx); err != nil { cx.t.Fatal(err) @@ -1227,85 +502,6 @@ func authTestCertCNAndUsernameNoPassword(cx ctlCtx) { certCNAndUsername(cx, true) } -func authTestJWTExpire(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // try a granted key - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Error(err) - } - - // wait an expiration of my JWT token - <-time.After(3 * time.Second) - - if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil { - cx.t.Error(err) - } -} - -func authTestRevisionConsistency(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - cx.user, cx.pass = "root", "root" - - // add user - if err := ctlV3User(cx, []string{"add", "test-user", "--interactive=false"}, "User test-user created", []string{"pass"}); err != nil { - cx.t.Fatal(err) - } - // delete the same user - if err := ctlV3User(cx, []string{"delete", "test-user"}, "User test-user deleted", []string{}); err != nil { - cx.t.Fatal(err) - } - - // get node0 auth revision - node0 := cx.epc.Procs[0] - endpoint := node0.EndpointsV3()[0] - cli, err := clientv3.New(clientv3.Config{Endpoints: []string{endpoint}, Username: cx.user, Password: cx.pass, DialTimeout: 3 * time.Second}) - if err != nil { - cx.t.Fatal(err) - } - defer cli.Close() - - sresp, err := cli.AuthStatus(context.TODO()) - if err != nil { - cx.t.Fatal(err) - } - oldAuthRevision := sresp.AuthRevision - - // restart the node - node0.WithStopSignal(syscall.SIGINT) - if err := node0.Restart(); err != nil { - cx.t.Fatal(err) - } - - // get node0 auth revision again - sresp, err = cli.AuthStatus(context.TODO()) - if err != nil { - cx.t.Fatal(err) - } - newAuthRevision := sresp.AuthRevision - - // assert AuthRevision equal - if newAuthRevision != oldAuthRevision { - cx.t.Fatalf("auth revison shouldn't change when restarting etcd, expected: %d, got: %d", oldAuthRevision, newAuthRevision) - } -} - -func ctlV3EndpointHealth(cx ctlCtx) error { - cmdArgs := append(cx.PrefixArgs(), "endpoint", "health") - lines := make([]string, cx.epc.Cfg.ClusterSize) - for i := range lines { - lines[i] = "is healthy" - } - return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...) -} - func ctlV3User(cx ctlCtx, args []string, expStr string, stdIn []string) error { cmdArgs := append(cx.PrefixArgs(), "user") cmdArgs = append(cmdArgs, args...) diff --git a/tests/framework/config/client.go b/tests/framework/config/client.go index e8ce9ea794a1..2c5b66867e88 100644 --- a/tests/framework/config/client.go +++ b/tests/framework/config/client.go @@ -20,6 +20,15 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" ) +type ClientOption struct { + UserName string + Password string +} + +func (opt ClientOption) Empty() bool { + return opt.UserName == "" && opt.Password == "" +} + type GetOptions struct { Revision int End string diff --git a/tests/framework/config/cluster.go b/tests/framework/config/cluster.go index f18c6e5c4061..46a33dd08252 100644 --- a/tests/framework/config/cluster.go +++ b/tests/framework/config/cluster.go @@ -27,4 +27,6 @@ type ClusterConfig struct { PeerTLS TLSConfig ClientTLS TLSConfig QuotaBackendBytes int64 + AuthToken string + SnapshotCount uint64 } diff --git a/tests/framework/e2e.go b/tests/framework/e2e.go index 4f8ec81daab9..6de746cab4af 100644 --- a/tests/framework/e2e.go +++ b/tests/framework/e2e.go @@ -44,6 +44,8 @@ func (e e2eRunner) NewCluster(ctx context.Context, t testing.TB, cfg config.Clus InitialToken: "new", ClusterSize: cfg.ClusterSize, QuotaBackendBytes: cfg.QuotaBackendBytes, + AuthTokenOpts: cfg.AuthToken, + SnapshotCount: int(cfg.SnapshotCount), } switch cfg.ClientTLS { case config.NoTLS: @@ -74,15 +76,32 @@ func (e e2eRunner) NewCluster(ctx context.Context, t testing.TB, cfg config.Clus if err != nil { t.Fatalf("could not start etcd integrationCluster: %s", err) } - return &e2eCluster{*epc} + return &e2eCluster{t, *epc} } type e2eCluster struct { + t testing.TB e2e.EtcdProcessCluster } -func (c *e2eCluster) Client() Client { - return e2eClient{e2e.NewEtcdctl(c.Cfg, c.EndpointsV3())} +func (c *e2eCluster) Client(cfg config.ClientOption) (Client, error) { + etcdctl := e2e.NewEtcdctl(c.Cfg, c.EndpointsV3()) + if !cfg.Empty() { + var err error + etcdctl, err = etcdctl.WithAuth(cfg.UserName, cfg.Password) + if err != nil { + return nil, err + } + } + return e2eClient{etcdctl}, nil +} + +func (c *e2eCluster) MustClient(cfg config.ClientOption) Client { + cc, err := c.Client(cfg) + if err != nil { + c.t.Fatal(err) + } + return cc } func (c *e2eCluster) Members() (ms []Member) { diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index 97de1f21f4c6..a3f22d17208b 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -20,16 +20,20 @@ import ( "io" "strconv" "strings" + "time" "go.etcd.io/etcd/api/v3/authpb" "go.etcd.io/etcd/api/v3/etcdserverpb" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/tests/v3/framework/config" + "google.golang.org/grpc" ) type EtcdctlV3 struct { cfg *EtcdProcessClusterConfig endpoints []string + userName string + password string } func NewEtcdctl(cfg *EtcdProcessClusterConfig, endpoints []string) *EtcdctlV3 { @@ -39,6 +43,23 @@ func NewEtcdctl(cfg *EtcdProcessClusterConfig, endpoints []string) *EtcdctlV3 { } } +func (ctl *EtcdctlV3) WithAuth(userName, password string) (*EtcdctlV3, error) { + ctl.userName = userName + ctl.password = password + cfg := clientv3.Config{ + Endpoints: ctl.endpoints, + DialTimeout: 5 * time.Second, + DialOptions: []grpc.DialOption{grpc.WithBlock()}, + Username: userName, + Password: password, + } + c, err := clientv3.New(cfg) + if c != nil { + c.Close() + } + return ctl, err +} + func (ctl *EtcdctlV3) DowngradeEnable(version string) error { return SpawnWithExpect(ctl.cmdArgs("downgrade", "enable", version), "Downgrade enable success") } @@ -272,6 +293,9 @@ func (ctl *EtcdctlV3) flags() map[string]string { } } fmap["endpoints"] = strings.Join(ctl.endpoints, ",") + if ctl.userName != "" && ctl.password != "" { + fmap["user"] = ctl.userName + ":" + ctl.password + } return fmap } @@ -435,6 +459,20 @@ func (ctl *EtcdctlV3) AlarmDisarm(_ *clientv3.AlarmMember) (*clientv3.AlarmRespo return &resp, err } +func (ctl *EtcdctlV3) AuthEnable() (*clientv3.AuthEnableResponse, error) { + return &clientv3.AuthEnableResponse{}, SpawnWithExpect(ctl.cmdArgs("auth", "enable"), "Authentication Enabled") +} + +func (ctl *EtcdctlV3) AuthDisable() (*clientv3.AuthDisableResponse, error) { + return &clientv3.AuthDisableResponse{}, SpawnWithExpect(ctl.cmdArgs("auth", "disable"), "Authentication Disabled") +} + +func (ctl *EtcdctlV3) AuthStatus() (*clientv3.AuthStatusResponse, error) { + var resp clientv3.AuthStatusResponse + err := ctl.spawnJsonCmd(&resp, "auth", "status") + return &resp, err +} + func (ctl *EtcdctlV3) UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) { args := ctl.cmdArgs() args = append(args, "user", "add") @@ -473,6 +511,12 @@ func (ctl *EtcdctlV3) UserAdd(name, password string, opts config.UserAddOptions) return &resp, err } +func (ctl *EtcdctlV3) UserGet(name string) (*clientv3.AuthUserGetResponse, error) { + var resp clientv3.AuthUserGetResponse + err := ctl.spawnJsonCmd(&resp, "user", "get", name) + return &resp, err +} + func (ctl *EtcdctlV3) UserList() (*clientv3.AuthUserListResponse, error) { var resp clientv3.AuthUserListResponse err := ctl.spawnJsonCmd(&resp, "user", "list") @@ -502,6 +546,18 @@ func (ctl *EtcdctlV3) UserChangePass(user, newPass string) error { return err } +func (ctl *EtcdctlV3) UserGrantRole(user string, role string) (*clientv3.AuthUserGrantRoleResponse, error) { + var resp clientv3.AuthUserGrantRoleResponse + err := ctl.spawnJsonCmd(&resp, "user", "grant-role", user, role) + return &resp, err +} + +func (ctl *EtcdctlV3) UserRevokeRole(user string, role string) (*clientv3.AuthUserRevokeRoleResponse, error) { + var resp clientv3.AuthUserRevokeRoleResponse + err := ctl.spawnJsonCmd(&resp, "user", "revoke-role", user, role) + return &resp, err +} + func (ctl *EtcdctlV3) RoleAdd(name string) (*clientv3.AuthRoleAddResponse, error) { var resp clientv3.AuthRoleAddResponse err := ctl.spawnJsonCmd(&resp, "role", "add", name) diff --git a/tests/framework/integration.go b/tests/framework/integration.go index 631b7263e696..12251899fd5a 100644 --- a/tests/framework/integration.go +++ b/tests/framework/integration.go @@ -46,8 +46,11 @@ func (e integrationRunner) NewCluster(ctx context.Context, t testing.TB, cfg con var err error var integrationCfg integration.ClusterConfig integrationCfg.Size = cfg.ClusterSize - integrationCfg.ClientTLS, err = tlsInfo(t, cfg.ClientTLS) integrationCfg.QuotaBackendBytes = cfg.QuotaBackendBytes + integrationCfg.AuthToken = cfg.AuthToken + integrationCfg.SnapshotCount = cfg.SnapshotCount + + integrationCfg.ClientTLS, err = tlsInfo(t, cfg.ClientTLS) if err != nil { t.Fatalf("ClientTLS: %s", err) } @@ -115,12 +118,30 @@ func (c *integrationCluster) Close() error { return nil } -func (c *integrationCluster) Client() Client { - cc, err := c.ClusterClient() +func (c *integrationCluster) Client(cfg config.ClientOption) (Client, error) { + option := func(_ *clientv3.Config) {} + if !cfg.Empty() { + option = func(clientCfg *clientv3.Config) { + clientCfg.Username = cfg.UserName + clientCfg.Password = cfg.Password + } + } + cc, err := c.ClusterClient(option) + if err != nil { + return nil, err + } + c.t.Cleanup(func() { + cc.Close() + }) + return integrationClient{cc, c.ctx}, nil +} + +func (c *integrationCluster) MustClient(cfg config.ClientOption) Client { + cc, err := c.Client(cfg) if err != nil { c.t.Fatal(err) } - return integrationClient{Client: cc, ctx: c.ctx} + return cc } type integrationClient struct { @@ -287,12 +308,28 @@ func (c integrationClient) LeaseRevoke(id clientv3.LeaseID) (*clientv3.LeaseRevo return c.Client.Revoke(c.ctx, id) } +func (c integrationClient) AuthEnable() (*clientv3.AuthEnableResponse, error) { + return c.Client.AuthEnable(context.Background()) +} + +func (c integrationClient) AuthDisable() (*clientv3.AuthDisableResponse, error) { + return c.Client.AuthDisable(context.Background()) +} + +func (c integrationClient) AuthStatus() (*clientv3.AuthStatusResponse, error) { + return c.Client.AuthStatus(context.Background()) +} + func (c integrationClient) UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) { return c.Client.UserAddWithOptions(c.ctx, name, password, &clientv3.UserAddOptions{ NoPassword: opts.NoPassword, }) } +func (c integrationClient) UserGet(name string) (*clientv3.AuthUserGetResponse, error) { + return c.Client.UserGet(context.Background(), name) +} + func (c integrationClient) UserList() (*clientv3.AuthUserListResponse, error) { return c.Client.UserList(c.ctx) } @@ -306,6 +343,14 @@ func (c integrationClient) UserChangePass(user, newPass string) error { return err } +func (c integrationClient) UserGrantRole(user string, role string) (*clientv3.AuthUserGrantRoleResponse, error) { + return c.Client.UserGrantRole(context.Background(), user, role) +} + +func (c integrationClient) UserRevokeRole(user string, role string) (*clientv3.AuthUserRevokeRoleResponse, error) { + return c.Client.UserRevokeRole(context.Background(), user, role) +} + func (c integrationClient) RoleAdd(name string) (*clientv3.AuthRoleAddResponse, error) { return c.Client.RoleAdd(c.ctx, name) } diff --git a/tests/framework/integration/cluster.go b/tests/framework/integration/cluster.go index a59499701862..17e1861b07c9 100644 --- a/tests/framework/integration/cluster.go +++ b/tests/framework/integration/cluster.go @@ -179,6 +179,8 @@ type Cluster struct { mu sync.Mutex clusterClient *clientv3.Client + + clusterAuthClients []*clientv3.Client } func SchemeFromTLSInfo(tls *transport.TLSInfo) string { @@ -1374,6 +1376,11 @@ func (c *Cluster) Terminate(t testutil.TB) { } } c.mu.Unlock() + for _, client := range c.clusterAuthClients { + if err := client.Close(); err != nil { + t.Error(err) + } + } for _, m := range c.Members { if m.Client != nil { m.Client.Close() @@ -1409,32 +1416,41 @@ func (c *Cluster) Endpoints() []string { return endpoints } -func (c *Cluster) ClusterClient() (client *clientv3.Client, err error) { - if c.clusterClient == nil { - var endpoints []string - for _, m := range c.Members { - endpoints = append(endpoints, m.GrpcURL) - } - cfg := clientv3.Config{ - Endpoints: endpoints, - DialTimeout: 5 * time.Second, - DialOptions: []grpc.DialOption{grpc.WithBlock()}, - MaxCallSendMsgSize: c.Cfg.ClientMaxCallSendMsgSize, - MaxCallRecvMsgSize: c.Cfg.ClientMaxCallRecvMsgSize, - } - if c.Cfg.ClientTLS != nil { - tls, err := c.Cfg.ClientTLS.ClientConfig() - if err != nil { - return nil, err - } - cfg.TLS = tls - } - c.clusterClient, err = newClientV3(cfg) +func (c *Cluster) ClusterClient(opts ...func(*clientv3.Config)) (client *clientv3.Client, err error) { + if c.clusterClient != nil && len(opts) == 0 { + return c.clusterClient, nil + } + + cfg, err := c.newClientCfg() + if err != nil { + return nil, err + } + for _, opt := range opts { + opt(cfg) + } + c.clusterClient, err = newClientV3(*cfg) + if err != nil { + return nil, err + } + return c.clusterClient, err +} + +func (c *Cluster) newClientCfg() (*clientv3.Config, error) { + cfg := &clientv3.Config{ + Endpoints: c.Endpoints(), + DialTimeout: 5 * time.Second, + DialOptions: []grpc.DialOption{grpc.WithBlock()}, + MaxCallSendMsgSize: c.Cfg.ClientMaxCallSendMsgSize, + MaxCallRecvMsgSize: c.Cfg.ClientMaxCallRecvMsgSize, + } + if c.Cfg.ClientTLS != nil { + tls, err := c.Cfg.ClientTLS.ClientConfig() if err != nil { return nil, err } + cfg.TLS = tls } - return c.clusterClient, nil + return cfg, nil } // NewClientV3 creates a new grpc client connection to the member diff --git a/tests/framework/interface.go b/tests/framework/interface.go index a7ded291a884..7a7a9d707187 100644 --- a/tests/framework/interface.go +++ b/tests/framework/interface.go @@ -30,7 +30,8 @@ type testRunner interface { type Cluster interface { Members() []Member - Client() Client + Client(cfg config.ClientOption) (Client, error) + MustClient(cfg config.ClientOption) Client Close() error } @@ -57,11 +58,16 @@ type Client interface { LeaseKeepAliveOnce(id clientv3.LeaseID) (*clientv3.LeaseKeepAliveResponse, error) LeaseRevoke(id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error) + AuthEnable() (*clientv3.AuthEnableResponse, error) + AuthDisable() (*clientv3.AuthDisableResponse, error) + AuthStatus() (*clientv3.AuthStatusResponse, error) UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) + UserGet(name string) (*clientv3.AuthUserGetResponse, error) UserList() (*clientv3.AuthUserListResponse, error) UserDelete(name string) (*clientv3.AuthUserDeleteResponse, error) UserChangePass(user, newPass string) error - + UserGrantRole(user string, role string) (*clientv3.AuthUserGrantRoleResponse, error) + UserRevokeRole(user string, role string) (*clientv3.AuthUserRevokeRoleResponse, error) RoleAdd(name string) (*clientv3.AuthRoleAddResponse, error) RoleGrantPermission(name string, key, rangeEnd string, permType clientv3.PermissionType) (*clientv3.AuthRoleGrantPermissionResponse, error) RoleGet(role string) (*clientv3.AuthRoleGetResponse, error)