fleet/server/service/service_users_test.go
Zachary Wasserman 0502412e15 Move live query operations from MySQL to Redis
This change optimizes live queries by pushing the computation of query
targets to the creation time of the query, and efficiently caching the
targets in Redis. This results in a huge performance improvement at both
steady-state, and when running live queries.

- Live queries are stored using a bitfield in Redis, and takes
advantage of bitfield operations to be extremely efficient.

- Only run Redis live query test when REDIS_TEST is set in environment

- Ensure that live queries are only sent to hosts when there is a client
listening for results. Addresses an existing issue in Fleet along with
appropriate cleanup for the refactored live query backend.
2020-07-21 14:05:46 -07:00

745 lines
20 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"github.com/kolide/fleet/server/config"
"github.com/kolide/fleet/server/contexts/viewer"
"github.com/kolide/fleet/server/datastore/inmem"
"github.com/kolide/fleet/server/kolide"
"github.com/WatchBeam/clock"
"github.com/kolide/fleet/server/mock"
pkg_errors "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthenticatedUser(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
createTestUsers(t, ds)
svc, err := newTestService(ds, nil, nil)
assert.Nil(t, err)
admin1, err := ds.User("admin1")
assert.Nil(t, err)
admin1Session, err := ds.NewSession(&kolide.Session{
UserID: admin1.ID,
Key: "admin1",
})
assert.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin1, Session: admin1Session})
user, err := svc.AuthenticatedUser(ctx)
assert.Nil(t, err)
assert.Equal(t, user, admin1)
}
func TestModifyUserEmail(t *testing.T) {
user := &kolide.User{
ID: 3,
Admin: false,
Email: "foo@bar.com",
Enabled: true,
}
user.SetPassword("password", 10, 10)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(id uint, em, tk string) error {
return nil
}
ms.UserByIDFunc = func(id uint) (*kolide.User, error) {
return user, nil
}
ms.AppConfigFunc = func() (*kolide.AppConfig, error) {
config := &kolide.AppConfig{
SMTPPort: 1025,
SMTPConfigured: true,
SMTPServer: "127.0.0.1",
SMTPSenderAddress: "xxx@kolide.co",
SMTPAuthenticationType: kolide.AuthTypeNone,
}
return config, nil
}
ms.SaveUserFunc = func(u *kolide.User) error {
// verify this isn't changed yet
assert.Equal(t, "foo@bar.com", u.Email)
// verify is changed per bug 1123
assert.Equal(t, "minion", u.Position)
return nil
}
svc, err := newTestService(ms, nil, nil)
require.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := kolide.UserPayload{
Email: stringPtr("zip@zap.com"),
Password: stringPtr("password"),
Position: stringPtr("minion"),
}
_, err = svc.ModifyUser(ctx, 3, payload)
require.Nil(t, err)
assert.True(t, ms.PendingEmailChangeFuncInvoked)
assert.True(t, ms.SaveUserFuncInvoked)
}
func TestModifyUserCannotUpdateAdminEnabled(t *testing.T) {
// The modify user function should not be able to update the admin or
// enabled status of a user. These should only be updated explicitly
// through the ChangeUserAdmin and ChangeUserEnabled functions.
user := &kolide.User{
ID: 3,
Admin: false,
Email: "foo@bar.com",
Enabled: true,
}
user.SetPassword("password", 10, 10)
ms := new(mock.Store)
ms.UserByIDFunc = func(id uint) (*kolide.User, error) {
return user, nil
}
ms.SaveUserFunc = func(u *kolide.User) error {
assert.Equal(t, false, u.Admin, "should not be able to update admin status!")
assert.Equal(t, true, u.Enabled, "should not be able to update enabled status!")
return nil
}
svc, err := newTestService(ms, nil, nil)
require.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := kolide.UserPayload{
Admin: boolPtr(true),
Enabled: boolPtr(false),
}
_, err = svc.ModifyUser(ctx, 3, payload)
require.Nil(t, err)
assert.True(t, ms.SaveUserFuncInvoked)
}
func TestModifyUserEmailNoPassword(t *testing.T) {
user := &kolide.User{
ID: 3,
Admin: true,
Email: "foo@bar.com",
Enabled: true,
}
user.SetPassword("password", 10, 10)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(id uint, em, tk string) error {
return nil
}
ms.UserByIDFunc = func(id uint) (*kolide.User, error) {
return user, nil
}
ms.AppConfigFunc = func() (*kolide.AppConfig, error) {
config := &kolide.AppConfig{
SMTPPort: 1025,
SMTPConfigured: true,
SMTPServer: "127.0.0.1",
SMTPSenderAddress: "xxx@kolide.co",
SMTPAuthenticationType: kolide.AuthTypeNone,
}
return config, nil
}
ms.SaveUserFunc = func(u *kolide.User) error {
return nil
}
svc, err := newTestService(ms, nil, nil)
require.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := kolide.UserPayload{
Email: stringPtr("zip@zap.com"),
// NO PASSWORD
// Password: stringPtr("password"),
}
_, err = svc.ModifyUser(ctx, 3, payload)
require.NotNil(t, err)
invalid, ok := err.(*invalidArgumentError)
require.True(t, ok)
require.Len(t, *invalid, 1)
assert.Equal(t, "cannot be empty if email is changed", (*invalid)[0].reason)
assert.False(t, ms.PendingEmailChangeFuncInvoked)
assert.False(t, ms.SaveUserFuncInvoked)
}
func TestModifyAdminUserEmailNoPassword(t *testing.T) {
user := &kolide.User{
ID: 3,
Admin: true,
Email: "foo@bar.com",
Enabled: true,
}
user.SetPassword("password", 10, 10)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(id uint, em, tk string) error {
return nil
}
ms.UserByIDFunc = func(id uint) (*kolide.User, error) {
return user, nil
}
ms.AppConfigFunc = func() (*kolide.AppConfig, error) {
config := &kolide.AppConfig{
SMTPPort: 1025,
SMTPConfigured: true,
SMTPServer: "127.0.0.1",
SMTPSenderAddress: "xxx@kolide.co",
SMTPAuthenticationType: kolide.AuthTypeNone,
}
return config, nil
}
ms.SaveUserFunc = func(u *kolide.User) error {
return nil
}
svc, err := newTestService(ms, nil, nil)
require.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := kolide.UserPayload{
Email: stringPtr("zip@zap.com"),
// NO PASSWORD
// Password: stringPtr("password"),
}
_, err = svc.ModifyUser(ctx, 3, payload)
require.NotNil(t, err)
invalid, ok := err.(*invalidArgumentError)
require.True(t, ok)
require.Len(t, *invalid, 1)
assert.Equal(t, "cannot be empty if email is changed", (*invalid)[0].reason)
assert.False(t, ms.PendingEmailChangeFuncInvoked)
assert.False(t, ms.SaveUserFuncInvoked)
}
func TestModifyAdminUserEmailPassword(t *testing.T) {
user := &kolide.User{
ID: 3,
Admin: true,
Email: "foo@bar.com",
Enabled: true,
}
user.SetPassword("password", 10, 10)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(id uint, em, tk string) error {
return nil
}
ms.UserByIDFunc = func(id uint) (*kolide.User, error) {
return user, nil
}
ms.AppConfigFunc = func() (*kolide.AppConfig, error) {
config := &kolide.AppConfig{
SMTPPort: 1025,
SMTPConfigured: true,
SMTPServer: "127.0.0.1",
SMTPSenderAddress: "xxx@kolide.co",
SMTPAuthenticationType: kolide.AuthTypeNone,
}
return config, nil
}
ms.SaveUserFunc = func(u *kolide.User) error {
return nil
}
svc, err := newTestService(ms, nil, nil)
require.Nil(t, err)
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := kolide.UserPayload{
Email: stringPtr("zip@zap.com"),
Password: stringPtr("password"),
}
_, err = svc.ModifyUser(ctx, 3, payload)
require.Nil(t, err)
assert.True(t, ms.PendingEmailChangeFuncInvoked)
assert.True(t, ms.SaveUserFuncInvoked)
}
func TestRequestPasswordReset(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
createTestAppConfig(t, ds)
createTestUsers(t, ds)
admin1, err := ds.User("admin1")
assert.Nil(t, err)
user1, err := ds.User("user1")
assert.Nil(t, err)
var defaultEmailFn = func(e kolide.Email) error {
return nil
}
var errEmailFn = func(e kolide.Email) error {
return errors.New("test err")
}
svc := service{
ds: ds,
config: config.TestConfig(),
}
var requestPasswordResetTests = []struct {
email string
emailFn func(e kolide.Email) error
wantErr error
user *kolide.User
vc *viewer.Viewer
}{
{
email: admin1.Email,
emailFn: defaultEmailFn,
user: admin1,
vc: &viewer.Viewer{User: admin1},
},
{
email: admin1.Email,
emailFn: defaultEmailFn,
user: admin1,
vc: nil,
},
{
email: user1.Email,
emailFn: defaultEmailFn,
user: user1,
vc: &viewer.Viewer{User: admin1},
},
{
email: admin1.Email,
emailFn: errEmailFn,
user: user1,
vc: nil,
wantErr: errors.New("test err"),
},
}
for _, tt := range requestPasswordResetTests {
t.Run("", func(t *testing.T) {
ctx := context.Background()
if tt.vc != nil {
ctx = viewer.NewContext(ctx, *tt.vc)
}
mailer := &mockMailService{SendEmailFn: tt.emailFn}
svc.mailService = mailer
serviceErr := svc.RequestPasswordReset(ctx, tt.email)
assert.Equal(t, tt.wantErr, serviceErr)
assert.True(t, mailer.Invoked, "email should be sent if vc is not admin")
if serviceErr == nil {
req, err := ds.FindPassswordResetsByUserID(tt.user.ID)
assert.Nil(t, err)
assert.NotEmpty(t, req, "user should have at least one password reset request")
}
})
}
}
func TestCreateUser(t *testing.T) {
ds, _ := inmem.New(config.TestConfig())
svc, _ := newTestService(ds, nil, nil)
invites := setupInvites(t, ds, []string{"admin2@example.com"})
ctx := context.Background()
var createUserTests = []struct {
Username *string
Password *string
Email *string
NeedsPasswordReset *bool
Admin *bool
InviteToken *string
wantErr error
}{
{
Username: stringPtr("admin2"),
Password: stringPtr("foobarbaz1234!"),
InviteToken: &invites["admin2@example.com"].Token,
wantErr: &invalidArgumentError{invalidArgument{name: "email", reason: "missing required argument"}},
},
{
Username: stringPtr("admin2"),
Password: stringPtr("foobarbaz1234!"),
Email: stringPtr("admin2@example.com"),
wantErr: &invalidArgumentError{invalidArgument{name: "invite_token", reason: "missing required argument"}},
},
{
Username: stringPtr("admin2"),
Password: stringPtr("foobarbaz1234!"),
Email: stringPtr("admin2@example.com"),
NeedsPasswordReset: boolPtr(true),
Admin: boolPtr(false),
InviteToken: &invites["admin2@example.com"].Token,
},
{ // should return ErrNotFound because the invite is deleted
// after a user signs up
Username: stringPtr("admin2"),
Password: stringPtr("foobarbaz1234!"),
Email: stringPtr("admin2@example.com"),
NeedsPasswordReset: boolPtr(true),
Admin: boolPtr(false),
InviteToken: &invites["admin2@example.com"].Token,
wantErr: errors.New("Invite with token admin2@example.com was not found in the datastore"),
},
{
Username: stringPtr("admin3"),
Password: stringPtr("foobarbaz1234!"),
Email: &invites["expired"].Email,
NeedsPasswordReset: boolPtr(true),
Admin: boolPtr(false),
InviteToken: &invites["expired"].Token,
wantErr: &invalidArgumentError{{name: "invite_token", reason: "Invite token has expired."}},
},
{
Username: stringPtr("@admin2"),
Password: stringPtr("foobarbaz1234!"),
Email: stringPtr("admin2@example.com"),
NeedsPasswordReset: boolPtr(true),
Admin: boolPtr(false),
InviteToken: &invites["admin2@example.com"].Token,
wantErr: &invalidArgumentError{invalidArgument{name: "username", reason: "'@' character not allowed in usernames"}},
},
}
for _, tt := range createUserTests {
t.Run("", func(t *testing.T) {
payload := kolide.UserPayload{
Username: tt.Username,
Password: tt.Password,
Email: tt.Email,
Admin: tt.Admin,
InviteToken: tt.InviteToken,
}
user, err := svc.NewUser(ctx, payload)
if tt.wantErr != nil {
require.Equal(t, tt.wantErr.Error(), err.Error())
}
if err != nil {
// skip rest of the test if error is not nil
return
}
assert.NotZero(t, user.ID)
err = user.ValidatePassword(*tt.Password)
assert.Nil(t, err)
err = user.ValidatePassword("different_password")
assert.NotNil(t, err)
assert.Equal(t, user.Admin, *tt.Admin)
})
}
}
func setupInvites(t *testing.T, ds kolide.Datastore, emails []string) map[string]*kolide.Invite {
invites := make(map[string]*kolide.Invite)
users := createTestUsers(t, ds)
mockClock := clock.NewMockClock()
for _, e := range emails {
invite, err := ds.NewInvite(&kolide.Invite{
InvitedBy: users["admin1"].ID,
Token: e,
Email: e,
UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
CreateTimestamp: kolide.CreateTimestamp{
CreatedAt: mockClock.Now(),
},
},
})
require.Nil(t, err)
invites[e] = invite
}
// add an expired invitation
invite, err := ds.NewInvite(&kolide.Invite{
InvitedBy: users["admin1"].ID,
Token: "expired",
Email: "expiredinvite@gmail.com",
UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
CreateTimestamp: kolide.CreateTimestamp{
CreatedAt: mockClock.Now().AddDate(-1, 0, 0),
},
},
})
require.Nil(t, err)
invites["expired"] = invite
return invites
}
func TestChangePassword(t *testing.T) {
ds, _ := inmem.New(config.TestConfig())
svc, _ := newTestService(ds, nil, nil)
users := createTestUsers(t, ds)
var passwordChangeTests = []struct {
user kolide.User
oldPassword string
newPassword string
anyErr bool
wantErr error
}{
{ // all good
user: users["admin1"],
oldPassword: "foobarbaz1234!",
newPassword: "12345cat!",
},
{ // prevent password reuse
user: users["admin1"],
oldPassword: "12345cat!",
newPassword: "foobarbaz1234!",
wantErr: &invalidArgumentError{invalidArgument{name: "new_password", reason: "cannot reuse old password"}},
},
{ // all good
user: users["user1"],
oldPassword: "foobarbaz1234!",
newPassword: "newpassa1234!",
},
{ // bad old password
user: users["user1"],
oldPassword: "wrong_password",
newPassword: "12345cat!",
anyErr: true,
},
{ // missing old password
newPassword: "123cataaa!",
wantErr: &invalidArgumentError{invalidArgument{name: "old_password", reason: "cannot be empty"}},
},
{ // missing new password
oldPassword: "abcd",
wantErr: &invalidArgumentError{
{name: "new_password", reason: "cannot be empty"},
{name: "new_password", reason: "password does not meet validation requirements"},
},
},
}
for _, tt := range passwordChangeTests {
t.Run("", func(t *testing.T) {
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &tt.user})
err := svc.ChangePassword(ctx, tt.oldPassword, tt.newPassword)
if tt.anyErr {
require.NotNil(t, err)
} else if tt.wantErr != nil {
require.Equal(t, tt.wantErr, pkg_errors.Cause(err))
} else {
require.Nil(t, err)
}
if err != nil {
return
}
// Attempt login after successful change
_, _, err = svc.Login(context.Background(), tt.user.Username, tt.newPassword)
require.Nil(t, err, "should be able to login with new password")
})
}
}
func TestResetPassword(t *testing.T) {
ds, _ := inmem.New(config.TestConfig())
svc, _ := newTestService(ds, nil, nil)
createTestUsers(t, ds)
var passwordResetTests = []struct {
token string
newPassword string
wantErr error
}{
{ // all good
token: "abcd",
newPassword: "123cat!",
},
{ // prevent reuse
token: "abcd",
newPassword: "123cat!",
wantErr: &invalidArgumentError{invalidArgument{name: "new_password", reason: "cannot reuse old password"}},
},
{ // bad token
token: "dcbaz",
newPassword: "123cat!",
wantErr: errors.New("PasswordResetRequest was not found in the datastore"),
},
{ // missing token
newPassword: "123cat!",
wantErr: &invalidArgumentError{invalidArgument{name: "token", reason: "cannot be empty field"}},
},
{ // missing password
token: "abcd",
wantErr: &invalidArgumentError{
{name: "new_password", reason: "cannot be empty field"},
{name: "new_password", reason: "password does not meet validation requirements"},
},
},
}
for _, tt := range passwordResetTests {
t.Run("", func(t *testing.T) {
ctx := context.Background()
request := &kolide.PasswordResetRequest{
UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
CreateTimestamp: kolide.CreateTimestamp{
CreatedAt: time.Now(),
},
UpdateTimestamp: kolide.UpdateTimestamp{
UpdatedAt: time.Now(),
},
},
ExpiresAt: time.Now().Add(time.Hour * 24),
UserID: 1,
Token: "abcd",
}
_, err := ds.NewPasswordResetRequest(request)
assert.Nil(t, err)
serr := svc.ResetPassword(ctx, tt.token, tt.newPassword)
if tt.wantErr != nil {
assert.Equal(t, tt.wantErr.Error(), pkg_errors.Cause(serr).Error())
} else {
assert.Nil(t, serr)
}
})
}
}
func TestRequirePasswordReset(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
createTestUsers(t, ds)
for _, tt := range testUsers {
t.Run(tt.Username, func(t *testing.T) {
user, err := ds.User(tt.Username)
require.Nil(t, err)
var sessions []*kolide.Session
ctx := context.Background()
// Log user in
if tt.Enabled {
_, _, err = svc.Login(ctx, tt.Username, tt.PlaintextPassword)
require.Nil(t, err, "login unsuccessful")
sessions, err = svc.GetInfoAboutSessionsForUser(ctx, user.ID)
require.Nil(t, err)
require.Len(t, sessions, 1, "user should have one session")
}
// Reset and verify sessions destroyed
retUser, err := svc.RequirePasswordReset(ctx, user.ID, true)
require.Nil(t, err)
assert.True(t, retUser.AdminForcedPasswordReset)
checkUser, err := ds.User(tt.Username)
require.Nil(t, err)
assert.True(t, checkUser.AdminForcedPasswordReset)
sessions, err = svc.GetInfoAboutSessionsForUser(ctx, user.ID)
require.Nil(t, err)
require.Len(t, sessions, 0, "sessions should be destroyed")
// try undo
retUser, err = svc.RequirePasswordReset(ctx, user.ID, false)
require.Nil(t, err)
assert.False(t, retUser.AdminForcedPasswordReset)
checkUser, err = ds.User(tt.Username)
require.Nil(t, err)
assert.False(t, checkUser.AdminForcedPasswordReset)
})
}
}
func TestPerformRequiredPasswordReset(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
createTestUsers(t, ds)
for _, tt := range testUsers {
t.Run(tt.Username, func(t *testing.T) {
if !tt.Enabled {
return
}
user, err := ds.User(tt.Username)
require.Nil(t, err)
ctx := context.Background()
_, err = svc.RequirePasswordReset(ctx, user.ID, true)
require.Nil(t, err)
// should error when not logged in
_, err = svc.PerformRequiredPasswordReset(ctx, "new_pass")
require.NotNil(t, err)
session, err := ds.NewSession(&kolide.Session{
UserID: user.ID,
})
require.Nil(t, err)
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user, Session: session})
// should error when reset not required
_, err = svc.RequirePasswordReset(ctx, user.ID, false)
require.Nil(t, err)
_, err = svc.PerformRequiredPasswordReset(ctx, "new_pass")
require.NotNil(t, err)
_, err = svc.RequirePasswordReset(ctx, user.ID, true)
require.Nil(t, err)
// should error when using same password
_, err = svc.PerformRequiredPasswordReset(ctx, tt.PlaintextPassword)
require.NotNil(t, err)
// should succeed with good new password
u, err := svc.PerformRequiredPasswordReset(ctx, "new_pass")
require.Nil(t, err)
assert.False(t, u.AdminForcedPasswordReset)
ctx = context.Background()
// Now user should be able to login with new password
u, _, err = svc.Login(ctx, tt.Username, "new_pass")
require.Nil(t, err)
assert.False(t, u.AdminForcedPasswordReset)
})
}
}
func TestUserPasswordRequirements(t *testing.T) {
var passwordTests = []struct {
password string
wantErr bool
}{
{
password: "foobar",
wantErr: true,
},
{
password: "foobarbaz",
wantErr: true,
},
{
password: "foobarbaz!",
wantErr: true,
},
{
password: "foobarbaz!3",
},
}
for _, tt := range passwordTests {
t.Run(tt.password, func(t *testing.T) {
err := validatePasswordRequirements(tt.password)
if tt.wantErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}