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

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