Livio Amstutz 8a5badddf6
feat: Login, OP Support and Auth Queries (#177)
* 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

* begin oidc

* user agent and more

* config

* keys

* key command and query

* add login statics

* key handler

* start login

* login handlers

* lot of fixes

* merge oidc

* add missing exports

* add missing exports

* fix some bugs

* authrequestid in htmls

* getrequest

* update auth request

* fix userid check

* add username to authrequest

* fix user session and auth request handling

* fix UserSessionsByAgentID

* fix auth request tests

* fix user session on UserPasswordChanged and MfaOtpRemoved

* fix MfaTypesSetupPossible

* handle mfa

* fill username

* auth request query checks new events

* fix userSessionByIDs

* fix tokens

* fix userSessionByIDs test

* add user selection

* init code

* user code creation date

* add init user step

* add verification failed types

* add verification failures

* verify init code

* user init code handle

* user init code handle

* fix userSessionByIDs

* update logging

* user agent cookie

* browserinfo from request

* add DeleteAuthRequest

* add static login files to binary

* add login statik to build

* move generate to separate file and remove statik.go files

* remove static dirs from startup.yaml

* generate into separate namespaces

* merge master

* auth request code

* auth request type mapping

* fix keys

* improve tokens

* improve register and basic styling

* fix ailerons font

* improve password reset

* add audience to token

* all oidc apps as audience

* fix test nextStep

* fix email texts

* remove "not set"

* lot of style changes

* improve copy to clipboard

* fix footer

* add cookie handler

* remove placeholders

* fix compilation after merge

* fix auth config

* remove comments

* typo

* use new secrets store

* change default pws to match default policy

* fixes

* add todo

* enable login

* fix db name

* Auth queries (#179)

* my usersession

* org structure/ auth handlers

* working user grant spooler

* auth internal user grants

* search my project orgs

* remove permissions file

* my zitadel permissions

* my zitadel permissions

* remove unused code

* authz

* app searches in view

* token verification

* fix user grant load

* fix tests

* fix tests

* read configs

* remove unused const

* remove todos

* env variables

* app_name

* working authz

* search projects

* global resourceowner

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* model2 rename

* at least it works

* check token expiry

* search my user grants

* remove token table from authz

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix test

* fix ports and enable console

Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
2020-06-05 07:50:04 +02:00

973 lines
32 KiB
Go

package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/id"
policy_model "github.com/caos/zitadel/internal/policy/model"
"github.com/pquerna/otp/totp"
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"
caos_errs "github.com/caos/zitadel/internal/errors"
es_int "github.com/caos/zitadel/internal/eventstore"
es_models "github.com/caos/zitadel/internal/eventstore/models"
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
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"
)
type UserEventstore struct {
es_int.Eventstore
userCache *UserCache
idGenerator id.Generator
PasswordAlg crypto.HashAlgorithm
InitializeUserCode crypto.Generator
EmailVerificationCode crypto.Generator
PhoneVerificationCode crypto.Generator
PasswordVerificationCode crypto.Generator
Multifactors global_model.Multifactors
validateTOTP func(string, string) bool
}
type UserConfig struct {
es_int.Eventstore
Cache *config.CacheConfig
PasswordSaltCost int
}
func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventstore, error) {
userCache, err := StartCache(conf.Cache)
if err != nil {
return nil, err
}
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
if err != nil {
return nil, err
}
initCodeGen := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.InitializeUserCode, aesCrypto)
emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto)
phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto)
passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, aesCrypto)
aesOtpCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey)
passwordAlg := crypto.NewBCrypt(systemDefaults.SecretGenerators.PasswordSaltCost)
return &UserEventstore{
Eventstore: conf.Eventstore,
userCache: userCache,
idGenerator: id.SonyFlakeGenerator,
InitializeUserCode: initCodeGen,
EmailVerificationCode: emailVerificationCode,
PhoneVerificationCode: phoneVerificationCode,
PasswordVerificationCode: passwordVerificationCode,
Multifactors: global_model.Multifactors{
OTP: global_model.OTP{
CryptoMFA: aesOtpCrypto,
Issuer: systemDefaults.Multifactors.OTP.Issuer,
},
},
PasswordAlg: passwordAlg,
validateTOTP: totp.Validate,
}, nil
}
func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.User, error) {
user := es.userCache.getUser(id)
query, err := UserByIDQuery(user.AggregateID, user.Sequence)
if err != nil {
return nil, err
}
err = es_sdk.Filter(ctx, es.FilterEvents, user.AppendEvents, query)
if err != nil && caos_errs.IsNotFound(err) && user.Sequence == 0 {
return nil, err
}
es.userCache.cacheUser(user)
return model.UserToModel(user), nil
}
func (es *UserEventstore) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) {
query, err := UserByIDQuery(id, sequence)
if err != nil {
return nil, err
}
return es.FilterEvents(ctx, query)
}
func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) {
user.SetEmailAsUsername()
if !user.IsValid() {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "User is invalid")
}
id, err := es.idGenerator.Next()
if err != nil {
return nil, nil, err
}
user.AggregateID = id
err = user.HashPasswordIfExisting(policy, es.PasswordAlg, true)
if err != nil {
return nil, nil, err
}
err = user.GenerateInitCodeIfNeeded(es.InitializeUserCode)
if err != nil {
return nil, nil, err
}
err = user.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
if err != nil {
return nil, nil, err
}
repoUser := model.UserFromModel(user)
repoInitCode := model.InitCodeFromModel(user.InitCode)
repoPhoneCode := model.PhoneCodeFromModel(user.PhoneCode)
createAggregates, err := UserCreateAggregate(ctx, es.AggregateCreator(), repoUser, repoInitCode, repoPhoneCode, resourceOwner)
return repoUser, createAggregates, err
}
func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy) (*usr_model.User, error) {
repoUser, aggregates, err := es.PrepareCreateUser(ctx, user, policy, "")
if err != nil {
return nil, err
}
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregates...)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.UserToModel(repoUser), nil
}
func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) {
user.SetEmailAsUsername()
if !user.IsValid() || user.Password == nil || user.SecretString == "" {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid")
}
id, err := es.idGenerator.Next()
if err != nil {
return nil, nil, err
}
user.AggregateID = id
err = user.HashPasswordIfExisting(policy, es.PasswordAlg, false)
if err != nil {
return nil, nil, err
}
err = user.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
if err != nil {
return nil, nil, err
}
repoUser := model.UserFromModel(user)
repoEmailCode := model.EmailCodeFromModel(user.EmailCode)
aggregates, err := UserRegisterAggregate(ctx, es.AggregateCreator(), repoUser, resourceOwner, repoEmailCode)
return repoUser, aggregates, err
}
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*usr_model.User, error) {
repoUser, createAggregates, err := es.PrepareRegisterUser(ctx, user, policy, resourceOwner)
if err != nil {
return nil, err
}
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregates...)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.UserToModel(repoUser), nil
}
func (es *UserEventstore) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if existing.IsInactive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-die45", "cant deactivate inactive user")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserDeactivateAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) ReactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsInactive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do94s", "user must be inactive")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserReactivateAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) LockUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsActive() && !existing.IsInitial() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di83s", "user must be active or initial")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserLockAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) UnlockUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsLocked() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dks83", "user must be locked")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserUnlockAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d8diw", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.InitCode != nil {
return user.InitCode, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "init code not found")
}
func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
initCode := new(usr_model.InitUserCode)
err = initCode.GenerateInitUserCode(es.InitializeUserCode)
if err != nil {
return nil, err
}
repoUser := model.UserFromModel(user)
repoInitCode := model.InitCodeFromModel(initCode)
agg := UserInitCodeAggregate(es.AggregateCreator(), repoUser, repoInitCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.InitCodeToModel(repoUser.InitCode), nil
}
func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := UserInitCodeSentAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) VerifyInitCode(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, userID, verificationCode, password string) error {
if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "userId or Code empty")
}
pw := &usr_model.Password{SecretString: password}
err := pw.HashPasswordIfExisting(policy, es.PasswordAlg, false)
if err != nil {
return err
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.InitCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-spo9W", "code not found")
}
repoPassword := model.PasswordFromModel(pw)
repoExisting := model.UserFromModel(existing)
var updateAggregate func(ctx context.Context) (*es_models.Aggregate, error)
if err := crypto.VerifyCode(existing.InitCode.CreationDate, existing.InitCode.Expiry, existing.InitCode.Code, verificationCode, es.InitializeUserCode); err != nil {
updateAggregate = InitCodeCheckFailedAggregate(es.AggregateCreator(), repoExisting)
} else {
updateAggregate = InitCodeVerifiedAggregate(es.AggregateCreator(), repoExisting, repoPassword)
}
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := SkipMfaAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (*usr_model.Password, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Password != nil {
return user.Password, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "password not found")
}
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, policy *policy_model.PasswordComplexityPolicy, 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, policy, password.SecretString, true)
}
func (es *UserEventstore) SetPassword(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, 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, policy, password, false)
return err
}
func (es *UserEventstore) ChangePassword(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, 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, policy, new, false)
}
func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, password string, onetime bool) (*usr_model.Password, error) {
pw := &usr_model.Password{SecretString: password}
err := pw.HashPasswordIfExisting(policy, es.PasswordAlg, onetime)
if err != nil {
return nil, err
}
repoPassword := model.PasswordFromModel(pw)
repoUser := model.UserFromModel(user)
agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.PasswordToModel(repoUser.Password), nil
}
func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string, notifyType usr_model.NotificationType) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
passwordCode := new(model.PasswordCode)
err = es.generatePasswordCode(passwordCode, notifyType)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := RequestSetPassword(es.AggregateCreator(), repoUser, passwordCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Profile != nil {
return user.Profile, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dk23f", "profile not found")
}
func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) {
if !profile.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "profile is invalid")
}
existing, err := es.UserByID(ctx, profile.AggregateID)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.ProfileFromModel(profile)
updateAggregate := ProfileChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.ProfileToModel(repoExisting.Profile), nil
}
func (es *UserEventstore) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Email != nil {
return user.Email, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dki89", "email not found")
}
func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Email) (*usr_model.Email, error) {
if !email.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "email is invalid")
}
existing, err := es.UserByID(ctx, email.AggregateID)
if err != nil {
return nil, err
}
emailCode, err := email.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.EmailFromModel(email)
repoEmailCode := model.EmailCodeFromModel(emailCode)
updateAggregates, err := EmailChangeAggregate(ctx, es.AggregateCreator(), repoExisting, repoNew, repoEmailCode)
if err != nil {
return nil, err
}
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregates...)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.EmailToModel(repoExisting.Email), nil
}
func (es *UserEventstore) VerifyEmail(ctx context.Context, userID, verificationCode string) error {
if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "userId or Code empty")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.EmailCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-lso9w", "code not found")
}
err = crypto.VerifyCode(existing.EmailCode.CreationDate, existing.EmailCode.Expiry, existing.EmailCode.Code, verificationCode, es.EmailVerificationCode)
if err == nil {
return es.setEmailVerifyResult(ctx, existing, EmailVerifiedAggregate)
}
if err := es.setEmailVerifyResult(ctx, existing, EmailVerificationFailedAggregate); err != nil {
return err
}
return caos_errs.ThrowInvalidArgument(err, "EVENT-dtGaa", "invalid code")
}
func (es *UserEventstore) setEmailVerifyResult(ctx context.Context, existing *usr_model.User, check func(aggCreator *es_models.AggregateCreator, existing *model.User) es_sdk.AggregateFunc) error {
repoExisting := model.UserFromModel(existing)
err := es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, check(es.AggregateCreator(), repoExisting))
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "userID missing")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.Email == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "no email existing")
}
if existing.IsEmailVerified {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "email already verified")
}
emailCode := new(usr_model.EmailCode)
err = emailCode.GenerateEmailCode(es.EmailVerificationCode)
if err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
repoEmailCode := model.EmailCodeFromModel(emailCode)
updateAggregate := EmailVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoEmailCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-spo0w", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := EmailCodeSentAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, 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.Phone != nil {
return user.Phone, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-pos9e", "phone not found")
}
func (es *UserEventstore) ChangePhone(ctx context.Context, phone *usr_model.Phone) (*usr_model.Phone, error) {
if !phone.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9s4", "phone is invalid")
}
existing, err := es.UserByID(ctx, phone.AggregateID)
if err != nil {
return nil, err
}
phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.PhoneFromModel(phone)
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
updateAggregate := PhoneChangeAggregate(es.AggregateCreator(), repoExisting, repoNew, repoPhoneCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.PhoneToModel(repoExisting.Phone), nil
}
func (es *UserEventstore) VerifyPhone(ctx context.Context, userID, verificationCode string) error {
if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dsi8s", "userId or Code empty")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.PhoneCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-slp0s", "code not found")
}
err = crypto.VerifyCode(existing.PhoneCode.CreationDate, existing.PhoneCode.Expiry, existing.PhoneCode.Code, verificationCode, es.PhoneVerificationCode)
if err == nil {
return es.setPhoneVerifyResult(ctx, existing, PhoneVerifiedAggregate)
}
if err := es.setPhoneVerifyResult(ctx, existing, PhoneVerificationFailedAggregate); err != nil {
return err
}
return caos_errs.ThrowInvalidArgument(err, "EVENT-dsf4G", "invalid code")
}
func (es *UserEventstore) setPhoneVerifyResult(ctx context.Context, existing *usr_model.User, check func(aggCreator *es_models.AggregateCreator, existing *model.User) es_sdk.AggregateFunc) error {
repoExisting := model.UserFromModel(existing)
err := es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, check(es.AggregateCreator(), repoExisting))
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9sw", "userID missing")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.Phone == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp9fs", "no phone existing")
}
if existing.IsPhoneVerified {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sleis", "phone already verified")
}
phoneCode := new(usr_model.PhoneCode)
err = phoneCode.GeneratePhoneCode(es.PhoneVerificationCode)
if err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
updateAggregate := PhoneVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoPhoneCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0wa", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := PhoneCodeSentAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Address != nil {
return user.Address, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-so9wa", "address not found")
}
func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) {
existing, err := es.UserByID(ctx, address.AggregateID)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.AddressFromModel(address)
updateAggregate := AddressChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.AddressToModel(repoExisting.Address), nil
}
func (es *UserEventstore) AddOTP(ctx context.Context, userID string) (*usr_model.OTP, error) {
existing, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if existing.IsOTPReady() {
return nil, caos_errs.ThrowAlreadyExists(nil, "EVENT-do9se", "user has already configured otp")
}
key, err := totp.Generate(totp.GenerateOpts{Issuer: es.Multifactors.OTP.Issuer, AccountName: userID})
if err != nil {
return nil, err
}
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), es.Multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
repoOtp := &model.OTP{Secret: encryptedSecret}
repoExisting := model.UserFromModel(existing)
updateAggregate := MfaOTPAddAggregate(es.AggregateCreator(), repoExisting, repoOtp)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
otp := model.OTPToModel(repoExisting.OTP)
otp.Url = key.URL()
otp.SecretString = key.Secret()
return otp, nil
}
func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.OTP == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
}
repoExisting := model.UserFromModel(existing)
updateAggregate := MfaOTPRemoveAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code string) error {
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if user.OTP == nil || user.IsOTPReady() {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "otp not existing or already set up")
}
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
}
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
}