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:
Livio Amstutz
2020-05-18 12:06:36 +02:00
committed by GitHub
parent 8203f2dad3
commit effc2c23c2
83 changed files with 5732 additions and 1177 deletions

View 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
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}
})
}
}

View 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,
}
}

View File

@@ -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"
)

View File

@@ -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})
}
}

View File

@@ -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)
}
}

View 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)
}
}

View 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 ""
}
}

View 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)
})
}
}

View File

@@ -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)
}
})
}
}

View 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)
}