diff --git a/tests/common/role_test.go b/tests/common/role_test.go new file mode 100644 index 00000000000..3cd8cad1e6d --- /dev/null +++ b/tests/common/role_test.go @@ -0,0 +1,168 @@ +// Copyright 2022 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "strings" + "testing" + "time" + + "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/tests/v3/framework/config" + "go.etcd.io/etcd/tests/v3/framework/testutils" +) + +func TestRoleAdd_Simple(t *testing.T) { + testRunner.BeforeTest(t) + tcs := []struct { + name string + config config.ClusterConfig + }{ + { + name: "NoTLS", + config: config.ClusterConfig{ClusterSize: 1}, + }, + { + name: "PeerTLS", + config: config.ClusterConfig{ClusterSize: 3, PeerTLS: config.ManualTLS}, + }, + { + name: "PeerAutoTLS", + config: config.ClusterConfig{ClusterSize: 3, PeerTLS: config.AutoTLS}, + }, + { + name: "ClientTLS", + config: config.ClusterConfig{ClusterSize: 1, ClientTLS: config.ManualTLS}, + }, + { + name: "ClientAutoTLS", + config: config.ClusterConfig{ClusterSize: 1, ClientTLS: config.AutoTLS}, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + clus := testRunner.NewCluster(t, tc.config) + defer clus.Close() + cc := clus.Client() + + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + _, err := cc.RoleAdd("root") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + }) + }) + } +} + +func TestRoleAdd_Error(t *testing.T) { + testRunner.BeforeTest(t) + clus := testRunner.NewCluster(t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.Client() + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + _, err := cc.RoleAdd("test-role") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleAdd("test-role") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrRoleAlreadyExist.Error()) { + t.Fatalf("want (%v) error, but got (%v)", rpctypes.ErrRoleAlreadyExist, err) + } + _, err = cc.RoleAdd("") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrRoleEmpty.Error()) { + t.Fatalf("want (%v) error, but got (%v)", rpctypes.ErrRoleEmpty, err) + } + }) +} + +func TestRootRole(t *testing.T) { + testRunner.BeforeTest(t) + clus := testRunner.NewCluster(t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.Client() + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + _, err := cc.RoleAdd("root") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + resp, err := cc.RoleGet("root") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + t.Logf("get role resp %+v", resp) + // granting to root should be refused by server and a no-op + _, err = cc.RoleGrantPermission("root", "foo", "", clientv3.PermissionType(clientv3.PermReadWrite)) + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + resp2, err := cc.RoleGet("root") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + t.Logf("get role resp %+v", resp2) + }) +} + +func TestRoleGrantRevokePermission(t *testing.T) { + testRunner.BeforeTest(t) + clus := testRunner.NewCluster(t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.Client() + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + _, err := cc.RoleAdd("role1") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleGrantPermission("role1", "bar", "", clientv3.PermissionType(clientv3.PermRead)) + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleGrantPermission("role1", "bar", "", clientv3.PermissionType(clientv3.PermWrite)) + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleGrantPermission("role1", "bar", "foo", clientv3.PermissionType(clientv3.PermReadWrite)) + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleRevokePermission("role1", "foo", "") + if err == nil || !strings.Contains(err.Error(), rpctypes.ErrPermissionNotGranted.Error()) { + t.Fatalf("want error (%v), but got (%v)", rpctypes.ErrPermissionNotGranted, err) + } + _, err = cc.RoleRevokePermission("role1", "bar", "foo") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + }) +} + +func TestRoleDelete(t *testing.T) { + testRunner.BeforeTest(t) + clus := testRunner.NewCluster(t, config.ClusterConfig{ClusterSize: 1}) + defer clus.Close() + cc := clus.Client() + testutils.ExecuteWithTimeout(t, 10*time.Second, func() { + _, err := cc.RoleAdd("role1") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + _, err = cc.RoleDelete("role1") + if err != nil { + t.Fatalf("want no error, but got (%v)", err) + } + }) +} diff --git a/tests/e2e/ctl_v3_role_test.go b/tests/e2e/ctl_v3_role_test.go index 2c15660bc64..7bdd7459804 100644 --- a/tests/e2e/ctl_v3_role_test.go +++ b/tests/e2e/ctl_v3_role_test.go @@ -21,17 +21,10 @@ import ( "go.etcd.io/etcd/tests/v3/framework/e2e" ) -func TestCtlV3RoleAdd(t *testing.T) { testCtl(t, roleAddTest) } -func TestCtlV3RootRoleGet(t *testing.T) { testCtl(t, rootRoleGetTest) } -func TestCtlV3RoleAddNoTLS(t *testing.T) { testCtl(t, roleAddTest, withCfg(*e2e.NewConfigNoTLS())) } -func TestCtlV3RoleAddClientTLS(t *testing.T) { - testCtl(t, roleAddTest, withCfg(*e2e.NewConfigClientTLS())) -} -func TestCtlV3RoleAddPeerTLS(t *testing.T) { testCtl(t, roleAddTest, withCfg(*e2e.NewConfigPeerTLS())) } +// TestCtlV3RoleAddTimeout tests add role with 0 grpc dial timeout while it tolerates dial timeout error. +// This is unique in e2e test func TestCtlV3RoleAddTimeout(t *testing.T) { testCtl(t, roleAddTest, withDialTimeout(0)) } -func TestCtlV3RoleGrant(t *testing.T) { testCtl(t, roleGrantTest) } - func roleAddTest(cx ctlCtx) { cmdSet := []struct { args []string @@ -58,94 +51,6 @@ func roleAddTest(cx ctlCtx) { } } -func rootRoleGetTest(cx ctlCtx) { - cmdSet := []struct { - args []string - expectedStr interface{} - }{ - // Add a role of root . - { - args: []string{"add", "root"}, - expectedStr: "Role root created", - }, - // get root role should always return [, - { - args: []string{"get", "root"}, - expectedStr: []string{"Role root\r\n", "KV Read:\r\n", "\t[, \r\n", "KV Write:\r\n", "\t[, \r\n"}, - }, - // granting to root should be refused by server - { - args: []string{"grant-permission", "root", "readwrite", "foo"}, - expectedStr: "Role root updated", - }, - { - args: []string{"get", "root"}, - expectedStr: []string{"Role root\r\n", "KV Read:\r\n", "\t[, \r\n", "KV Write:\r\n", "\t[, \r\n"}, - }, - } - - for i, cmd := range cmdSet { - if _, ok := cmd.expectedStr.(string); ok { - if err := ctlV3Role(cx, cmd.args, cmd.expectedStr.(string)); err != nil { - if cx.dialTimeout > 0 && !isGRPCTimedout(err) { - cx.t.Fatalf("roleAddTest #%d: ctlV3Role error (%v)", i, err) - } - } - } else { - if err := ctlV3RoleMultiExpect(cx, cmd.args, cmd.expectedStr.([]string)...); err != nil { - if cx.dialTimeout > 0 && !isGRPCTimedout(err) { - cx.t.Fatalf("roleAddTest #%d: ctlV3Role error (%v)", i, err) - } - } - } - } -} - -func roleGrantTest(cx ctlCtx) { - cmdSet := []struct { - args []string - expectedStr string - }{ - // Add a role. - { - args: []string{"add", "root"}, - expectedStr: "Role root created", - }, - // Grant read permission to the role. - { - args: []string{"grant", "root", "read", "foo"}, - expectedStr: "Role root updated", - }, - // Grant write permission to the role. - { - args: []string{"grant", "root", "write", "foo"}, - expectedStr: "Role root updated", - }, - // Grant rw permission to the role. - { - args: []string{"grant", "root", "readwrite", "foo"}, - expectedStr: "Role root updated", - }, - // Try granting invalid permission to the role. - { - args: []string{"grant", "root", "123", "foo"}, - expectedStr: "invalid permission type", - }, - } - - for i, cmd := range cmdSet { - if err := ctlV3Role(cx, cmd.args, cmd.expectedStr); err != nil { - cx.t.Fatalf("roleGrantTest #%d: ctlV3Role error (%v)", i, err) - } - } -} - -func ctlV3RoleMultiExpect(cx ctlCtx, args []string, expStr ...string) error { - cmdArgs := append(cx.PrefixArgs(), "role") - cmdArgs = append(cmdArgs, args...) - - return e2e.SpawnWithExpects(cmdArgs, cx.envMap, expStr...) -} func ctlV3Role(cx ctlCtx, args []string, expStr string) error { cmdArgs := append(cx.PrefixArgs(), "role") cmdArgs = append(cmdArgs, args...) diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index 6fcecbecfbb..b4a8b6bf978 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" + "go.etcd.io/etcd/api/v3/authpb" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/tests/v3/framework/config" ) @@ -507,3 +508,53 @@ func (ctl *EtcdctlV3) UserChangePass(user, newPass string) error { _, err = cmd.Expect("Password updated") return err } + +func (ctl *EtcdctlV3) RoleAdd(name string) (*clientv3.AuthRoleAddResponse, error) { + var resp clientv3.AuthRoleAddResponse + err := ctl.spawnJsonCmd(&resp, "role", "add", name) + return &resp, err +} + +func (ctl *EtcdctlV3) RoleGrantPermission(name string, key, rangeEnd string, permType clientv3.PermissionType) (*clientv3.AuthRoleGrantPermissionResponse, error) { + permissionType := authpb.Permission_Type_name[int32(permType)] + var resp clientv3.AuthRoleGrantPermissionResponse + err := ctl.spawnJsonCmd(&resp, "role", "grant-permission", name, permissionType, key, rangeEnd) + return &resp, err +} + +func (ctl *EtcdctlV3) RoleGet(role string) (*clientv3.AuthRoleGetResponse, error) { + var resp clientv3.AuthRoleGetResponse + err := ctl.spawnJsonCmd(&resp, "role", "get", role) + return &resp, err +} + +func (ctl *EtcdctlV3) RoleList() (*clientv3.AuthRoleListResponse, error) { + var resp clientv3.AuthRoleListResponse + err := ctl.spawnJsonCmd(&resp, "role", "list") + return &resp, err +} + +func (ctl *EtcdctlV3) RoleRevokePermission(role string, key, rangeEnd string) (*clientv3.AuthRoleRevokePermissionResponse, error) { + var resp clientv3.AuthRoleRevokePermissionResponse + err := ctl.spawnJsonCmd(&resp, "role", "revoke-permission", role, key, rangeEnd) + return &resp, err +} + +func (ctl *EtcdctlV3) RoleDelete(role string) (*clientv3.AuthRoleDeleteResponse, error) { + var resp clientv3.AuthRoleDeleteResponse + err := ctl.spawnJsonCmd(&resp, "role", "delete", role) + return &resp, err +} + +func (ctl *EtcdctlV3) spawnJsonCmd(output interface{}, args ...string) error { + args = append(args, "-w", "json") + cmd, err := SpawnCmd(append(ctl.cmdArgs(), args...), nil) + if err != nil { + return err + } + line, err := cmd.Expect("header") + if err != nil { + return err + } + return json.Unmarshal([]byte(line), output) +} diff --git a/tests/framework/integration.go b/tests/framework/integration.go index baa10890ed7..8140bd5dd23 100644 --- a/tests/framework/integration.go +++ b/tests/framework/integration.go @@ -311,3 +311,27 @@ func (c integrationClient) UserChangePass(user, newPass string) error { _, err := c.Client.UserChangePassword(context.Background(), user, newPass) return err } + +func (c integrationClient) RoleAdd(name string) (*clientv3.AuthRoleAddResponse, error) { + return c.Client.RoleAdd(context.Background(), name) +} + +func (c integrationClient) RoleGrantPermission(name string, key, rangeEnd string, permType clientv3.PermissionType) (*clientv3.AuthRoleGrantPermissionResponse, error) { + return c.Client.RoleGrantPermission(context.Background(), name, key, rangeEnd, permType) +} + +func (c integrationClient) RoleGet(role string) (*clientv3.AuthRoleGetResponse, error) { + return c.Client.RoleGet(context.Background(), role) +} + +func (c integrationClient) RoleList() (*clientv3.AuthRoleListResponse, error) { + return c.Client.RoleList(context.Background()) +} + +func (c integrationClient) RoleRevokePermission(role string, key, rangeEnd string) (*clientv3.AuthRoleRevokePermissionResponse, error) { + return c.Client.RoleRevokePermission(context.Background(), role, key, rangeEnd) +} + +func (c integrationClient) RoleDelete(role string) (*clientv3.AuthRoleDeleteResponse, error) { + return c.Client.RoleDelete(context.Background(), role) +} diff --git a/tests/framework/interface.go b/tests/framework/interface.go index ad952b709d9..43945a41c8f 100644 --- a/tests/framework/interface.go +++ b/tests/framework/interface.go @@ -60,4 +60,10 @@ type Client interface { UserList() (*clientv3.AuthUserListResponse, error) UserDelete(name string) (*clientv3.AuthUserDeleteResponse, error) UserChangePass(user, newPass string) 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) + RoleList() (*clientv3.AuthRoleListResponse, error) + RoleRevokePermission(role string, key, rangeEnd string) (*clientv3.AuthRoleRevokePermissionResponse, error) + RoleDelete(role string) (*clientv3.AuthRoleDeleteResponse, error) } diff --git a/tests/integration/clientv3/role_test.go b/tests/integration/clientv3/role_test.go deleted file mode 100644 index 38d74d90eb9..00000000000 --- a/tests/integration/clientv3/role_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2016 The etcd Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package clientv3test - -import ( - "context" - "testing" - - "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" - integration2 "go.etcd.io/etcd/tests/v3/framework/integration" -) - -func TestRoleError(t *testing.T) { - integration2.BeforeTest(t) - - clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1}) - defer clus.Terminate(t) - - authapi := clus.RandClient() - - _, err := authapi.RoleAdd(context.TODO(), "test-role") - if err != nil { - t.Fatal(err) - } - - _, err = authapi.RoleAdd(context.TODO(), "test-role") - if err != rpctypes.ErrRoleAlreadyExist { - t.Fatalf("expected %v, got %v", rpctypes.ErrRoleAlreadyExist, err) - } - - _, err = authapi.RoleAdd(context.TODO(), "") - if err != rpctypes.ErrRoleEmpty { - t.Fatalf("expected %v, got %v", rpctypes.ErrRoleEmpty, err) - } -}