mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
feat: add auth command side (#107)
* fix: query tests * fix: use prepare funcs * fix: go mod * fix: generate files * fix(eventstore): tests * fix(eventstore): rename modifier to editor * fix(migrations): add cluster migration, fix(migrations): fix typo of host in clean clsuter * fix(eventstore): move health * fix(eventstore): AggregateTypeFilter aggregateType as param * code quality * fix: go tests * feat: add member funcs * feat: add member model * feat: add member events * feat: add member repo model * fix: better error func testing * fix: project member funcs * fix: add tests * fix: add tests * feat: implement member requests * fix: merge master * fix: merge master * fix: read existing in project repo * fix: fix tests * feat: add internal cache * feat: add cache mock * fix: return values of cache mock * feat: add project role * fix: add cache config * fix: add role to eventstore * fix: use eventstore sdk * fix: use eventstore sdk * fix: add project role grpc requests * fix: fix getby id * fix: changes for mr * fix: change value to interface * feat: add app event creations * fix: searchmethods * Update internal/project/model/project_member.go Co-Authored-By: Silvan <silvan.reusser@gmail.com> * fix: use get project func * fix: append events * fix: check if value is string on equal ignore case * fix: add changes test * fix: add go mod * fix: add some tests * fix: return err not nil * fix: return err not nil * fix: add aggregate funcs and tests * fix: add oidc aggregate funcs and tests * fix: add oidc * fix: add some tests * fix: tests * feat: eventstore repository * fix: remove gorm * version * feat: pkg * feat: eventstore without eventstore-lib * rename files * gnueg * fix: global model * feat: add global view functions * feat(eventstore): sdk * fix(eventstore): rename app to eventstore * delete empty test * fix(models): delete unused struct * feat(eventstore): overwrite context data * fix: use global sql config * fix: oidc validation * fix: generate client secret * fix: generate client id * fix: test change app * fix: deactivate/reactivate application * fix: change oidc config * fix: change oidc config secret * begin models * begin repo * fix: implement grpc app funcs * fix: add application requests * fix: converter * fix: converter * fix: converter and generate clientid * fix: tests * feat: project grant aggregate * feat: project grant * fix: project grant check if role existing * fix: project grant requests * fix: project grant fixes * fix: project grant member model * fix: project grant member aggregate * fix: project grant member eventstore * fix: project grant member requests * feat: user model * begin repo * repo models and more * feat: user command side * lots of functions * user command side * profile requests * commit before rebase on user * save * local config with gopass and more * begin new auth command (user centric) * Update internal/user/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/user_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/eventstore_mock_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * changes from mr review * save files into basedir * changes from mr review * changes from mr review * move to auth request * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * changes requested on mr * fix generate codes * fix return if no events * password code * email verification step * more steps * lot of mfa * begin tests * more next steps * auth api * auth api (user) * auth api (user) * auth api (user) * differ requests * merge * tests * fix compilation error * mock for id generator * Update internal/user/repository/eventsourcing/model/password.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * requests of mr * check email * begin separation of command and query * otp * change packages * some cleanup and fixes * tests for auth request / next steps * add VerificationLifetimes to config and make it run * tests * fix code challenge validation * cleanup * fix merge * begin view * repackaging tests and configs * fix startup config for auth * add migration * add PromptSelectAccount * fix copy / paste * remove user_agent files * fixes * fix sequences in user_session * token commands * token queries and signout * fix * fix set password test * add token handler and table * handle session init * add session state * add user view test cases * change VerifyMyMfaOTP * some fixes * fix user repo in auth api * cleanup * add user session view test * fix merge * fixes * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * extract method usersForUserSelection * add todo for policy check * id on auth req * fix enum name Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com> Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
61
internal/user/model/user_session_view.go
Normal file
61
internal/user/model/user_session_view.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
)
|
||||
|
||||
type UserSessionView struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
State req_model.UserSessionState
|
||||
ResourceOwner string
|
||||
UserAgentID string
|
||||
UserID string
|
||||
UserName string
|
||||
PasswordVerification time.Time
|
||||
MfaSoftwareVerification time.Time
|
||||
MfaHardwareVerification time.Time
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type UserSessionSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn UserSessionSearchKey
|
||||
Asc bool
|
||||
Queries []*UserSessionSearchQuery
|
||||
}
|
||||
|
||||
type UserSessionSearchKey int32
|
||||
|
||||
const (
|
||||
USERSESSIONSEARCHKEY_UNSPECIFIED UserSessionSearchKey = iota
|
||||
USERSESSIONSEARCHKEY_SESSION_ID
|
||||
USERSESSIONSEARCHKEY_USER_AGENT_ID
|
||||
USERSESSIONSEARCHKEY_USER_ID
|
||||
USERSESSIONSEARCHKEY_STATE
|
||||
USERSESSIONSEARCHKEY_RESOURCEOWNER
|
||||
)
|
||||
|
||||
type UserSessionSearchQuery struct {
|
||||
Key UserSessionSearchKey
|
||||
Method model.SearchMethod
|
||||
Value string
|
||||
}
|
||||
|
||||
type UserSessionSearchResponse struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*UserSessionView
|
||||
}
|
||||
|
||||
func (r *UserSessionSearchRequest) EnsureLimit(limit uint64) {
|
||||
if r.Limit == 0 || r.Limit > limit {
|
||||
r.Limit = limit
|
||||
}
|
||||
}
|
@@ -1,36 +1,42 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
"time"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
)
|
||||
|
||||
type UserView struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
State UserState
|
||||
ResourceOwner string
|
||||
PasswordChanged time.Time
|
||||
LastLogin time.Time
|
||||
UserName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage string
|
||||
Gender Gender
|
||||
Email string
|
||||
IsEmailVerified bool
|
||||
Phone string
|
||||
IsPhoneVerified bool
|
||||
Country string
|
||||
Locality string
|
||||
PostalCode string
|
||||
Region string
|
||||
StreetAddress string
|
||||
OTPState MfaState
|
||||
Sequence uint64
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
State UserState
|
||||
ResourceOwner string
|
||||
PasswordSet bool
|
||||
PasswordChangeRequired bool
|
||||
PasswordChanged time.Time
|
||||
LastLogin time.Time
|
||||
UserName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage string
|
||||
Gender Gender
|
||||
Email string
|
||||
IsEmailVerified bool
|
||||
Phone string
|
||||
IsPhoneVerified bool
|
||||
Country string
|
||||
Locality string
|
||||
PostalCode string
|
||||
Region string
|
||||
StreetAddress string
|
||||
OTPState MfaState
|
||||
MfaMaxSetUp req_model.MfaLevel
|
||||
MfaInitSkipped time.Time
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type UserSearchRequest struct {
|
||||
@@ -78,3 +84,35 @@ func (r *UserSearchRequest) EnsureLimit(limit uint64) {
|
||||
func (r *UserSearchRequest) AppendMyOrgQuery(orgID string) {
|
||||
r.Queries = append(r.Queries, &UserSearchQuery{Key: USERSEARCHKEY_RESOURCEOWNER, Method: model.SEARCHMETHOD_EQUALS, Value: orgID})
|
||||
}
|
||||
|
||||
func (u *UserView) MfaTypesSetupPossible(level req_model.MfaLevel) []req_model.MfaType {
|
||||
types := make([]req_model.MfaType, 0)
|
||||
switch level {
|
||||
case req_model.MfaLevelSoftware:
|
||||
if u.OTPState != MFASTATE_READY {
|
||||
types = append(types, req_model.MfaTypeOTP)
|
||||
}
|
||||
//PLANNED: add sms
|
||||
fallthrough
|
||||
case req_model.MfaLevelHardware:
|
||||
//PLANNED: add token
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
func (u *UserView) MfaTypesAllowed(level req_model.MfaLevel) []req_model.MfaType {
|
||||
types := make([]req_model.MfaType, 0)
|
||||
switch level {
|
||||
default:
|
||||
fallthrough
|
||||
case req_model.MfaLevelSoftware:
|
||||
if u.OTPState == MFASTATE_READY {
|
||||
types = append(types, req_model.MfaTypeOTP)
|
||||
}
|
||||
//PLANNED: add sms
|
||||
fallthrough
|
||||
case req_model.MfaLevelHardware:
|
||||
//PLANNED: add token
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
@@ -4,6 +4,10 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/sony/sonyflake"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/cache/config"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
@@ -14,8 +18,6 @@ import (
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/sony/sonyflake"
|
||||
)
|
||||
|
||||
type UserEventstore struct {
|
||||
@@ -28,6 +30,7 @@ type UserEventstore struct {
|
||||
PhoneVerificationCode crypto.Generator
|
||||
PasswordVerificationCode crypto.Generator
|
||||
Multifactors global_model.Multifactors
|
||||
validateTOTP func(string, string) bool
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
@@ -71,6 +74,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
|
||||
PasswordVerificationCode: passwordVerificationCode,
|
||||
Multifactors: mfa,
|
||||
PasswordAlg: passwordAlg,
|
||||
validateTOTP: totp.Validate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -333,31 +337,80 @@ func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "password not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
|
||||
return es.changedPassword(ctx, password, true)
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SetPassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
|
||||
return es.changedPassword(ctx, password, false)
|
||||
}
|
||||
|
||||
func (es *UserEventstore) changedPassword(ctx context.Context, password *usr_model.Password, onetime bool) (*usr_model.Password, error) {
|
||||
if !password.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dosi3", "password invalid")
|
||||
func (es *UserEventstore) CheckPassword(ctx context.Context, userID, password string, authRequest *req_model.AuthRequest) error {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.Password == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s35Fa", "no password set")
|
||||
}
|
||||
if err := crypto.CompareHash(existing.Password.SecretCrypto, []byte(password), es.PasswordAlg); err == nil {
|
||||
return es.setPasswordCheckResult(ctx, existing, authRequest, PasswordCheckSucceededAggregate)
|
||||
}
|
||||
if err := es.setPasswordCheckResult(ctx, existing, authRequest, PasswordCheckFailedAggregate); err != nil {
|
||||
return err
|
||||
}
|
||||
return caos_errs.ThrowInvalidArgument(nil, "EVENT-452ad", "invalid password")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) setPasswordCheckResult(ctx context.Context, user *usr_model.User, authRequest *req_model.AuthRequest, check func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc) error {
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoAuthRequest := model.AuthRequestFromModel(authRequest)
|
||||
agg := check(es.AggregateCreator(), repoUser, repoAuthRequest)
|
||||
err := es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
|
||||
user, err := es.UserByID(ctx, password.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es.changedPassword(ctx, user, password.SecretString, true)
|
||||
}
|
||||
|
||||
err = password.HashPasswordIfExisting(es.PasswordAlg, onetime)
|
||||
func (es *UserEventstore) SetPassword(ctx context.Context, userID, code, password string) error {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.PasswordCode == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-65sdr", "reset code not found")
|
||||
}
|
||||
if err := crypto.VerifyCode(user.PasswordCode.CreationDate, user.PasswordCode.Expiry, user.PasswordCode.Code, code, es.PasswordVerificationCode); err != nil {
|
||||
return caos_errs.ThrowPreconditionFailed(err, "EVENT-sd6DF", "code invalid")
|
||||
}
|
||||
_, err = es.changedPassword(ctx, user, password, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangePassword(ctx context.Context, userID, old, new string) (*usr_model.Password, error) {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Password == nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fds3s", "user has no password")
|
||||
}
|
||||
if err := crypto.CompareHash(user.Password.SecretCrypto, []byte(old), es.PasswordAlg); err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-s56a3", "invalid password")
|
||||
}
|
||||
return es.changedPassword(ctx, user, new, false)
|
||||
}
|
||||
|
||||
func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, password string, onetime bool) (*usr_model.Password, error) {
|
||||
//TODO: check password policy
|
||||
secret, err := crypto.Hash([]byte(password), es.PasswordAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoPassword := &model.Password{Secret: secret, ChangeRequired: onetime}
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoPassword := model.PasswordFromModel(password)
|
||||
|
||||
agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
@@ -666,21 +719,6 @@ func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model.
|
||||
return model.AddressToModel(repoExisting.Address), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) OTPByID(ctx context.Context, userID string) (*usr_model.OTP, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.OTP != nil {
|
||||
return user.OTP, nil
|
||||
}
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dps09", "otp not found")
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddOTP(ctx context.Context, userID string) (*usr_model.OTP, error) {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -731,22 +769,77 @@ func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string) error {
|
||||
existing, err := es.UserByID(ctx, userID)
|
||||
func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code string) error {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.OTP == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
|
||||
if user.OTP == nil || user.IsOTPReady() {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "otp not existing or already set up")
|
||||
}
|
||||
decrypt, err := crypto.DecryptString(existing.OTP.Secret, es.Multifactors.OTP.CryptoMFA)
|
||||
if err := es.verifyMfaOTP(user.OTP, code); err != nil {
|
||||
return err
|
||||
}
|
||||
repoUser := model.UserFromModel(user)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MfaOTPVerifyAggregate(es.AggregateCreator(), repoUser))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := totp.Validate(code, decrypt)
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !user.IsOTPReady() {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "opt not ready")
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoAuthReq := model.AuthRequestFromModel(authRequest)
|
||||
var aggregate func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc
|
||||
if err := es.verifyMfaOTP(user.OTP, code); err != nil {
|
||||
aggregate = MfaOTPCheckFailedAggregate
|
||||
} else {
|
||||
aggregate = MfaOTPCheckSucceededAggregate
|
||||
}
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, aggregate(es.AggregateCreator(), repoUser, repoAuthReq))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) verifyMfaOTP(otp *usr_model.OTP, code string) error {
|
||||
decrypt, err := crypto.DecryptString(otp.Secret, es.Multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := es.validateTOTP(code, decrypt)
|
||||
if !valid {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Invalid code")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SignOut(ctx context.Context, agentID, userID string) error {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoUser := model.UserFromModel(user)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, SignOutAggregate(es.AggregateCreator(), repoUser, agentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
@@ -2,15 +2,17 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sony/sonyflake"
|
||||
|
||||
mock_cache "github.com/caos/zitadel/internal/cache/mock"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore/mock"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sony/sonyflake"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore {
|
||||
@@ -38,10 +40,7 @@ func GetMockedEventstoreWithPw(ctrl *gomock.Controller, mockEs *mock.MockEventst
|
||||
}
|
||||
if password {
|
||||
es.PasswordVerificationCode = GetMockPwGenerator(ctrl)
|
||||
hash := crypto.NewMockHashAlgorithm(ctrl)
|
||||
hash.EXPECT().Hash(gomock.Any()).Return(nil, nil)
|
||||
hash.EXPECT().Algorithm().Return("bcrypt")
|
||||
es.PasswordAlg = hash
|
||||
es.PasswordAlg = crypto.CreateMockHashAlg(ctrl)
|
||||
}
|
||||
return es
|
||||
}
|
||||
@@ -174,8 +173,10 @@ func GetMockManipulateUserWithPhoneCodeGen(ctrl *gomock.Controller, user model.U
|
||||
|
||||
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
|
||||
data, _ := json.Marshal(user)
|
||||
code, _ := json.Marshal(user.PasswordCode)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPasswordCodeAdded, Data: code},
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
@@ -394,29 +395,48 @@ func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
}
|
||||
|
||||
func GetMockManipulateUserWithOTP(ctrl *gomock.Controller) *UserEventstore {
|
||||
func GetMockManipulateUserWithOTP(ctrl *gomock.Controller, decrypt, verified bool) *UserEventstore {
|
||||
user := model.User{
|
||||
Profile: &model.Profile{
|
||||
UserName: "UserName",
|
||||
},
|
||||
}
|
||||
otp := model.OTP{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
}}
|
||||
otp := model.OTP{
|
||||
Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
}
|
||||
dataUser, _ := json.Marshal(user)
|
||||
dataOtp, _ := json.Marshal(otp)
|
||||
events := []*es_models.Event{
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
|
||||
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpAdded, Data: dataOtp},
|
||||
}
|
||||
if verified {
|
||||
events = append(events, &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpVerified})
|
||||
}
|
||||
mockEs := mock.NewMockEventstore(ctrl)
|
||||
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
|
||||
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
|
||||
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
|
||||
return GetMockedEventstore(ctrl, mockEs)
|
||||
es := GetMockedEventstore(ctrl, mockEs)
|
||||
if !decrypt {
|
||||
return es
|
||||
}
|
||||
enc := crypto.NewMockEncryptionAlgorithm(ctrl)
|
||||
enc.EXPECT().Algorithm().Return("enc")
|
||||
enc.EXPECT().Encrypt(gomock.Any()).Return(nil, nil)
|
||||
enc.EXPECT().EncryptionKeyID().Return("id")
|
||||
enc.EXPECT().DecryptionKeyIDs().Return([]string{"id"})
|
||||
enc.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("code", nil)
|
||||
es.Multifactors = global_model.Multifactors{OTP: global_model.OTP{
|
||||
Issuer: "Issuer",
|
||||
CryptoMFA: enc,
|
||||
}}
|
||||
return es
|
||||
}
|
||||
|
||||
func GetMockManipulateUserNoEvents(ctrl *gomock.Controller) *UserEventstore {
|
||||
|
@@ -2,14 +2,19 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
repo_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/golang/mock/gomock"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUserByID(t *testing.T) {
|
||||
@@ -1025,16 +1030,17 @@ func TestSetOneTimePassword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPassword(t *testing.T) {
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
password *model.Password
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
password string
|
||||
authRequest *req_model.AuthRequest
|
||||
}
|
||||
type res struct {
|
||||
password *model.Password
|
||||
errFunc func(err error) bool
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1042,22 +1048,40 @@ func TestSetPassword(t *testing.T) {
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "create pw",
|
||||
name: "check pw ok",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}}),
|
||||
es: GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
Password: &repo_model.Password{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
Crypted: []byte("password"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"},
|
||||
},
|
||||
res: res{
|
||||
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false},
|
||||
userID: "userID",
|
||||
password: "password",
|
||||
authRequest: &req_model.AuthRequest{
|
||||
ID: "id",
|
||||
AgentID: "agentID",
|
||||
BrowserInfo: &req_model.BrowserInfo{
|
||||
UserAgent: "user agent",
|
||||
AcceptLanguage: "accept langugage",
|
||||
RemoteIP: net.IPv4(29, 4, 20, 19),
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: ""}, SecretString: "Password"},
|
||||
userID: "",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
@@ -1068,25 +1092,311 @@ func TestSetPassword(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"},
|
||||
userID: "userID",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no password",
|
||||
args: args{
|
||||
es: GetMockUserByIDOK(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid password",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
Password: &repo_model.Password{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
Crypted: []byte("password"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
password: "wrong password",
|
||||
authRequest: &req_model.AuthRequest{
|
||||
ID: "id",
|
||||
AgentID: "agentID",
|
||||
BrowserInfo: &req_model.BrowserInfo{
|
||||
UserAgent: "user agent",
|
||||
AcceptLanguage: "accept langugage",
|
||||
RemoteIP: net.IPv4(29, 4, 20, 19),
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := tt.args.es.SetPassword(tt.args.ctx, tt.args.password)
|
||||
err := tt.args.es.CheckPassword(tt.args.ctx, tt.args.userID, tt.args.password, tt.args.authRequest)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("result has error: %v", err)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPassword(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
password string
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "create pw",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
PasswordCode: &repo_model.PasswordCode{Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
password: "password",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "",
|
||||
code: "code",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no passcode",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid passcode",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
PasswordCode: &repo_model.PasswordCode{Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc2",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code2"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.SetPassword(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.password)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("result has error: %v", err)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePassword(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
old string
|
||||
new string
|
||||
}
|
||||
type res struct {
|
||||
password string
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "change pw",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
Password: &repo_model.Password{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
Crypted: []byte("old"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
old: "old",
|
||||
new: "new",
|
||||
},
|
||||
res: res{
|
||||
password: "new",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "",
|
||||
old: "old",
|
||||
new: "new",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
old: "old",
|
||||
new: "new",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no password",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
old: "old",
|
||||
new: "new",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid password",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPasswordCodeGen(ctrl,
|
||||
repo_model.User{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
Password: &repo_model.Password{Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
Crypted: []byte("older"),
|
||||
}},
|
||||
},
|
||||
),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
old: "old",
|
||||
new: "new",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.userID, tt.args.old, tt.args.new)
|
||||
|
||||
if tt.res.errFunc == nil && result.AggregateID == "" {
|
||||
t.Errorf("result has no id")
|
||||
}
|
||||
if tt.res.errFunc == nil && result.ChangeRequired != false {
|
||||
t.Errorf("should not be one time")
|
||||
if tt.res.errFunc == nil && string(result.SecretCrypto.Crypted) != tt.res.password {
|
||||
t.Errorf("got wrong result crypted: expected: %v, actual: %v ", tt.res.password, result.SecretString)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
t.Errorf("got wrong err: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2036,69 +2346,6 @@ func TestChangeAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOTPByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "get by id, ok",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithOTP(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := tt.args.es.OTPByID(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && result.AggregateID == "" {
|
||||
t.Errorf("result has no id")
|
||||
}
|
||||
if tt.res.errFunc == nil && result == nil {
|
||||
t.Errorf("got wrong result change required: actual: %v ", result)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddOTP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@@ -2168,6 +2415,245 @@ func TestAddOTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMfaOTPSetup(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "setup ok",
|
||||
args: args{
|
||||
es: func() *UserEventstore {
|
||||
es := GetMockManipulateUserWithOTP(ctrl, true, false)
|
||||
es.validateTOTP = func(string, string) bool {
|
||||
return true
|
||||
}
|
||||
return es
|
||||
}(),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "id",
|
||||
code: "code",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "wrong code",
|
||||
args: args{
|
||||
es: func() *UserEventstore {
|
||||
es := GetMockManipulateUserWithOTP(ctrl, true, false)
|
||||
es.validateTOTP = func(string, string) bool {
|
||||
return false
|
||||
}
|
||||
return es
|
||||
}(),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "id",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user has no otp",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.CheckMfaOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("result should not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMfaOTP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
authRequest *req_model.AuthRequest
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "check ok",
|
||||
args: args{
|
||||
es: func() *UserEventstore {
|
||||
es := GetMockManipulateUserWithOTP(ctrl, true, true)
|
||||
es.validateTOTP = func(string, string) bool {
|
||||
return true
|
||||
}
|
||||
return es
|
||||
}(),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "id",
|
||||
code: "code",
|
||||
authRequest: &req_model.AuthRequest{
|
||||
ID: "id",
|
||||
AgentID: "agentID",
|
||||
BrowserInfo: &req_model.BrowserInfo{
|
||||
UserAgent: "user agent",
|
||||
AcceptLanguage: "accept langugage",
|
||||
RemoteIP: net.IPv4(29, 4, 20, 19),
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "wrong code",
|
||||
args: args{
|
||||
es: func() *UserEventstore {
|
||||
es := GetMockManipulateUserWithOTP(ctrl, true, true)
|
||||
es.validateTOTP = func(string, string) bool {
|
||||
return false
|
||||
}
|
||||
return es
|
||||
}(),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "id",
|
||||
code: "code",
|
||||
authRequest: &req_model.AuthRequest{
|
||||
ID: "id",
|
||||
AgentID: "agentID",
|
||||
BrowserInfo: &req_model.BrowserInfo{
|
||||
UserAgent: "user agent",
|
||||
AcceptLanguage: "accept langugage",
|
||||
RemoteIP: net.IPv4(29, 4, 20, 19),
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user has no otp",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.CheckMfaOTP(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.authRequest)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("result should not get err, got : %v", err)
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveOTP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@@ -2186,7 +2672,7 @@ func TestRemoveOTP(t *testing.T) {
|
||||
{
|
||||
name: "remove ok",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithOTP(ctrl),
|
||||
es: GetMockManipulateUserWithOTP(ctrl, false, true),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
@@ -2238,80 +2724,3 @@ func TestRemoveOTP(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckOTP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user has no otp",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
userID: "userID",
|
||||
code: "code",
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.CheckMfaOTP(tt.args.ctx, tt.args.userID, tt.args.code)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("result should not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
35
internal/user/repository/eventsourcing/model/auth_request.go
Normal file
35
internal/user/repository/eventsourcing/model/auth_request.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
*BrowserInfo
|
||||
}
|
||||
|
||||
func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest {
|
||||
return &AuthRequest{
|
||||
ID: request.ID,
|
||||
UserAgentID: request.AgentID,
|
||||
BrowserInfo: BrowserInfoFromModel(request.BrowserInfo),
|
||||
}
|
||||
}
|
||||
|
||||
type BrowserInfo struct {
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
AcceptLanguage string `json:"acceptLanguage,omitempty"`
|
||||
RemoteIP net.IP `json:"remoteIP,omitempty"`
|
||||
}
|
||||
|
||||
func BrowserInfoFromModel(info *model.BrowserInfo) *BrowserInfo {
|
||||
return &BrowserInfo{
|
||||
UserAgent: info.UserAgent,
|
||||
AcceptLanguage: info.AcceptLanguage,
|
||||
RemoteIP: info.RemoteIP,
|
||||
}
|
||||
}
|
@@ -23,9 +23,11 @@ const (
|
||||
UserReactivated models.EventType = "user.reactivated"
|
||||
UserDeleted models.EventType = "user.deleted"
|
||||
|
||||
UserPasswordChanged models.EventType = "user.password.changed"
|
||||
UserPasswordCodeAdded models.EventType = "user.password.code.added"
|
||||
UserPasswordCodeSent models.EventType = "user.password.code.sent"
|
||||
UserPasswordChanged models.EventType = "user.password.changed"
|
||||
UserPasswordCodeAdded models.EventType = "user.password.code.added"
|
||||
UserPasswordCodeSent models.EventType = "user.password.code.sent"
|
||||
UserPasswordCheckSucceeded models.EventType = "user.password.check.succeeded"
|
||||
UserPasswordCheckFailed models.EventType = "user.password.check.failed"
|
||||
|
||||
UserEmailChanged models.EventType = "user.email.changed"
|
||||
UserEmailVerified models.EventType = "user.email.verified"
|
||||
@@ -40,8 +42,12 @@ const (
|
||||
UserProfileChanged models.EventType = "user.profile.changed"
|
||||
UserAddressChanged models.EventType = "user.address.changed"
|
||||
|
||||
MfaOtpAdded models.EventType = "user.mfa.otp.added"
|
||||
MfaOtpVerified models.EventType = "user.mfa.otp.verified"
|
||||
MfaOtpRemoved models.EventType = "user.mfa.otp.removed"
|
||||
MfaInitSkipped models.EventType = "user.mfa.init.skipped"
|
||||
MfaOtpAdded models.EventType = "user.mfa.otp.added"
|
||||
MfaOtpVerified models.EventType = "user.mfa.otp.verified"
|
||||
MfaOtpRemoved models.EventType = "user.mfa.otp.removed"
|
||||
MfaOtpCheckSucceeded models.EventType = "user.mfa.otp.check.succeeded"
|
||||
MfaOtpCheckFailed models.EventType = "user.mfa.otp.check.failed"
|
||||
MfaInitSkipped models.EventType = "user.mfa.init.skipped"
|
||||
|
||||
SignedOut models.EventType = "user.signed.out"
|
||||
)
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
@@ -167,6 +168,25 @@ func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, existing *m
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordCheckSucceededAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, check *model.AuthRequest) es_sdk.AggregateFunc {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordCheckSucceeded, check)
|
||||
}
|
||||
}
|
||||
func PasswordCheckFailedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, check *model.AuthRequest) es_sdk.AggregateFunc {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordCheckFailed, check)
|
||||
}
|
||||
}
|
||||
|
||||
func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.User, request *model.PasswordCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if request == nil {
|
||||
@@ -338,6 +358,32 @@ func MfaOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, existing *mod
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPCheckSucceededAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, authReq *model.AuthRequest) es_sdk.AggregateFunc {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if authReq == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-sd5DA", "authReq must not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaOtpCheckSucceeded, authReq)
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPCheckFailedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, authReq *model.AuthRequest) es_sdk.AggregateFunc {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if authReq == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-64sd6", "authReq must not be nil")
|
||||
}
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.MfaOtpCheckFailed, authReq)
|
||||
}
|
||||
}
|
||||
|
||||
func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
@@ -347,3 +393,13 @@ func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *mod
|
||||
return agg.AppendEvent(model.MfaOtpRemoved, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func SignOutAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, agentID string) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.SignedOut, map[string]interface{}{"agentID": agentID})
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,15 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,90 +26,102 @@ const (
|
||||
)
|
||||
|
||||
type UserView struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
State int32 `json:"-" gorm:"column:user_state"`
|
||||
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
|
||||
LastLogin time.Time `json:"-" gorm:"column:last_login"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
FirstName string `json:"firstName" gorm:"column:first_name"`
|
||||
LastName string `json:"lastName" gorm:"column:last_name"`
|
||||
NickName string `json:"nickName" gorm:"column:nick_name"`
|
||||
DisplayName string `json:"displayName" gorm:"column:display_name"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
Gender int32 `json:"gender" gorm:"column:gender"`
|
||||
Email string `json:"email" gorm:"column:email"`
|
||||
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
|
||||
Phone string `json:"phone" gorm:"column:phone"`
|
||||
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
|
||||
Country string `json:"country" gorm:"column:country"`
|
||||
Locality string `json:"locality" gorm:"column:locality"`
|
||||
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
|
||||
Region string `json:"region" gorm:"column:region"`
|
||||
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
|
||||
OTPState int32 `json:"-" gorm:"column:otp_state"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
State int32 `json:"-" gorm:"column:user_state"`
|
||||
PasswordSet bool `json:"-" gorm:"column:password_set"`
|
||||
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
|
||||
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
|
||||
LastLogin time.Time `json:"-" gorm:"column:last_login"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
FirstName string `json:"firstName" gorm:"column:first_name"`
|
||||
LastName string `json:"lastName" gorm:"column:last_name"`
|
||||
NickName string `json:"nickName" gorm:"column:nick_name"`
|
||||
DisplayName string `json:"displayName" gorm:"column:display_name"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
Gender int32 `json:"gender" gorm:"column:gender"`
|
||||
Email string `json:"email" gorm:"column:email"`
|
||||
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
|
||||
Phone string `json:"phone" gorm:"column:phone"`
|
||||
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
|
||||
Country string `json:"country" gorm:"column:country"`
|
||||
Locality string `json:"locality" gorm:"column:locality"`
|
||||
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
|
||||
Region string `json:"region" gorm:"column:region"`
|
||||
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
|
||||
OTPState int32 `json:"-" gorm:"column:otp_state"`
|
||||
MfaMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
|
||||
MfaInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func UserFromModel(user *model.UserView) *UserView {
|
||||
return &UserView{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
State: int32(user.State),
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: int32(user.Gender),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
Country: user.Country,
|
||||
Locality: user.Locality,
|
||||
PostalCode: user.PostalCode,
|
||||
Region: user.Region,
|
||||
StreetAddress: user.StreetAddress,
|
||||
OTPState: int32(user.OTPState),
|
||||
Sequence: user.Sequence,
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
State: int32(user.State),
|
||||
PasswordSet: user.PasswordSet,
|
||||
PasswordChangeRequired: user.PasswordChangeRequired,
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: int32(user.Gender),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
Country: user.Country,
|
||||
Locality: user.Locality,
|
||||
PostalCode: user.PostalCode,
|
||||
Region: user.Region,
|
||||
StreetAddress: user.StreetAddress,
|
||||
OTPState: int32(user.OTPState),
|
||||
MfaMaxSetUp: int32(user.MfaMaxSetUp),
|
||||
MfaInitSkipped: user.MfaInitSkipped,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func UserToModel(user *UserView) *model.UserView {
|
||||
return &model.UserView{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
State: model.UserState(user.State),
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: model.Gender(user.Gender),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
Country: user.Country,
|
||||
Locality: user.Locality,
|
||||
PostalCode: user.PostalCode,
|
||||
Region: user.Region,
|
||||
StreetAddress: user.StreetAddress,
|
||||
OTPState: model.MfaState(user.OTPState),
|
||||
Sequence: user.Sequence,
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
State: model.UserState(user.State),
|
||||
PasswordSet: user.PasswordSet,
|
||||
PasswordChangeRequired: user.PasswordChangeRequired,
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: model.Gender(user.Gender),
|
||||
Email: user.Email,
|
||||
IsEmailVerified: user.IsEmailVerified,
|
||||
Phone: user.Phone,
|
||||
IsPhoneVerified: user.IsPhoneVerified,
|
||||
Country: user.Country,
|
||||
Locality: user.Locality,
|
||||
PostalCode: user.PostalCode,
|
||||
Region: user.Region,
|
||||
StreetAddress: user.StreetAddress,
|
||||
OTPState: model.MfaState(user.OTPState),
|
||||
MfaMaxSetUp: req_model.MfaLevel(user.MfaMaxSetUp),
|
||||
MfaInitSkipped: user.MfaInitSkipped,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,43 +133,52 @@ func UsersToModel(users []*UserView) []*model.UserView {
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *UserView) AppendEvent(event *models.Event) (err error) {
|
||||
p.ChangeDate = event.CreationDate
|
||||
p.Sequence = event.Sequence
|
||||
func (u *UserView) AppendEvent(event *models.Event) (err error) {
|
||||
u.ChangeDate = event.CreationDate
|
||||
u.Sequence = event.Sequence
|
||||
switch event.Type {
|
||||
case es_model.UserAdded,
|
||||
es_model.UserRegistered:
|
||||
p.CreationDate = event.CreationDate
|
||||
p.setRootData(event)
|
||||
err = p.setData(event)
|
||||
u.CreationDate = event.CreationDate
|
||||
u.setRootData(event)
|
||||
err = u.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = u.setPasswordData(event)
|
||||
case es_model.UserPasswordChanged:
|
||||
err = u.setPasswordData(event)
|
||||
case es_model.UserProfileChanged,
|
||||
es_model.UserAddressChanged:
|
||||
err = p.setData(event)
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailChanged:
|
||||
p.IsEmailVerified = false
|
||||
err = p.setData(event)
|
||||
u.IsEmailVerified = false
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailVerified:
|
||||
p.IsEmailVerified = true
|
||||
u.IsEmailVerified = true
|
||||
case es_model.UserPhoneChanged:
|
||||
p.IsPhoneVerified = false
|
||||
err = p.setData(event)
|
||||
u.IsPhoneVerified = false
|
||||
err = u.setData(event)
|
||||
case es_model.UserPhoneVerified:
|
||||
p.IsPhoneVerified = true
|
||||
u.IsPhoneVerified = true
|
||||
case es_model.UserDeactivated:
|
||||
p.State = int32(model.USERSTATE_INACTIVE)
|
||||
u.State = int32(model.USERSTATE_INACTIVE)
|
||||
case es_model.UserReactivated,
|
||||
es_model.UserUnlocked:
|
||||
p.State = int32(model.USERSTATE_ACTIVE)
|
||||
u.State = int32(model.USERSTATE_ACTIVE)
|
||||
case es_model.UserLocked:
|
||||
p.State = int32(model.USERSTATE_LOCKED)
|
||||
u.State = int32(model.USERSTATE_LOCKED)
|
||||
case es_model.MfaOtpAdded:
|
||||
p.OTPState = int32(model.MFASTATE_NOTREADY)
|
||||
u.OTPState = int32(model.MFASTATE_NOTREADY)
|
||||
case es_model.MfaOtpVerified:
|
||||
p.OTPState = int32(model.MFASTATE_READY)
|
||||
u.OTPState = int32(model.MFASTATE_READY)
|
||||
u.MfaInitSkipped = time.Time{}
|
||||
case es_model.MfaOtpRemoved:
|
||||
p.OTPState = int32(model.MFASTATE_UNSPECIFIED)
|
||||
u.OTPState = int32(model.MFASTATE_UNSPECIFIED)
|
||||
case es_model.MfaInitSkipped:
|
||||
u.MfaInitSkipped = event.CreationDate
|
||||
}
|
||||
p.ComputeObject()
|
||||
u.ComputeObject()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -165,12 +189,23 @@ func (u *UserView) setRootData(event *models.Event) {
|
||||
|
||||
func (u *UserView) setData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, u); err != nil {
|
||||
logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
logging.Log("MODEL-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) setPasswordData(event *models.Event) error {
|
||||
password := new(es_model.Password)
|
||||
if err := json.Unmarshal(event.Data, password); err != nil {
|
||||
logging.Log("MODEL-sdw4r").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.PasswordSet = password.Secret != nil
|
||||
u.PasswordChangeRequired = password.ChangeRequired
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserView) ComputeObject() {
|
||||
if u.State == int32(model.USERSTATE_UNSPECIFIED) || u.State == int32(model.USERSTATE_INITIAL) {
|
||||
if u.IsEmailVerified {
|
||||
@@ -179,4 +214,7 @@ func (u *UserView) ComputeObject() {
|
||||
u.State = int32(model.USERSTATE_INITIAL)
|
||||
}
|
||||
}
|
||||
if u.OTPState == int32(model.MFASTATE_READY) {
|
||||
u.MfaMaxSetUp = int32(req_model.MfaLevelSoftware)
|
||||
}
|
||||
}
|
||||
|
91
internal/user/repository/view/model/user_session.go
Normal file
91
internal/user/repository/view/model/user_session.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
const (
|
||||
UserSessionKeySessionID = "id"
|
||||
UserSessionKeyUserAgentID = "user_agent_id"
|
||||
UserSessionKeyUserID = "user_id"
|
||||
UserSessionKeyState = "state"
|
||||
UserSessionKeyResourceOwner = "resource_owner"
|
||||
)
|
||||
|
||||
type UserSessionView struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
State int32 `json:"-" gorm:"column:state"`
|
||||
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id"`
|
||||
UserID string `json:"userID" gorm:"column:user_id"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
PasswordVerification time.Time `json:"-" gorm:"column:password_verification"`
|
||||
MfaSoftwareVerification time.Time `json:"-" gorm:"column:mfa_software_verification"`
|
||||
MfaHardwareVerification time.Time `json:"-" gorm:"column:mfa_hardware_verification"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func UserSessionFromEvent(event *models.Event) (*UserSessionView, error) {
|
||||
v := new(UserSessionView)
|
||||
if err := json.Unmarshal(event.Data, v); err != nil {
|
||||
logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
return nil, caos_errs.ThrowInternal(nil, "MODEL-sd325", "could not unmarshal data")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
|
||||
return &model.UserSessionView{
|
||||
ID: userSession.ID,
|
||||
ChangeDate: userSession.ChangeDate,
|
||||
CreationDate: userSession.CreationDate,
|
||||
ResourceOwner: userSession.ResourceOwner,
|
||||
State: req_model.UserSessionState(userSession.State),
|
||||
UserAgentID: userSession.UserAgentID,
|
||||
UserID: userSession.UserID,
|
||||
UserName: userSession.UserName,
|
||||
PasswordVerification: userSession.PasswordVerification,
|
||||
MfaSoftwareVerification: userSession.MfaSoftwareVerification,
|
||||
MfaHardwareVerification: userSession.MfaHardwareVerification,
|
||||
Sequence: userSession.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView {
|
||||
result := make([]*model.UserSessionView, len(userSessions))
|
||||
for i, s := range userSessions {
|
||||
result[i] = UserSessionToModel(s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *UserSessionView) AppendEvent(event *models.Event) {
|
||||
v.ChangeDate = event.CreationDate
|
||||
switch event.Type {
|
||||
case es_model.UserPasswordCheckSucceeded:
|
||||
v.PasswordVerification = event.CreationDate
|
||||
case es_model.UserPasswordCheckFailed,
|
||||
es_model.UserPasswordChanged:
|
||||
v.PasswordVerification = time.Time{}
|
||||
case es_model.MfaOtpCheckSucceeded:
|
||||
v.MfaSoftwareVerification = event.CreationDate
|
||||
case es_model.MfaOtpCheckFailed,
|
||||
es_model.MfaOtpRemoved:
|
||||
v.MfaSoftwareVerification = time.Time{}
|
||||
case es_model.SignedOut:
|
||||
v.PasswordVerification = time.Time{}
|
||||
v.MfaSoftwareVerification = time.Time{}
|
||||
v.State = int32(req_model.UserSessionStateTerminated)
|
||||
}
|
||||
}
|
67
internal/user/repository/view/model/user_session_query.go
Normal file
67
internal/user/repository/view/model/user_session_query.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
type UserSessionSearchRequest usr_model.UserSessionSearchRequest
|
||||
type UserSessionSearchQuery usr_model.UserSessionSearchQuery
|
||||
type UserSessionSearchKey usr_model.UserSessionSearchKey
|
||||
|
||||
func (req UserSessionSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetSortingColumn() view.ColumnKey {
|
||||
if req.SortingColumn == usr_model.USERSESSIONSEARCHKEY_UNSPECIFIED {
|
||||
return nil
|
||||
}
|
||||
return UserSessionSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req UserSessionSearchRequest) GetQueries() []view.SearchQuery {
|
||||
result := make([]view.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = UserSessionSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetKey() view.ColumnKey {
|
||||
return UserSessionSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetMethod() global_model.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req UserSessionSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key UserSessionSearchKey) ToColumnName() string {
|
||||
switch usr_model.UserSessionSearchKey(key) {
|
||||
case usr_model.USERSESSIONSEARCHKEY_SESSION_ID:
|
||||
return UserSessionKeySessionID
|
||||
case usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID:
|
||||
return UserSessionKeyUserAgentID
|
||||
case usr_model.USERSESSIONSEARCHKEY_USER_ID:
|
||||
return UserSessionKeyUserID
|
||||
case usr_model.USERSESSIONSEARCHKEY_STATE:
|
||||
return UserSessionKeyState
|
||||
case usr_model.USERSESSIONSEARCHKEY_RESOURCEOWNER:
|
||||
return UserSessionKeyResourceOwner
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
90
internal/user/repository/view/model/user_session_test.go
Normal file
90
internal/user/repository/view/model/user_session_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func now() time.Time {
|
||||
return time.Now().UTC().Round(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event *es_models.Event
|
||||
userView *UserSessionView
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *UserSessionView
|
||||
}{
|
||||
{
|
||||
name: "append password check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordCheckSucceeded},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: now()},
|
||||
},
|
||||
{
|
||||
name: "append password check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordCheckFailed},
|
||||
userView: &UserSessionView{PasswordVerification: now()},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}},
|
||||
},
|
||||
{
|
||||
name: "append password changed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordChanged},
|
||||
userView: &UserSessionView{PasswordVerification: now()},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}},
|
||||
},
|
||||
{
|
||||
name: "append otp check succeeded event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckSucceeded},
|
||||
userView: &UserSessionView{},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: now()},
|
||||
},
|
||||
{
|
||||
name: "append otp check failed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckFailed},
|
||||
userView: &UserSessionView{MfaSoftwareVerification: now()},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: time.Time{}},
|
||||
},
|
||||
{
|
||||
name: "append otp removed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckFailed},
|
||||
userView: &UserSessionView{MfaSoftwareVerification: now()},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: time.Time{}},
|
||||
},
|
||||
{
|
||||
name: "append otp removed event",
|
||||
args: args{
|
||||
event: &es_models.Event{CreationDate: now(), Type: es_model.SignedOut},
|
||||
userView: &UserSessionView{PasswordVerification: now(), MfaSoftwareVerification: now()},
|
||||
},
|
||||
result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}, MfaSoftwareVerification: time.Time{}, State: 1},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.userView.AppendEvent(tt.args.event)
|
||||
assert.Equal(t, tt.result, tt.args.userView)
|
||||
})
|
||||
}
|
||||
}
|
@@ -2,10 +2,13 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockUserData(user *es_model.User) []byte {
|
||||
@@ -13,6 +16,11 @@ func mockUserData(user *es_model.User) []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func mockPasswordData(password *es_model.Password) []byte {
|
||||
data, _ := json.Marshal(password)
|
||||
return data
|
||||
}
|
||||
|
||||
func mockProfileData(profile *es_model.Profile) []byte {
|
||||
data, _ := json.Marshal(profile)
|
||||
return data
|
||||
@@ -33,7 +41,7 @@ func mockAddressData(address *es_model.Address) []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func getFullUser() *es_model.User {
|
||||
func getFullUser(password *es_model.Password) *es_model.User {
|
||||
return &es_model.User{
|
||||
Profile: &es_model.Profile{
|
||||
UserName: "UserName",
|
||||
@@ -49,6 +57,7 @@ func getFullUser() *es_model.User {
|
||||
Address: &es_model.Address{
|
||||
Country: "Country",
|
||||
},
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +74,43 @@ func TestUserAppendEvent(t *testing.T) {
|
||||
{
|
||||
name: "append added user event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser())},
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(nil))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL)},
|
||||
},
|
||||
{
|
||||
name: "append added user with password event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(&es_model.Password{Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL), PasswordSet: true},
|
||||
},
|
||||
{
|
||||
name: "append added user with password but change required event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))},
|
||||
user: &UserView{},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL), PasswordSet: true, PasswordChangeRequired: true},
|
||||
},
|
||||
{
|
||||
name: "append password change event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPasswordChanged, ResourceOwner: "OrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), PasswordSet: true},
|
||||
},
|
||||
{
|
||||
name: "append password change with change required event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPasswordChanged, ResourceOwner: "OrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), PasswordSet: true, PasswordChangeRequired: true},
|
||||
},
|
||||
{
|
||||
name: "append change user profile event",
|
||||
args: args{
|
||||
@@ -174,6 +215,14 @@ func TestUserAppendEvent(t *testing.T) {
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), OTPState: int32(model.MFASTATE_UNSPECIFIED)},
|
||||
},
|
||||
{
|
||||
name: "append mfa init skipped event",
|
||||
args: args{
|
||||
event: &es_models.Event{Sequence: 1, CreationDate: time.Now().UTC(), Type: es_model.MfaInitSkipped, AggregateID: "AggregateID", ResourceOwner: "OrgID"},
|
||||
user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)},
|
||||
},
|
||||
result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), MfaInitSkipped: time.Now().UTC()},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -211,6 +260,15 @@ func TestUserAppendEvent(t *testing.T) {
|
||||
if tt.args.user.OTPState != tt.result.OTPState {
|
||||
t.Errorf("got wrong result OTPState: expected: %v, actual: %v ", tt.result.OTPState, tt.args.user.OTPState)
|
||||
}
|
||||
if tt.args.user.MfaInitSkipped.Round(1*time.Second) != tt.result.MfaInitSkipped.Round(1*time.Second) {
|
||||
t.Errorf("got wrong result MfaInitSkipped: expected: %v, actual: %v ", tt.result.MfaInitSkipped.Round(1*time.Second), tt.args.user.MfaInitSkipped.Round(1*time.Second))
|
||||
}
|
||||
if tt.args.user.PasswordSet != tt.result.PasswordSet {
|
||||
t.Errorf("got wrong result PasswordSet: expected: %v, actual: %v ", tt.result.PasswordSet, tt.args.user.PasswordSet)
|
||||
}
|
||||
if tt.args.user.PasswordChangeRequired != tt.result.PasswordChangeRequired {
|
||||
t.Errorf("got wrong result PasswordChangeRequired: expected: %v, actual: %v ", tt.result.PasswordChangeRequired, tt.args.user.PasswordChangeRequired)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
58
internal/user/repository/view/user_session_view.go
Normal file
58
internal/user/repository/view/user_session_view.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
func UserSessionByID(db *gorm.DB, table, sessionID string) (*model.UserSessionView, error) {
|
||||
userSession := new(model.UserSessionView)
|
||||
query := view.PrepareGetByKey(table, model.UserSessionSearchKey(usr_model.USERSESSIONSEARCHKEY_SESSION_ID), sessionID)
|
||||
err := query(db, userSession)
|
||||
return userSession, err
|
||||
}
|
||||
|
||||
func UserSessionByIDs(db *gorm.DB, table, agentID, userID string) (*model.UserSessionView, error) {
|
||||
userSession := new(model.UserSessionView)
|
||||
userAgentQuery := model.UserSessionSearchQuery{
|
||||
Key: usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID,
|
||||
Method: global_model.SEARCHMETHOD_EQUALS,
|
||||
Value: agentID,
|
||||
}
|
||||
userQuery := model.UserSessionSearchQuery{
|
||||
Key: usr_model.USERSESSIONSEARCHKEY_USER_ID,
|
||||
Method: global_model.SEARCHMETHOD_EQUALS,
|
||||
Value: userID,
|
||||
}
|
||||
query := view.PrepareGetByQuery(table, userAgentQuery, userQuery)
|
||||
err := query(db, userSession)
|
||||
return userSession, err
|
||||
}
|
||||
|
||||
func UserSessionsByAgentID(db *gorm.DB, table, agentID string) ([]*model.UserSessionView, error) {
|
||||
userSessions := make([]*model.UserSessionView, 0)
|
||||
userAgentQuery := &usr_model.UserSessionSearchQuery{
|
||||
Key: usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID,
|
||||
Method: global_model.SEARCHMETHOD_EQUALS,
|
||||
Value: agentID,
|
||||
}
|
||||
query := view.PrepareSearchQuery(table, model.UserSessionSearchRequest{
|
||||
Queries: []*usr_model.UserSessionSearchQuery{userAgentQuery},
|
||||
})
|
||||
_, err := query(db, userSessions)
|
||||
return userSessions, err
|
||||
}
|
||||
|
||||
func PutUserSession(db *gorm.DB, table string, session *model.UserSessionView) error {
|
||||
save := view.PrepareSave(table)
|
||||
return save(db, session)
|
||||
}
|
||||
|
||||
func DeleteUserSession(db *gorm.DB, table, sessionID string) error {
|
||||
delete := view.PrepareDeleteByKey(table, model.UserSessionSearchKey(usr_model.USERSESSIONSEARCHKEY_USER_ID), sessionID)
|
||||
return delete(db)
|
||||
}
|
Reference in New Issue
Block a user