diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go new file mode 100644 index 000000000000..b0e705f93214 --- /dev/null +++ b/tests/common/auth_test.go @@ -0,0 +1,1303 @@ +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() + cc := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + }) +} + +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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + err := cc.Put(ctx, "hoo", "a", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + authEnable(ctx, cc, t) + + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + // test-user doesn't have the permission, it must fail + if err = testUserAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err == nil { + t.Fatalf("want error but got nil") + } + if err = rootAuthClient.AuthDisable(ctx); err != nil { + t.Fatalf("failed to auth disable %v", err) + } + // now ErrAuthNotEnabled of Authenticate() is simply ignored + if err = testUserAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // now the key can be accessed + if err = cc.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := cc.Get(ctx, "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 TestAuthGracefulDisable(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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + donec := make(chan struct{}) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + + go func() { + defer close(donec) + // sleep a bit to let the watcher connects while auth is still enabled + time.Sleep(time.Second) + // now disable auth... + if err := rootAuthClient.AuthDisable(ctx); err != nil { + t.Errorf("failed to auth disable %v", err) + return + } + // ...and restart the node + clus.Members()[0].Stop() + if err := clus.Members()[0].Start(ctx); err != nil { + t.Errorf("failed to restart member %v", err) + return + } + // the watcher should still work after reconnecting + if err := rootAuthClient.Put(ctx, "key", "value", config.PutOptions{}); err != nil { + t.Errorf("failed to put key value %v", err) + } + }() + + wCtx, wCancel := context.WithCancel(ctx) + defer wCancel() + + watchCh := rootAuthClient.Watch(wCtx, "key", config.WatchOptions{Revision: 1}) + wantedLen := 1 + watchTimeout := 10 * time.Second + wanted := []testutils.KV{{Key: "key", Val: "value"}} + kvs, err := testutils.KeyValuesFromWatchChan(watchCh, wantedLen, watchTimeout) + if err != nil { + t.Fatalf("failed to get key-values from watch channel %s", err) + } + assert.Equal(t, wanted, kvs) + <-donec + }) +} + +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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + resp, err := cc.AuthStatus(ctx) + if err != nil { + t.Fatal(err) + } + if resp.Enabled { + t.Fatal("want not enabled but enabled") + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + resp, err = rootAuthClient.AuthStatus(ctx) + 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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // try put to not granted key + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + putFailPerm(ctx, testUserAuthClient, "hoo", "bar", t) + // grant a new key + if _, err := rootAuthClient.RoleGrantPermission(ctx, "test-role", "hoo", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // try a newly granted key + if err := testUserAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get(ctx, "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(ctx, "test-role", "hoo", ""); err != nil { + t.Fatal(err) + } + // try put to the revoked key + putFailPerm(ctx, testUserAuthClient, "hoo", "bar", t) + // confirm a key still granted can be accessed + resp, err = testUserAuthClient.Get(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + + // create a key + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + if err := testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get(ctx, "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(ctx, "test-user"); err != nil { + t.Fatal(err) + } + // check the user is deleted + err = testUserAuthClient.Put(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // create a key + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + if err := testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err := testUserAuthClient.Get(ctx, "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(ctx, "test-role2"); err != nil { + t.Fatal(err) + } + // grant a new key to the new role + if _, err = rootAuthClient.RoleGrantPermission(ctx, "test-role2", "hoo", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // grant the new role to the user + if _, err = rootAuthClient.UserGrantRole(ctx, "test-user", "test-role2"); err != nil { + t.Fatal(err) + } + + // try a newly granted key + if err := testUserAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get(ctx, "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(ctx, "test-user", "test-role"); err != nil { + t.Fatal(err) + } + // check the role is revoked and permission is lost from the user + putFailPerm(ctx, testUserAuthClient, "foo", "baz", t) + + // try a key that can be accessed from the remaining role + if err := testUserAuthClient.Put(ctx, "hoo", "bar2", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + if err := cc.Put(ctx, "foo", "a", config.PutOptions{}); err != nil { + t.Fatal(err) + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + + // confirm root role can access to all keys + if err := rootAuthClient.Put(ctx, "foo", "bar", config.PutOptions{}); err != nil { + t.Fatal(err) + } + resp, err := rootAuthClient.Get(ctx, "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(WithAuth("a", "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 := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + resp, err = testUserAuthClient.Get(ctx, "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(ctx, "foo", "bar2", config.PutOptions{}); err != nil { + t.Fatal(err) + } + // confirm put succeeded + resp, err = testUserAuthClient.Get(ctx, "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(WithAuth("test-user", "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(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + _, err := cc.Get(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + // 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(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + rctx, cancel := context.WithCancel(context.TODO()) + donec := make(chan struct{}) + go func() { + defer close(donec) + for rctx.Err() == nil { + rootAuthClient.Put(ctx, "abc", "def", config.PutOptions{}) + } + }() + time.Sleep(10 * time.Millisecond) + if err := rootAuthClient.AuthDisable(ctx); 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 := framework.MustClient(clus.Client()) + 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(ctx, key, "v", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + for _, key := range grantedKeys { + if err := cc.Put(ctx, key, "v", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // grant keys to test-user + for _, key := range grantedKeys { + if _, err := rootAuthClient.RoleGrantPermission(ctx, "test-role", key, "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + } + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + for _, req := range reqs { + resp, err := testUserAuthClient.Txn(ctx, 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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + prefix := "/prefix/" // directory like prefix + // grant keys to test-user + if _, err := rootAuthClient.RoleGrantPermission(ctx, "test-role", prefix, clientv3.GetPrefixRangeEnd(prefix), clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + // try a prefix granted permission + testUserClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + for i := 0; i < 10; i++ { + key := fmt.Sprintf("%s%d", prefix, i) + if err := testUserClient.Put(ctx, key, "val", config.PutOptions{}); err != nil { + t.Fatal(err) + } + } + putFailPerm(ctx, testUserClient, clientv3.GetPrefixRangeEnd(prefix), "baz", t) + // grant the prefix2 keys to test-user + prefix2 := "/prefix2/" + if _, err := rootAuthClient.RoleGrantPermission(ctx, "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(ctx, 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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // create a new role + if _, err := rootAuthClient.RoleAdd(ctx, "test-role2"); err != nil { + t.Fatal(err) + } + // grant the new role to the user + if _, err := rootAuthClient.UserGrantRole(ctx, "test-user", "test-role2"); err != nil { + t.Fatal(err) + } + // check the result + resp, err := rootAuthClient.UserGet(ctx, "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(ctx, "test-role2"); err != nil { + t.Fatal(err) + } + // check the result + resp, err = rootAuthClient.UserGet(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + _, err := rootAuthClient.RoleDelete(ctx, "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(ctx, "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() + cc := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + resp, err := rootAuthClient.Grant(ctx, 10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootAuthClient.Put(ctx, "key", "value", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + if _, err = rootAuthClient.KeepAliveOnce(ctx, leaseID); err != nil { + t.Fatal(err) + } + gresp, err := rootAuthClient.Get(ctx, "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() + cc := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + resp, err := rootAuthClient.Grant(ctx, 2) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootAuthClient.Put(ctx, "key", "val", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + // eliminate false positive + time.Sleep(3 * time.Second) + tresp, err := rootAuthClient.TimeToLive(ctx, 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(ctx, "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, tc.cfg) + defer clus.Close() + testutils.ExecuteUntil(ctx, t, func() { + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + resp, err := rootAuthClient.Grant(ctx, 10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + lresp, err := rootAuthClient.Leases(ctx) + 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 := framework.MustClient(clus.Client()) + 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(ctx, cc, user.name, user.password, user.role, user.key, user.end, t) + } + authEnable(ctx, cc, t) + user1c := framework.MustClient(clus.Client(WithAuth("user1", "user1-123"))) + user2c := framework.MustClient(clus.Client(WithAuth("user2", "user2-123"))) + leaseResp, err := user1c.Grant(ctx, 90) + testutil.AssertNil(t, err) + leaseID := leaseResp.ID + // permission of k2 is also granted to user2 + err = user1c.Put(ctx, "k2", "val", config.PutOptions{LeaseID: leaseID}) + testutil.AssertNil(t, err) + _, err = user2c.Revoke(ctx, leaseID) + testutil.AssertNil(t, err) + + leaseResp, err = user1c.Grant(ctx, 90) + testutil.AssertNil(t, err) + leaseID = leaseResp.ID + // permission of k1 isn't granted to user2 + err = user1c.Put(ctx, "k1", "val", config.PutOptions{LeaseID: leaseID}) + testutil.AssertNil(t, err) + _, err = user2c.Revoke(ctx, 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 := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // put with TTL 10 seconds and revoke + resp, err := rootAuthClient.Grant(ctx, 10) + if err != nil { + t.Fatal(err) + } + leaseID := resp.ID + if err = rootAuthClient.Put(ctx, "key", "val", config.PutOptions{LeaseID: leaseID}); err != nil { + t.Fatal(err) + } + if _, err = rootAuthClient.Revoke(ctx, leaseID); err != nil { + t.Fatal(err) + } + gresp, err := rootAuthClient.Get(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + if _, err := rootAuthClient.RoleGet(ctx, "test-role"); err != nil { + t.Fatal(err) + } + // test-user can get the information of test-role because it belongs to the role + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + if _, err := testUserAuthClient.RoleGet(ctx, "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(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + resp, err := rootAuthClient.UserGet(ctx, "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 := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + resp, err = testUserAuthClient.UserGet(ctx, "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(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + resp, err := rootAuthClient.RoleList(ctx) + 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 := framework.MustClient(clus.Client()) + 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(ctx, kvs[i].Key, kvs[i].Val, config.PutOptions{}); err != nil { + t.Fatalf("TestAuthDefrag #%d: put kv error (%v)", i, err) + } + } + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // ordinary user cannot defrag + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + if err := testUserAuthClient.Defragment(ctx, config.DefragOption{Timeout: 5 * time.Second}); err == nil { + t.Fatal("want error but got no error") + } + // root can defrag + if err := rootAuthClient.Defragment(ctx, 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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + + if err := rootAuthClient.Health(ctx); err != nil { + t.Fatal(err) + } + // health checking with an ordinary user "succeeds" since permission denial goes through consensus + if err := testUserAuthClient.Health(ctx); err != nil { + t.Fatal(err) + } + // succeed if permissions granted for ordinary user + if _, err := rootAuthClient.RoleGrantPermission(ctx, "test-role", "health", "", clientv3.PermissionType(clientv3.PermReadWrite)); err != nil { + t.Fatal(err) + } + if err := testUserAuthClient.Health(ctx); err != nil { + t.Fatal(err) + } + }) +} + +func TestAuthWatch(t *testing.T) { + watchTimeout := 1 * time.Second + tcs := []struct { + name string + cfg config.ClusterConfig + }{ + { + "NoJWT", + config.ClusterConfig{ClusterSize: 1}, + }, + { + "JWT", + config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}, + }, + } + + tests := []struct { + puts []testutils.KV + watchKey string + opts config.WatchOptions + want bool + wanted []testutils.KV + }{ + { // watch 1 key, should be successful + puts: []testutils.KV{{Key: "key", Val: "value"}}, + watchKey: "key", + opts: config.WatchOptions{Revision: 1}, + want: true, + wanted: []testutils.KV{{Key: "key", Val: "value"}}, + }, + { // watch 3 keys by range, should be successful + puts: []testutils.KV{{Key: "key1", Val: "value1"}, {Key: "key3", Val: "value3"}, {Key: "key2", Val: "value2"}}, + watchKey: "key", + opts: config.WatchOptions{RangeEnd: "key3", Revision: 1}, + want: true, + wanted: []testutils.KV{{Key: "key1", Val: "value1"}, {Key: "key2", Val: "value2"}}, + }, + { // watch 1 key, should not be successful + puts: []testutils.KV{}, + watchKey: "key5", + opts: config.WatchOptions{Revision: 1}, + want: false, + wanted: []testutils.KV{}, + }, + { // watch 3 keys by range, should not be successful + puts: []testutils.KV{}, + watchKey: "key", + opts: config.WatchOptions{RangeEnd: "key6", Revision: 1}, + want: false, + wanted: []testutils.KV{}, + }, + } + + for _, tc := range tcs { + for i, tt := range tests { + 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() + testutils.ExecuteUntil(ctx, t, func() { + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + + _, err := rootAuthClient.RoleGrantPermission(ctx, "test-role", "key", "key4", clientv3.PermissionType(clientv3.PermReadWrite)) + assert.NoError(t, err) + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + + donec := make(chan struct{}) + go func(i int, puts []testutils.KV) { + defer close(donec) + for j := range puts { + if err := testUserAuthClient.Put(ctx, puts[j].Key, puts[j].Val, config.PutOptions{}); err != nil { + t.Errorf("test #%d-%d: put error (%v)", i, j, err) + } + } + }(i, tt.puts) + wCtx, wCancel := context.WithCancel(ctx) + wch := testUserAuthClient.Watch(wCtx, tt.watchKey, tt.opts) + if wch == nil { + t.Fatalf("failed to watch %s", tt.watchKey) + } + kvs, err := testutils.KeyValuesFromWatchChan(wch, len(tt.wanted), watchTimeout) + if err != nil { + wCancel() + assert.False(t, tt.want) + } else { + assert.Equal(t, tt.wanted, kvs) + } + wCancel() + <-donec + }) + }) + } + } +} + +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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + // try a granted key + if err := rootAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Error(err) + } + // wait an expiration of my JWT token + <-time.After(3 * time.Second) + if err := rootAuthClient.Put(ctx, "hoo", "bar", config.PutOptions{}); err != nil { + t.Error(err) + } + }) +} + +func TestAuthMemberRemove(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: 3}) + defer clus.Close() + cc := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + authSetupDefaultTestUser(ctx, rootAuthClient, t) + testUserAuthClient := framework.MustClient(clus.Client(WithAuth("test-user", "pass"))) + + memberList, err := rootAuthClient.MemberList(ctx) + assert.NoError(t, err) + assert.Equal(t, 3, len(memberList.Members), "want 3 member but got %d", len(memberList.Members)) + id := memberList.Members[0].ID + + // 5 seconds is the minimum required amount of time peer is considered active + time.Sleep(5 * time.Second) + if _, err := testUserAuthClient.MemberRemove(ctx, id); err == nil { + t.Fatalf("ordinary user must not be allowed to remove a member") + } + if _, err := rootAuthClient.MemberRemove(ctx, id); err != nil { + t.Fatal(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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "root"))) + // add user + if _, err := rootAuthClient.UserAdd(ctx, "test-user", "pass", config.UserAddOptions{}); err != nil { + t.Fatal(err) + } + // delete the same user + if _, err := rootAuthClient.UserDelete(ctx, "test-user"); err != nil { + t.Fatal(err) + } + sresp, err := rootAuthClient.AuthStatus(ctx) + if err != nil { + t.Fatal(err) + } + oldAuthRevision := sresp.AuthRevision + // restart the node + m := clus.Members()[0] + m.Stop() + if err = m.Start(ctx); err != nil { + t.Fatal(err) + } + sresp, err = rootAuthClient.AuthStatus(ctx) + 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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + err := cc.Put(ctx, "foo", "bar", config.PutOptions{}) + if err != nil { + t.Fatal(err) + } + gresp, err := cc.Get(ctx, "foo", config.GetOptions{}) + if err != nil { + t.Fatal(err) + } + rev := gresp.Header.Revision + aresp, aerr := cc.UserAdd(ctx, "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 := framework.MustClient(clus.Client()) + testutils.ExecuteUntil(ctx, t, func() { + authEnable(ctx, cc, t) + rootAuthClient := framework.MustClient(clus.Client(WithAuth("root", "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 := rootAuthClient.RoleAdd(ctx, role) + testutil.AssertNil(t, err) + _, err = rootAuthClient.RoleGrantPermission(ctx, role, "a", clientv3.GetPrefixRangeEnd("a"), clientv3.PermissionType(clientv3.PermReadWrite)) + testutil.AssertNil(t, err) + _, err = rootAuthClient.UserAdd(ctx, user, "123", config.UserAddOptions{NoPassword: false}) + testutil.AssertNil(t, err) + err = rootAuthClient.Put(ctx, "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(ctx context.Context, cc framework.Client, t *testing.T) { + // create root user with root role + _, err := cc.UserAdd(ctx, "root", "root", config.UserAddOptions{}) + if err != nil { + t.Fatalf("failed to create root user %v", err) + } + _, err = cc.UserGrantRole(ctx, "root", "root") + if err != nil { + t.Fatalf("failed to grant root user root role %v", err) + } + if err = cc.AuthEnable(ctx); err != nil { + t.Fatalf("failed to enable auth %v", err) + } +} + +func authSetupDefaultTestUser(ctx context.Context, cc framework.Client, t *testing.T) { + authSetupTestUser(ctx, cc, "test-user", "pass", "test-role", "foo", "", t) +} + +func authSetupTestUser(ctx context.Context, cc framework.Client, userName, password, roleName, key, end string, t *testing.T) { + _, err := cc.UserAdd(ctx, userName, password, config.UserAddOptions{}) + if err != nil { + t.Fatalf("failed to create test-user %v", err) + } + _, err = cc.RoleAdd(ctx, roleName) + if err != nil { + t.Fatalf("failed to create test-role %v", err) + } + _, err = cc.UserGrantRole(ctx, userName, roleName) + if err != nil { + t.Fatalf("failed to grant role test-role to user test-user %v", err) + } + _, err = cc.RoleGrantPermission(ctx, 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(ctx context.Context, cc framework.Client, key, val string, t *testing.T) { + err := cc.Put(ctx, 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/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index bcdc2254cedb..12a20e802fee 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -26,65 +26,18 @@ import ( "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) } -func TestCtlV3AuthMemberRemove(t *testing.T) { - testCtl(t, authTestMemberRemove, withQuorum(), withDisableStrictReconfig()) -} -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 TestCtlV3AuthMemberAdd(t *testing.T) { testCtl(t, authTestMemberAdd) } +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()) -} 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 TestCtlV3AuthTestCacheReload(t *testing.T) { testCtl(t, authTestCacheReload) } -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 if err := ctlV3User(cx, []string{"add", "root", "--interactive=false"}, "User root created", []string{"root"}); err != nil { @@ -104,332 +57,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" - err := ctlV3PutFailPerm(cx, "hoo", "bar") - require.ErrorContains(cx.t, err, "permission denied") - - 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) - } - - cx.user, cx.pass = "root", "root" - - donec := make(chan struct{}) - - go func() { - defer close(donec) - - // sleep a bit to let the watcher connects while auth is still enabled - time.Sleep(time.Second) - - // now disable auth... - if err := ctlV3AuthDisable(cx); err != nil { - cx.t.Fatalf("authGracefulDisableTest ctlV3AuthDisable error (%v)", err) - } - - // ...and restart the node - node0 := cx.epc.Procs[0] - if rerr := node0.Restart(context.TODO()); rerr != nil { - cx.t.Fatal(rerr) - } - - // the watcher should still work after reconnecting - if perr := ctlV3Put(cx, "key", "value", ""); perr != nil { - cx.t.Errorf("authGracefulDisableTest ctlV3Put error (%v)", perr) - } - }() - - err := ctlV3Watch(cx, []string{"key"}, kvExec{key: "key", val: "value"}) - - if err != nil { - if cx.dialTimeout > 0 && !isGRPCTimedout(err) { - cx.t.Errorf("authGracefulDisableTest ctlV3Watch error (%v)", err) - } - } - - <-donec -} - -func ctlV3AuthDisable(cx ctlCtx) error { - cmdArgs := append(cx.PrefixArgs(), "auth", "disable") - 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" - err := ctlV3PutFailAuth(cx, "foo", "bar") - require.ErrorContains(cx.t, err, "authentication failed") - - // 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" - err = ctlV3PutFailAuth(cx, "foo", "baz") - require.ErrorContains(cx.t, err, "authentication failed") - - // 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" - err := ctlV3PutFailPerm(cx, "hoo", "bar") - require.ErrorContains(cx.t, err, "permission denied") - - // 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" - err = ctlV3PutFailPerm(cx, "hoo", "bar") - require.ErrorContains(cx.t, err, "permission denied") - - // 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" - err = ctlV3PutFailAuth(cx, "foo", "baz") - require.ErrorContains(cx.t, err, "authentication failed") -} - -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" - err = ctlV3PutFailPerm(cx, "foo", "baz") - require.ErrorContains(cx.t, err, "permission denied") - - // 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") } @@ -450,130 +77,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"`}, - ifSuccess: []string{"get s2"}, - ifFail: []string{"get f2"}, - results: []string{"SUCCESS", "s2", "v"}, - } - if err := ctlV3Txn(cx, rqs, false); err != nil { - cx.t.Fatal(err) - } - - // a key of compare case isn't granted - rqs = txnRequests{ - compare: []string{`version("c1") = "1"`}, - ifSuccess: []string{"get s2"}, - ifFail: []string{"get f2"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs, true); err != nil { - cx.t.Fatal(err) - } - - // a key of success case isn't granted - rqs = txnRequests{ - compare: []string{`version("c2") = "1"`}, - ifSuccess: []string{"get s1"}, - ifFail: []string{"get f2"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs, true); err != nil { - cx.t.Fatal(err) - } - - // a key of failure case isn't granted - rqs = txnRequests{ - compare: []string{`version("c2") = "1"`}, - ifSuccess: []string{"get s2"}, - ifFail: []string{"get f1"}, - results: []string{"Error: etcdserver: permission denied"}, - } - if err := ctlV3Txn(cx, rqs, true); 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) - } - } - - err := ctlV3PutFailPerm(cx, clientv3.GetPrefixRangeEnd(prefix), "baz") - require.ErrorContains(cx.t, err, "permission denied") - - // 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) @@ -586,35 +89,12 @@ func authTestMemberAdd(cx ctlCtx) { // ordinary user cannot add a new member cx.user, cx.pass = "test-user", "pass" if err := ctlV3MemberAdd(cx, peerURL, false); err == nil { - cx.t.Fatalf("ordinary user must not be allowed to add a member") - } - - // root can add a new member - cx.user, cx.pass = "root", "root" - if err := ctlV3MemberAdd(cx, peerURL, false); err != nil { - cx.t.Fatal(err) - } -} - -func authTestMemberRemove(cx ctlCtx) { - if err := authEnable(cx); err != nil { - cx.t.Fatal(err) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - ep, memIDToRemove, clusterID := cx.memberToRemove() - - // ordinary user cannot remove a member - cx.user, cx.pass = "test-user", "pass" - if err := ctlV3MemberRemove(cx, ep, memIDToRemove, clusterID); err == nil { - cx.t.Fatalf("ordinary user must not be allowed to remove a member") + cx.t.Fatalf("ordinary user must not be allowed to add a member") } - // root can remove a member + // root can add a new member cx.user, cx.pass = "root", "root" - if err := ctlV3MemberRemove(cx, ep, memIDToRemove, clusterID); err != nil { + if err := ctlV3MemberAdd(cx, peerURL, false); err != nil { cx.t.Fatal(err) } } @@ -680,55 +160,6 @@ func authTestCertCN(cx ctlCtx) { require.ErrorContains(cx.t, err, "permission denied") } -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) @@ -807,281 +238,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 - err := leaseTestTimeToLiveExpire(cx, ttl) - require.NoError(cx.t, 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) - } - - cx.user, cx.pass = "root", "root" - authSetupTestUser(cx) - - // grant a key range - if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "key", "key4", false}); err != nil { - cx.t.Fatal(err) - } - - tests := []struct { - puts []kv - args []string - - wkv []kvExec - want bool - }{ - { // watch 1 key, should be successful - []kv{{"key", "value"}}, - []string{"key", "--rev", "1"}, - []kvExec{{key: "key", val: "value"}}, - true, - }, - { // watch 3 keys by range, should be successful - []kv{{"key1", "val1"}, {"key3", "val3"}, {"key2", "val2"}}, - []string{"key", "key3", "--rev", "1"}, - []kvExec{{key: "key1", val: "val1"}, {key: "key2", val: "val2"}}, - true, - }, - - { // watch 1 key, should not be successful - []kv{}, - []string{"key5", "--rev", "1"}, - []kvExec{}, - false, - }, - { // watch 3 keys by range, should not be successful - []kv{}, - []string{"key", "key6", "--rev", "1"}, - []kvExec{}, - false, - }, - } - - cx.user, cx.pass = "test-user", "pass" - for i, tt := range tests { - donec := make(chan struct{}) - go func(i int, puts []kv) { - defer close(donec) - for j := range puts { - if err := ctlV3Put(cx, puts[j].key, puts[j].val, ""); err != nil { - cx.t.Errorf("watchTest #%d-%d: ctlV3Put error (%v)", i, j, err) - } - } - }(i, tt.puts) - - var err error - if tt.want { - err = ctlV3Watch(cx, tt.args, tt.wkv...) - if err != nil && cx.dialTimeout > 0 && !isGRPCTimedout(err) { - cx.t.Errorf("watchTest #%d: ctlV3Watch error (%v)", i, err) - } - } else { - err = ctlV3WatchFailPerm(cx, tt.args) - // this will not have any meaningful error output, but the process fails due to the cancellation - require.ErrorContains(cx.t, err, "unexpected exit code") - } - - <-donec - } - -} - -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", - } - err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "role", "get", "root"), cx.envMap, expected...) - require.ErrorContains(cx.t, err, "permission denied") -} - -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", - } - err := e2e.SpawnWithExpects(append(cx.PrefixArgs(), "user", "get", "root"), cx.envMap, expected...) - require.ErrorContains(cx.t, err, "permission denied") -} - -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) @@ -1119,35 +275,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) @@ -1212,84 +339,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 - if err := node0.Restart(context.TODO()); 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...) @@ -1313,7 +362,6 @@ func ctlV3User(cx ctlCtx, args []string, expStr string, stdIn []string) error { // authTestCacheReload tests the permissions when a member restarts func authTestCacheReload(cx ctlCtx) { - authData := []struct { user string role string