mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved As part of #11035 , this PR implements the password check logic for session validation # How the Problems Are Solved - Add system config to default configuration of `domain` package for easy initialization. Intialize the system settings when Zitadel starts up - Add password hasher verify logic to the default configuration of `domain` package. Initialize it when Zitadel starts up. - Add settings repositories with their mocks - Implement the logic for doing a password check # Additional Context This is a cherry-picked PR + minor changes, coming from https://github.com/zitadel/zitadel/pull/11164 - Relates to #11035 - Depends on https://github.com/zitadel/zitadel/pull/11777
1215 lines
42 KiB
Go
1215 lines
42 KiB
Go
package domain_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/muhlemmer/gu"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/passwap"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/domain"
|
|
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
|
|
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/session"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func TestPasswordCheckCommand_Validate(t *testing.T) {
|
|
t.Parallel()
|
|
sessionGetErr := errors.New("session get error")
|
|
userGetErr := errors.New("user get error")
|
|
notFoundErr := database.NewNoRowFoundError(nil)
|
|
|
|
tt := []struct {
|
|
testName string
|
|
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
|
|
userRepo func(ctrl *gomock.Controller) domain.UserRepository
|
|
cmd *domain.PasswordCheckCommand
|
|
expectedError error
|
|
expectedUser domain.User
|
|
}{
|
|
{
|
|
testName: "when checkPassword is nil should return no error",
|
|
cmd: &domain.PasswordCheckCommand{},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
testName: "when session ID is not set should return error",
|
|
cmd: &domain.PasswordCheckCommand{CheckPassword: &domain.CheckPasswordType{Password: "test-password"}},
|
|
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-cRKWNx", "Errors.Missing.SessionID"),
|
|
},
|
|
{
|
|
testName: "when instance ID is not set should return error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-JnEtcJ", "Errors.Missing.InstanceID"),
|
|
},
|
|
{
|
|
testName: "when retrieving session fails should return error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(nil, sessionGetErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(sessionGetErr, "DOM-qAoQrg", "failed fetching session"),
|
|
},
|
|
{
|
|
testName: "when session not found should return not found error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(nil, notFoundErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-0XRmp8", "session not found"),
|
|
},
|
|
{
|
|
testName: "when session userID is empty should return precondition failed error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-hord0Z", "Errors.User.UserIDMissing"),
|
|
},
|
|
{
|
|
testName: "when retrieving user fails should return error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(nil, userGetErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(userGetErr, "DOM-nKD4Gq", "failed fetching user"),
|
|
},
|
|
{
|
|
testName: "when user not found should return not found error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(nil, notFoundErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-zxKosn", "Errors.User.NotFound"),
|
|
},
|
|
{
|
|
testName: "when user is not human should return precondition failed error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.User{
|
|
ID: "user-1",
|
|
Human: nil,
|
|
}, nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-ADhxAx", "user not human"),
|
|
},
|
|
{
|
|
testName: "when user is locked should return precondition failed error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.User{
|
|
ID: "user-1",
|
|
State: domain.UserStateLocked,
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
FailedAttempts: 5,
|
|
},
|
|
},
|
|
}, nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowPreconditionFailed(domain.NewPasswordVerificationError(5), "DOM-D804Sj", "Errors.User.Locked"),
|
|
},
|
|
{
|
|
testName: "when user password is not set should return precondition failed error",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.User{
|
|
ID: "user-1",
|
|
State: domain.UserStateActive,
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
Hash: "",
|
|
},
|
|
},
|
|
}, nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-gklgos", "Errors.User.Password.NotSet"),
|
|
},
|
|
{
|
|
testName: "when all validations pass should return no error and set user",
|
|
cmd: domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test-password"}),
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.Session{
|
|
ID: "session-1",
|
|
UserID: "user-1",
|
|
}, nil)
|
|
return repo
|
|
},
|
|
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
|
|
repo := domainmock.NewUserRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(
|
|
database.WithCondition(
|
|
idCondition,
|
|
),
|
|
)).
|
|
Times(1).
|
|
Return(&domain.User{
|
|
ID: "user-1",
|
|
State: domain.UserStateActive,
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
},
|
|
},
|
|
}, nil)
|
|
return repo
|
|
},
|
|
expectedUser: domain.User{
|
|
ID: "user-1",
|
|
State: domain.UserStateActive,
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given
|
|
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
|
|
ctrl := gomock.NewController(t)
|
|
|
|
opts := &domain.InvokeOpts{
|
|
Invoker: domain.NewTransactionInvoker(nil),
|
|
}
|
|
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
|
if tc.sessionRepo != nil {
|
|
domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts)
|
|
}
|
|
if tc.userRepo != nil {
|
|
domain.WithUserRepo(tc.userRepo(ctrl))(opts)
|
|
}
|
|
|
|
// Test
|
|
err := tc.cmd.Validate(ctx, opts)
|
|
|
|
// Verify
|
|
assert.ErrorIs(t, err, tc.expectedError)
|
|
assert.Equal(t, tc.expectedUser, tc.cmd.FetchedUser)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPasswordCheckCommand_GetPasswordCheckAndError(t *testing.T) {
|
|
t.Parallel()
|
|
otherErr := errors.New("some other error")
|
|
|
|
tt := []struct {
|
|
testName string
|
|
inputErr error
|
|
fetchedUser domain.User
|
|
expectedCheckType domain.VerificationType
|
|
expectedError error
|
|
}{
|
|
{
|
|
testName: "when error is nil should return CheckTypeSucceeded with no error",
|
|
inputErr: nil,
|
|
fetchedUser: domain.User{},
|
|
expectedCheckType: &domain.VerificationTypeSucceeded{},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
testName: "when error is password mismatch should return CheckTypeFailed with invalid argument error",
|
|
inputErr: passwap.ErrPasswordMismatch,
|
|
fetchedUser: domain.User{
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
FailedAttempts: 2,
|
|
},
|
|
},
|
|
},
|
|
expectedCheckType: &domain.VerificationTypeFailed{},
|
|
expectedError: zerrors.ThrowInvalidArgument(domain.NewPasswordVerificationError(3), "DOM-3gcfDV", "Errors.User.Password.Invalid"),
|
|
},
|
|
{
|
|
testName: "when error is other error should return CheckTypeFailed with internal error",
|
|
inputErr: otherErr,
|
|
fetchedUser: domain.User{
|
|
Human: &domain.HumanUser{
|
|
Password: domain.HumanPassword{
|
|
FailedAttempts: 0,
|
|
},
|
|
},
|
|
},
|
|
expectedCheckType: &domain.VerificationTypeFailed{},
|
|
expectedError: zerrors.ThrowInternal(otherErr, "DOM-xceNzI", "Errors.Internal"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given
|
|
cmd := domain.NewPasswordCheckCommand("session-1", "instance-1", nil, nil, &domain.CheckPasswordType{Password: "test"})
|
|
cmd.FetchedUser = tc.fetchedUser
|
|
|
|
// Test
|
|
checkType, err := cmd.GetPasswordCheckAndError(tc.inputErr)
|
|
|
|
// Verify
|
|
assert.IsType(t, tc.expectedCheckType, checkType)
|
|
assert.ErrorIs(t, err, tc.expectedError)
|
|
if tc.expectedError != nil {
|
|
checkFailed := checkType.(*domain.VerificationTypeFailed)
|
|
assert.NotZero(t, checkFailed.FailedAt)
|
|
} else {
|
|
checkSuccess := checkType.(*domain.VerificationTypeSucceeded)
|
|
assert.NotZero(t, checkSuccess.VerifiedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPasswordCheckCommand_GetPasswordCheckChanges(t *testing.T) {
|
|
t.Parallel()
|
|
listErr := errors.New("list error")
|
|
|
|
tt := []struct {
|
|
testName string
|
|
humanRepo func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository
|
|
lockoutRepo func(ctrl *gomock.Controller) domain.LockoutSettingsRepository
|
|
updatedHash string
|
|
checkType domain.VerificationType
|
|
cmd *domain.PasswordCheckCommand
|
|
|
|
expectedChanges int
|
|
expectedError error
|
|
expectedHashedPsw string
|
|
expectedUserLocked bool
|
|
}{
|
|
{
|
|
testName: "when check type succeeded with empty hash should return only check password change",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
},
|
|
updatedHash: "",
|
|
checkType: &domain.VerificationTypeSucceeded{},
|
|
expectedChanges: 1,
|
|
},
|
|
{
|
|
testName: "when check type succeeded with hash should return check password and set password changes",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
},
|
|
updatedHash: "new-hash",
|
|
checkType: &domain.VerificationTypeSucceeded{},
|
|
expectedChanges: 2,
|
|
expectedHashedPsw: "new-hash",
|
|
},
|
|
{
|
|
testName: "when check type failed and no lockout policy should return internal error",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
lockoutRepo: func(ctrl *gomock.Controller) domain.LockoutSettingsRepository {
|
|
repo := domainmock.NewLockoutSettingsRepo(ctrl)
|
|
|
|
instanceAndOrg := database.And(repo.InstanceIDCondition("instance-1"), repo.OrganizationIDCondition(gu.Ptr("org-1")))
|
|
orgNullOrEmpty := database.Or(repo.OrganizationIDCondition(nil), repo.OrganizationIDCondition(gu.Ptr("")))
|
|
onlyInstance := database.And(repo.InstanceIDCondition("instance-1"), orgNullOrEmpty)
|
|
orCondition := database.Or(instanceAndOrg, onlyInstance)
|
|
|
|
repo.EXPECT().
|
|
List(gomock.Any(), gomock.Any(),
|
|
dbmock.QueryOptions(database.WithCondition(orCondition)),
|
|
dbmock.QueryOptions(database.WithOrderByAscending(repo.OrganizationIDColumn(), repo.InstanceIDColumn())),
|
|
dbmock.QueryOptions(database.WithLimit(1)),
|
|
).Times(1).
|
|
Return(nil, nil)
|
|
|
|
return repo
|
|
},
|
|
updatedHash: "",
|
|
checkType: &domain.VerificationTypeFailed{},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
FetchedUser: domain.User{OrganizationID: "org-1"},
|
|
},
|
|
expectedError: zerrors.ThrowInternal(domain.NewRowsReturnedMismatchError(1, 0), "DOM-mmsrCt", "unexpected number of rows returned"),
|
|
},
|
|
{
|
|
testName: "when check type failed and get lockout policy returns error should return error",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
lockoutRepo: func(ctrl *gomock.Controller) domain.LockoutSettingsRepository {
|
|
repo := domainmock.NewLockoutSettingsRepo(ctrl)
|
|
|
|
instanceAndOrg := database.And(repo.InstanceIDCondition("instance-1"), repo.OrganizationIDCondition(gu.Ptr("org-1")))
|
|
orgNullOrEmpty := database.Or(repo.OrganizationIDCondition(nil), repo.OrganizationIDCondition(gu.Ptr("")))
|
|
onlyInstance := database.And(repo.InstanceIDCondition("instance-1"), orgNullOrEmpty)
|
|
orCondition := database.Or(instanceAndOrg, onlyInstance)
|
|
|
|
repo.EXPECT().
|
|
List(gomock.Any(), gomock.Any(),
|
|
dbmock.QueryOptions(database.WithCondition(orCondition)),
|
|
dbmock.QueryOptions(database.WithOrderByAscending(repo.OrganizationIDColumn(), repo.InstanceIDColumn())),
|
|
dbmock.QueryOptions(database.WithLimit(1)),
|
|
).Times(1).
|
|
Return(nil, listErr)
|
|
|
|
return repo
|
|
},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
FetchedUser: domain.User{OrganizationID: "org-1"},
|
|
},
|
|
updatedHash: "",
|
|
checkType: &domain.VerificationTypeFailed{},
|
|
expectedError: zerrors.ThrowInternal(listErr, "DOM-3B8Z6s", "failed fetching lockout settings"),
|
|
},
|
|
{
|
|
testName: "when check type failed with nil max attempts should return check type failed change",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
lockoutRepo: func(ctrl *gomock.Controller) domain.LockoutSettingsRepository {
|
|
repo := domainmock.NewLockoutSettingsRepo(ctrl)
|
|
|
|
instanceAndOrg := database.And(repo.InstanceIDCondition("instance-1"), repo.OrganizationIDCondition(gu.Ptr("org-1")))
|
|
orgNullOrEmpty := database.Or(repo.OrganizationIDCondition(nil), repo.OrganizationIDCondition(gu.Ptr("")))
|
|
onlyInstance := database.And(repo.InstanceIDCondition("instance-1"), orgNullOrEmpty)
|
|
orCondition := database.Or(instanceAndOrg, onlyInstance)
|
|
|
|
setting := &domain.LockoutSettings{
|
|
Settings: domain.Settings{},
|
|
LockoutSettingsAttributes: domain.LockoutSettingsAttributes{},
|
|
}
|
|
|
|
repo.EXPECT().
|
|
List(gomock.Any(), gomock.Any(),
|
|
dbmock.QueryOptions(database.WithCondition(orCondition)),
|
|
dbmock.QueryOptions(database.WithOrderByAscending(repo.OrganizationIDColumn(), repo.InstanceIDColumn())),
|
|
dbmock.QueryOptions(database.WithLimit(1)),
|
|
).Times(1).
|
|
Return([]*domain.LockoutSettings{
|
|
setting,
|
|
}, nil)
|
|
|
|
return repo
|
|
},
|
|
updatedHash: "",
|
|
checkType: &domain.VerificationTypeFailed{},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
FetchedUser: domain.User{OrganizationID: "org-1"},
|
|
},
|
|
expectedChanges: 1,
|
|
},
|
|
{
|
|
testName: "when check type failed and user max attempts >= lockout policy max attempts should return check type and user locked changes",
|
|
humanRepo: func(ctrl *gomock.Controller, checkTime time.Time) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
return repo
|
|
},
|
|
lockoutRepo: func(ctrl *gomock.Controller) domain.LockoutSettingsRepository {
|
|
repo := domainmock.NewLockoutSettingsRepo(ctrl)
|
|
|
|
instanceAndOrg := database.And(repo.InstanceIDCondition("instance-1"), repo.OrganizationIDCondition(gu.Ptr("org-1")))
|
|
orgNullOrEmpty := database.Or(repo.OrganizationIDCondition(nil), repo.OrganizationIDCondition(gu.Ptr("")))
|
|
onlyInstance := database.And(repo.InstanceIDCondition("instance-1"), orgNullOrEmpty)
|
|
orCondition := database.Or(instanceAndOrg, onlyInstance)
|
|
|
|
setting := &domain.LockoutSettings{
|
|
Settings: domain.Settings{},
|
|
LockoutSettingsAttributes: domain.LockoutSettingsAttributes{
|
|
MaxPasswordAttempts: gu.Ptr(uint64(1)),
|
|
},
|
|
}
|
|
|
|
repo.EXPECT().
|
|
List(gomock.Any(), gomock.Any(),
|
|
dbmock.QueryOptions(database.WithCondition(orCondition)),
|
|
dbmock.QueryOptions(database.WithOrderByAscending(repo.OrganizationIDColumn(), repo.InstanceIDColumn())),
|
|
dbmock.QueryOptions(database.WithLimit(1)),
|
|
).Times(1).
|
|
Return([]*domain.LockoutSettings{
|
|
setting,
|
|
}, nil)
|
|
|
|
return repo
|
|
},
|
|
updatedHash: "",
|
|
checkType: &domain.VerificationTypeFailed{},
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckTime: time.Now(),
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(_, _ string) (_ string, _ error) { return "", nil },
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
FetchedUser: domain.User{
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{FailedAttempts: 2}},
|
|
},
|
|
},
|
|
expectedChanges: 2,
|
|
expectedUserLocked: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given
|
|
ctx := authz.NewMockContext("instance-1", "", "")
|
|
ctrl := gomock.NewController(t)
|
|
|
|
opts := &domain.InvokeOpts{
|
|
Invoker: domain.NewTransactionInvoker(nil),
|
|
}
|
|
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
|
if tc.lockoutRepo != nil {
|
|
domain.WithLockoutSettingsRepo(tc.lockoutRepo(ctrl))(opts)
|
|
}
|
|
|
|
// Test
|
|
changes, err := tc.cmd.GetPasswordCheckChanges(ctx, opts, tc.humanRepo(ctrl, tc.cmd.CheckTime), tc.updatedHash, tc.checkType)
|
|
|
|
// Verify
|
|
assert.ErrorIs(t, err, tc.expectedError)
|
|
assert.Equal(t, tc.expectedHashedPsw, tc.cmd.UpdatedHashedPsw)
|
|
assert.Equal(t, tc.expectedUserLocked, tc.cmd.IsUserLocked)
|
|
assert.Len(t, changes, tc.expectedChanges)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPasswordCheckCommand_Execute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
userUpdateErr := errors.New("user update error")
|
|
sessionUpdateErr := errors.New("session update error")
|
|
|
|
tt := []struct {
|
|
testName string
|
|
humanRepo func(ctrl *gomock.Controller) domain.HumanUserRepository
|
|
lockoutRepo func(ctrl *gomock.Controller) domain.LockoutSettingsRepository
|
|
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
|
|
|
|
cmd *domain.PasswordCheckCommand
|
|
|
|
expectTarpitCalled bool
|
|
expectedError error
|
|
expectedValidated bool
|
|
expectedValidationSuccess bool
|
|
expectedUserLocked bool
|
|
}{
|
|
{
|
|
testName: "when checkPassword is nil should return no error",
|
|
cmd: &domain.PasswordCheckCommand{},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
testName: "when user update fails should return error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(0), userUpdateErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(userUpdateErr, "DOM-netNam", "failed updating user"),
|
|
},
|
|
{
|
|
testName: "when user not found should return not found error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(0), nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowNotFound(nil, "DOM-8wVrNc", "user not found"),
|
|
},
|
|
{
|
|
testName: "when user update returns multiple rows should return internal error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(2), nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-D4hy9C", "unexpected number of rows updated"),
|
|
},
|
|
{
|
|
testName: "when session update fails should return error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
sessionFactorChange := repo.SetFactor(&domain.SessionFactorPassword{LastVerifiedAt: time.Now()})
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, sessionFactorChange).
|
|
Times(1).
|
|
Return(int64(0), sessionUpdateErr)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(sessionUpdateErr, "DOM-IZagay", "failed updating session"),
|
|
},
|
|
{
|
|
testName: "when session not found should return not found error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
sessionFactorChange := repo.SetFactor(&domain.SessionFactorPassword{LastVerifiedAt: time.Now()})
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, sessionFactorChange).
|
|
Times(1).
|
|
Return(int64(0), nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowNotFound(nil, "DOM-H9Q59c", "session not found"),
|
|
},
|
|
{
|
|
testName: "when session update returns multiple rows should return internal error",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
sessionFactorChange := repo.SetFactor(&domain.SessionFactorPassword{LastVerifiedAt: time.Now()})
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, sessionFactorChange).
|
|
Times(1).
|
|
Return(int64(2), nil)
|
|
return repo
|
|
},
|
|
expectedError: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-Tbvpy8", "unexpected number of rows updated"),
|
|
},
|
|
{
|
|
testName: "when password check succeeds should execute successfully",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) { return "", nil },
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 0,
|
|
}},
|
|
},
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
sessionFactorChange := repo.SetFactor(&domain.SessionFactorPassword{LastVerifiedAt: time.Now()})
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, sessionFactorChange).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
expectedError: nil,
|
|
expectedValidated: true,
|
|
expectedValidationSuccess: true,
|
|
},
|
|
{
|
|
testName: "when password check fails should execute successfully with failed state",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "wrong-password"},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
VerifierFn: func(password string, hash string) (string, error) {
|
|
return "", passwap.ErrPasswordMismatch
|
|
},
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
Human: &domain.HumanUser{Password: domain.HumanPassword{
|
|
Hash: "hashed-password",
|
|
FailedAttempts: 2,
|
|
}},
|
|
},
|
|
},
|
|
lockoutRepo: func(ctrl *gomock.Controller) domain.LockoutSettingsRepository {
|
|
repo := domainmock.NewLockoutSettingsRepo(ctrl)
|
|
|
|
instanceAndOrg := database.And(repo.InstanceIDCondition("instance-1"), repo.OrganizationIDCondition(gu.Ptr("org-1")))
|
|
orgNullOrEmpty := database.Or(repo.OrganizationIDCondition(nil), repo.OrganizationIDCondition(gu.Ptr("")))
|
|
onlyInstance := database.And(repo.InstanceIDCondition("instance-1"), orgNullOrEmpty)
|
|
orCondition := database.Or(instanceAndOrg, onlyInstance)
|
|
|
|
setting := &domain.LockoutSettings{
|
|
Settings: domain.Settings{},
|
|
LockoutSettingsAttributes: domain.LockoutSettingsAttributes{
|
|
MaxPasswordAttempts: gu.Ptr(uint64(1)),
|
|
},
|
|
}
|
|
|
|
repo.EXPECT().
|
|
List(gomock.Any(), gomock.Any(),
|
|
dbmock.QueryOptions(database.WithCondition(orCondition)),
|
|
dbmock.QueryOptions(database.WithOrderByAscending(repo.OrganizationIDColumn(), repo.InstanceIDColumn())),
|
|
dbmock.QueryOptions(database.WithLimit(1)),
|
|
).Times(1).
|
|
Return([]*domain.LockoutSettings{
|
|
setting,
|
|
}, nil)
|
|
|
|
return repo
|
|
},
|
|
humanRepo: func(ctrl *gomock.Controller) domain.HumanUserRepository {
|
|
repo := domainmock.NewHumanRepo(ctrl)
|
|
idCondition := repo.IDCondition("user-1")
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, gomock.Any()).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
|
repo := domainmock.NewSessionRepo(ctrl)
|
|
idCondition := repo.IDCondition("session-1")
|
|
sessionFactorChange := repo.SetFactor(&domain.SessionFactorPassword{LastFailedAt: time.Now()})
|
|
repo.EXPECT().
|
|
Update(gomock.Any(), gomock.Any(), idCondition, sessionFactorChange).
|
|
Times(1).
|
|
Return(int64(1), nil)
|
|
return repo
|
|
},
|
|
expectTarpitCalled: true,
|
|
expectedError: zerrors.ThrowInvalidArgument(domain.NewPasswordVerificationError(3), "DOM-3gcfDV", "Errors.User.Password.Invalid"),
|
|
expectedValidated: true,
|
|
expectedValidationSuccess: false,
|
|
expectedUserLocked: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given
|
|
ctx := authz.NewMockContext("instance-1", "", "")
|
|
ctrl := gomock.NewController(t)
|
|
|
|
tarpitCalled := false
|
|
tc.cmd.TarpitFunc = func(_ uint64) {
|
|
tarpitCalled = true
|
|
}
|
|
|
|
opts := &domain.InvokeOpts{
|
|
Invoker: domain.NewTransactionInvoker(nil),
|
|
}
|
|
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
|
if tc.sessionRepo != nil {
|
|
domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts)
|
|
}
|
|
if tc.lockoutRepo != nil {
|
|
domain.WithLockoutSettingsRepo(tc.lockoutRepo(ctrl))(opts)
|
|
}
|
|
if tc.humanRepo != nil {
|
|
humanRepo := tc.humanRepo(ctrl)
|
|
userRepo := domainmock.NewMockUserRepository(ctrl)
|
|
userRepo.EXPECT().Human().Times(1).Return(humanRepo)
|
|
domain.WithUserRepo(userRepo)(opts)
|
|
}
|
|
|
|
// Test
|
|
err := tc.cmd.Execute(ctx, opts)
|
|
|
|
// Verify
|
|
assert.ErrorIs(t, err, tc.expectedError)
|
|
if tc.cmd.CheckPassword != nil {
|
|
assert.Equal(t, tc.expectedValidated, tc.cmd.IsValidated)
|
|
assert.Equal(t, tc.expectedValidationSuccess, tc.cmd.IsValidationSuccessful)
|
|
assert.Equal(t, tc.expectedUserLocked, tc.cmd.IsUserLocked)
|
|
assert.Equal(t, tc.expectTarpitCalled, tarpitCalled)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPasswordCheckCommand_Events(t *testing.T) {
|
|
t.Parallel()
|
|
domain.SetPasswordHasher(&crypto.Hasher{})
|
|
userAgg := user.NewAggregate("user-1", "org-1").Aggregate
|
|
sessAgg := session.NewAggregate("session-1", "instance-1").Aggregate
|
|
|
|
tt := []struct {
|
|
testName string
|
|
cmd *domain.PasswordCheckCommand
|
|
expectedEventTypes []eventstore.Command
|
|
}{
|
|
{
|
|
testName: "when checkPassword is nil should return nil",
|
|
cmd: &domain.PasswordCheckCommand{IsValidated: true},
|
|
},
|
|
{
|
|
testName: "when not validated should return nil",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
IsValidated: false,
|
|
},
|
|
},
|
|
{
|
|
testName: "when validation successful with no hash update should return check succeeded and password checked events",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
IsValidated: true,
|
|
IsValidationSuccessful: true,
|
|
UpdatedHashedPsw: "",
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
CheckTime: time.Now(),
|
|
},
|
|
expectedEventTypes: []eventstore.Command{
|
|
user.NewHumanPasswordCheckSucceededEvent(t.Context(), &userAgg, nil),
|
|
session.NewPasswordCheckedEvent(t.Context(), &sessAgg, time.Now()),
|
|
},
|
|
},
|
|
{
|
|
testName: "when validation successful with hash update should return check succeeded, hash updated, and password checked events",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "test"},
|
|
IsValidated: true,
|
|
IsValidationSuccessful: true,
|
|
UpdatedHashedPsw: "new-hash",
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
CheckTime: time.Now(),
|
|
},
|
|
expectedEventTypes: []eventstore.Command{
|
|
user.NewHumanPasswordCheckSucceededEvent(t.Context(), &userAgg, nil),
|
|
user.NewHumanPasswordHashUpdatedEvent(t.Context(), &userAgg, "new-hash"),
|
|
session.NewPasswordCheckedEvent(t.Context(), &sessAgg, time.Now()),
|
|
},
|
|
},
|
|
{
|
|
testName: "when validation failed without user locked should return check failed and password checked events",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "wrong"},
|
|
IsValidated: true,
|
|
IsValidationSuccessful: false,
|
|
IsUserLocked: false,
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
CheckTime: time.Now(),
|
|
},
|
|
expectedEventTypes: []eventstore.Command{
|
|
user.NewHumanPasswordCheckFailedEvent(t.Context(), &userAgg, nil),
|
|
session.NewPasswordCheckedEvent(t.Context(), &sessAgg, time.Now()),
|
|
},
|
|
},
|
|
{
|
|
testName: "when validation failed with user locked should return check failed, user locked, and password checked events",
|
|
cmd: &domain.PasswordCheckCommand{
|
|
CheckPassword: &domain.CheckPasswordType{Password: "wrong"},
|
|
IsValidated: true,
|
|
IsValidationSuccessful: false,
|
|
IsUserLocked: true,
|
|
FetchedUser: domain.User{
|
|
ID: "user-1",
|
|
OrganizationID: "org-1",
|
|
},
|
|
SessionID: "session-1",
|
|
InstanceID: "instance-1",
|
|
CheckTime: time.Now(),
|
|
},
|
|
expectedEventTypes: []eventstore.Command{
|
|
user.NewHumanPasswordCheckFailedEvent(t.Context(), &userAgg, nil),
|
|
user.NewUserLockedEvent(t.Context(), &userAgg),
|
|
session.NewPasswordCheckedEvent(t.Context(), &sessAgg, time.Now())},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given
|
|
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
|
|
|
|
// Test
|
|
events, err := tc.cmd.Events(ctx, &domain.InvokeOpts{})
|
|
|
|
// Verify
|
|
assert.NoError(t, err)
|
|
require.Len(t, events, len(tc.expectedEventTypes))
|
|
for i, expectedType := range tc.expectedEventTypes {
|
|
assert.IsType(t, expectedType, events[i])
|
|
switch expectedAssertedType := expectedType.(type) {
|
|
case *user.HumanPasswordCheckSucceededEvent:
|
|
actualAssertedType, ok := events[i].(*user.HumanPasswordCheckSucceededEvent)
|
|
require.True(t, ok)
|
|
assert.Equal(t, expectedAssertedType.AuthRequestInfo, actualAssertedType.AuthRequestInfo)
|
|
case *user.HumanPasswordCheckFailedEvent:
|
|
actualAssertedType, ok := events[i].(*user.HumanPasswordCheckFailedEvent)
|
|
require.True(t, ok)
|
|
assert.Equal(t, expectedAssertedType.AuthRequestInfo, actualAssertedType.AuthRequestInfo)
|
|
case *user.UserLockedEvent:
|
|
continue
|
|
case *user.HumanPasswordHashUpdatedEvent:
|
|
actualAssertedType, ok := events[i].(*user.HumanPasswordHashUpdatedEvent)
|
|
require.True(t, ok)
|
|
assert.Equal(t, expectedAssertedType.EncodedHash, actualAssertedType.EncodedHash)
|
|
case *session.PasswordCheckedEvent:
|
|
actualAssertedType, ok := events[i].(*session.PasswordCheckedEvent)
|
|
require.True(t, ok)
|
|
assert.InDelta(t, expectedAssertedType.CheckedAt.UnixMilli(), actualAssertedType.CheckedAt.UnixMilli(), 10)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|