Merge pull request #13819 from endocrimes/dani/auth_test.go

migrate e2e/users tests to common framework
This commit is contained in:
Marek Siarkowicz 2022-04-06 16:02:46 +02:00 committed by GitHub
commit c4d055fe7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 505 additions and 209 deletions

366
tests/common/user_test.go Normal file
View File

@ -0,0 +1,366 @@
// 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 (
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/tests/v3/framework/config"
"go.etcd.io/etcd/tests/v3/framework/testutils"
)
func TestUserAdd_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 {
nestedCases := []struct {
name string
username string
password string
noPassword bool
expectedError string
}{
{
name: "empty_username_not_allowed",
username: "",
password: "foobar",
// Very Vague error expectation because the CLI and the API return very
// different error structures.
expectedError: "user name",
},
{
// Can create a user with no password, restricted to CN auth
name: "no_password_with_noPassword_set",
username: "foo",
password: "",
noPassword: true,
},
{
// Can create a user with no password, but not restricted to CN auth
name: "no_password_without_noPassword_set",
username: "foo",
password: "",
noPassword: false,
},
{
name: "regular_user_with_password",
username: "foo",
password: "bar",
},
}
for _, nc := range nestedCases {
t.Run(tc.name+"/"+nc.name, func(t *testing.T) {
clus := testRunner.NewCluster(t, tc.config)
defer clus.Close()
cc := clus.Client()
testutils.ExecuteWithTimeout(t, 10*time.Second, func() {
resp, err := cc.UserAdd(nc.username, nc.password, config.UserAddOptions{NoPassword: nc.noPassword})
if nc.expectedError != "" {
if err != nil {
assert.Contains(t, err.Error(), nc.expectedError)
return
}
t.Fatalf("expected user creation to fail")
}
if err != nil {
t.Fatalf("expected no error, err: %v", err)
}
if resp == nil {
t.Fatalf("unexpected nil response to successful user creation")
}
})
})
}
}
}
func TestUserAdd_DuplicateUserNotAllowed(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() {
user := "barb"
password := "rhubarb"
_, err := cc.UserAdd(user, password, config.UserAddOptions{})
if err != nil {
t.Fatalf("first user creation should succeed, err: %v", err)
}
_, err = cc.UserAdd(user, password, config.UserAddOptions{})
if err == nil {
t.Fatalf("duplicate user creation should fail")
}
assert.Contains(t, err.Error(), "etcdserver: user name already exists")
})
})
}
}
func TestUserList(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() {
// No Users Yet
resp, err := cc.UserList()
if err != nil {
t.Fatalf("user listing should succeed, err: %v", err)
}
if len(resp.Users) != 0 {
t.Fatalf("expected no pre-existing users, found: %q", resp.Users)
}
user := "barb"
password := "rhubarb"
_, err = cc.UserAdd(user, password, config.UserAddOptions{})
if err != nil {
t.Fatalf("user creation should succeed, err: %v", err)
}
// Users!
resp, err = cc.UserList()
if err != nil {
t.Fatalf("user listing should succeed, err: %v", err)
}
if len(resp.Users) != 1 {
t.Fatalf("expected one user, found: %q", resp.Users)
}
})
})
}
}
func TestUserDelete(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() {
user := "barb"
password := "rhubarb"
_, err := cc.UserAdd(user, password, config.UserAddOptions{})
if err != nil {
t.Fatalf("user creation should succeed, err: %v", err)
}
resp, err := cc.UserList()
if err != nil {
t.Fatalf("user listing should succeed, err: %v", err)
}
if len(resp.Users) != 1 {
t.Fatalf("expected one user, found: %q", resp.Users)
}
// Delete barb, sorry barb!
_, err = cc.UserDelete(user)
if err != nil {
t.Fatalf("user deletion should succeed at first, err: %v", err)
}
resp, err = cc.UserList()
if err != nil {
t.Fatalf("user listing should succeed, err: %v", err)
}
if len(resp.Users) != 0 {
t.Fatalf("expected no users after deletion, found: %q", resp.Users)
}
// Try to delete barb again
_, err = cc.UserDelete(user)
if err == nil {
t.Fatalf("deleting a non-existent user should fail")
}
assert.Contains(t, err.Error(), "user name not found")
})
})
}
}
func TestUserChangePassword(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() {
user := "barb"
password := "rhubarb"
newPassword := "potato"
_, err := cc.UserAdd(user, password, config.UserAddOptions{})
if err != nil {
t.Fatalf("user creation should succeed, err: %v", err)
}
err = cc.UserChangePass(user, newPassword)
if err != nil {
t.Fatalf("user password change should succeed, err: %v", err)
}
err = cc.UserChangePass("non-existent-user", newPassword)
if err == nil {
t.Fatalf("user password change for non-existent user should fail")
}
assert.Contains(t, err.Error(), "user name not found")
})
})
}
}

View File

@ -1305,3 +1305,24 @@ func ctlV3EndpointHealth(cx ctlCtx) error {
}
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...)
proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap)
if err != nil {
return err
}
defer proc.Close()
// Send 'stdIn' strings as input.
for _, s := range stdIn {
if err = proc.Send(s + "\r"); err != nil {
return err
}
}
_, err = proc.Expect(expStr)
return err
}

View File

@ -1,209 +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 e2e
import (
"testing"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)
func TestCtlV3UserAdd(t *testing.T) { testCtl(t, userAddTest) }
func TestCtlV3UserAddNoTLS(t *testing.T) { testCtl(t, userAddTest, withCfg(*e2e.NewConfigNoTLS())) }
func TestCtlV3UserAddClientTLS(t *testing.T) {
testCtl(t, userAddTest, withCfg(*e2e.NewConfigClientTLS()))
}
func TestCtlV3UserAddPeerTLS(t *testing.T) { testCtl(t, userAddTest, withCfg(*e2e.NewConfigPeerTLS())) }
func TestCtlV3UserAddTimeout(t *testing.T) { testCtl(t, userAddTest, withDialTimeout(0)) }
func TestCtlV3UserAddClientAutoTLS(t *testing.T) {
testCtl(t, userAddTest, withCfg(*e2e.NewConfigClientAutoTLS()))
}
func TestCtlV3UserList(t *testing.T) { testCtl(t, userListTest) }
func TestCtlV3UserListNoTLS(t *testing.T) { testCtl(t, userListTest, withCfg(*e2e.NewConfigNoTLS())) }
func TestCtlV3UserListClientTLS(t *testing.T) {
testCtl(t, userListTest, withCfg(*e2e.NewConfigClientTLS()))
}
func TestCtlV3UserListPeerTLS(t *testing.T) {
testCtl(t, userListTest, withCfg(*e2e.NewConfigPeerTLS()))
}
func TestCtlV3UserListClientAutoTLS(t *testing.T) {
testCtl(t, userListTest, withCfg(*e2e.NewConfigClientAutoTLS()))
}
func TestCtlV3UserDelete(t *testing.T) { testCtl(t, userDelTest) }
func TestCtlV3UserDeleteNoTLS(t *testing.T) { testCtl(t, userDelTest, withCfg(*e2e.NewConfigNoTLS())) }
func TestCtlV3UserDeleteClientTLS(t *testing.T) {
testCtl(t, userDelTest, withCfg(*e2e.NewConfigClientTLS()))
}
func TestCtlV3UserDeletePeerTLS(t *testing.T) {
testCtl(t, userDelTest, withCfg(*e2e.NewConfigPeerTLS()))
}
func TestCtlV3UserDeleteClientAutoTLS(t *testing.T) {
testCtl(t, userDelTest, withCfg(*e2e.NewConfigClientAutoTLS()))
}
func TestCtlV3UserPasswd(t *testing.T) { testCtl(t, userPasswdTest) }
func TestCtlV3UserPasswdNoTLS(t *testing.T) {
testCtl(t, userPasswdTest, withCfg(*e2e.NewConfigNoTLS()))
}
func TestCtlV3UserPasswdClientTLS(t *testing.T) {
testCtl(t, userPasswdTest, withCfg(*e2e.NewConfigClientTLS()))
}
func TestCtlV3UserPasswdPeerTLS(t *testing.T) {
testCtl(t, userPasswdTest, withCfg(*e2e.NewConfigPeerTLS()))
}
func TestCtlV3UserPasswdClientAutoTLS(t *testing.T) {
testCtl(t, userPasswdTest, withCfg(*e2e.NewConfigClientAutoTLS()))
}
type userCmdDesc struct {
args []string
expectedStr string
stdIn []string
}
func userAddTest(cx ctlCtx) {
cmdSet := []userCmdDesc{
// Adds a user name.
{
args: []string{"add", "username", "--interactive=false"},
expectedStr: "User username created",
stdIn: []string{"password"},
},
// Adds a user name using the usertest:password syntax.
{
args: []string{"add", "usertest:password"},
expectedStr: "User usertest created",
stdIn: []string{},
},
// Tries to add a user with empty username.
{
args: []string{"add", ":password"},
expectedStr: "empty user name is not allowed",
stdIn: []string{},
},
// Tries to add a user name that already exists.
{
args: []string{"add", "username", "--interactive=false"},
expectedStr: "user name already exists",
stdIn: []string{"password"},
},
// Adds a user without password.
{
args: []string{"add", "userwopasswd", "--no-password"},
expectedStr: "User userwopasswd created",
stdIn: []string{},
},
}
for i, cmd := range cmdSet {
if err := ctlV3User(cx, cmd.args, cmd.expectedStr, cmd.stdIn); err != nil {
if cx.dialTimeout > 0 && !isGRPCTimedout(err) {
cx.t.Fatalf("userAddTest #%d: ctlV3User error (%v)", i, err)
}
}
}
}
func userListTest(cx ctlCtx) {
cmdSet := []userCmdDesc{
// Adds a user name.
{
args: []string{"add", "username", "--interactive=false"},
expectedStr: "User username created",
stdIn: []string{"password"},
},
// List user name
{
args: []string{"list"},
expectedStr: "username",
},
}
for i, cmd := range cmdSet {
if err := ctlV3User(cx, cmd.args, cmd.expectedStr, cmd.stdIn); err != nil {
cx.t.Fatalf("userListTest #%d: ctlV3User error (%v)", i, err)
}
}
}
func userDelTest(cx ctlCtx) {
cmdSet := []userCmdDesc{
// Adds a user name.
{
args: []string{"add", "username", "--interactive=false"},
expectedStr: "User username created",
stdIn: []string{"password"},
},
// Deletes the user name just added.
{
args: []string{"delete", "username"},
expectedStr: "User username deleted",
},
// Deletes a user name that is not present.
{
args: []string{"delete", "username"},
expectedStr: "user name not found",
},
}
for i, cmd := range cmdSet {
if err := ctlV3User(cx, cmd.args, cmd.expectedStr, cmd.stdIn); err != nil {
cx.t.Fatalf("userDelTest #%d: ctlV3User error (%v)", i, err)
}
}
}
func userPasswdTest(cx ctlCtx) {
cmdSet := []userCmdDesc{
// Adds a user name.
{
args: []string{"add", "username", "--interactive=false"},
expectedStr: "User username created",
stdIn: []string{"password"},
},
// Changes the password.
{
args: []string{"passwd", "username", "--interactive=false"},
expectedStr: "Password updated",
stdIn: []string{"password1"},
},
}
for i, cmd := range cmdSet {
if err := ctlV3User(cx, cmd.args, cmd.expectedStr, cmd.stdIn); err != nil {
cx.t.Fatalf("userPasswdTest #%d: ctlV3User error (%v)", i, err)
}
}
}
func ctlV3User(cx ctlCtx, args []string, expStr string, stdIn []string) error {
cmdArgs := append(cx.PrefixArgs(), "user")
cmdArgs = append(cmdArgs, args...)
proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap)
if err != nil {
return err
}
defer proc.Close()
// Send 'stdIn' strings as input.
for _, s := range stdIn {
if err = proc.Send(s + "\r"); err != nil {
return err
}
}
_, err = proc.Expect(expStr)
return err
}

View File

@ -55,3 +55,7 @@ type DefragOption struct {
type LeaseOption struct {
WithAttachedKeys bool
}
type UserAddOptions struct {
NoPassword bool
}

View File

@ -378,3 +378,90 @@ func (ctl *EtcdctlV3) AlarmDisarm(_ *clientv3.AlarmMember) (*clientv3.AlarmRespo
err = json.Unmarshal([]byte(line), &resp)
return &resp, err
}
func (ctl *EtcdctlV3) UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) {
args := ctl.cmdArgs()
args = append(args, "user", "add")
if password == "" {
args = append(args, name)
} else {
args = append(args, fmt.Sprintf("%s:%s", name, password))
}
if opts.NoPassword {
args = append(args, "--no-password")
}
args = append(args, "--interactive=false", "-w", "json")
cmd, err := SpawnCmd(args, nil)
if err != nil {
return nil, err
}
// If no password is provided, and NoPassword isn't set, the CLI will always
// wait for a password, send an enter in this case for an "empty" password.
if !opts.NoPassword && password == "" {
err := cmd.Send("\n")
if err != nil {
return nil, err
}
}
var resp clientv3.AuthUserAddResponse
line, err := cmd.Expect("header")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(line), &resp)
return &resp, err
}
func (ctl *EtcdctlV3) UserList() (*clientv3.AuthUserListResponse, error) {
args := ctl.cmdArgs()
args = append(args, "user", "list", "-w", "json")
cmd, err := SpawnCmd(args, nil)
if err != nil {
return nil, err
}
var resp clientv3.AuthUserListResponse
line, err := cmd.Expect("header")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(line), &resp)
return &resp, err
}
func (ctl *EtcdctlV3) UserDelete(name string) (*clientv3.AuthUserDeleteResponse, error) {
args := ctl.cmdArgs()
args = append(args, "user", "delete", name, "-w", "json")
cmd, err := SpawnCmd(args, nil)
if err != nil {
return nil, err
}
var resp clientv3.AuthUserDeleteResponse
line, err := cmd.Expect("header")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(line), &resp)
return &resp, err
}
func (ctl *EtcdctlV3) UserChangePass(user, newPass string) error {
args := ctl.cmdArgs()
args = append(args, "user", "passwd", user, "--interactive=false")
cmd, err := SpawnCmd(args, nil)
if err != nil {
return err
}
err = cmd.Send(newPass + "\n")
if err != nil {
return err
}
_, err = cmd.Expect("Password updated")
return err
}

View File

@ -289,3 +289,25 @@ func (c integrationClient) LeaseRevoke(id clientv3.LeaseID) (*clientv3.LeaseRevo
return c.Client.Revoke(ctx, id)
}
func (c integrationClient) UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) {
ctx := context.Background()
return c.Client.UserAddWithOptions(ctx, name, password, &clientv3.UserAddOptions{
NoPassword: opts.NoPassword,
})
}
func (c integrationClient) UserList() (*clientv3.AuthUserListResponse, error) {
ctx := context.Background()
return c.Client.UserList(ctx)
}
func (c integrationClient) UserDelete(name string) (*clientv3.AuthUserDeleteResponse, error) {
ctx := context.Background()
return c.Client.UserDelete(ctx, name)
}
func (c integrationClient) UserChangePass(user, newPass string) error {
_, err := c.Client.UserChangePassword(context.Background(), user, newPass)
return err
}

View File

@ -55,4 +55,9 @@ type Client interface {
LeaseList() (*clientv3.LeaseLeasesResponse, error)
LeaseKeepAliveOnce(id clientv3.LeaseID) (*clientv3.LeaseKeepAliveResponse, error)
LeaseRevoke(id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error)
UserAdd(name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error)
UserList() (*clientv3.AuthUserListResponse, error)
UserDelete(name string) (*clientv3.AuthUserDeleteResponse, error)
UserChangePass(user, newPass string) error
}