feat: new user auth api (#1168)

* fix: correct selectors for extended writemodel

* fix: no previous checks in eventstore

* start check previous

* feat: auth user commands

* feat: auth user commands

* feat: auth user commands

* feat: otp

* feat: corrections from pr merge

* feat: webauthn

* feat: comment old webauthn

* feat: refactor user, human, machine

* feat: webauth command side

* feat: command and query side in login

* feat: fix user writemodel append events

* fix: remove creation dates on command side

* fix: remove previous sequence

* previous sequence

* fix: external idps

* Update internal/api/grpc/management/user.go

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

* Update internal/v2/command/user_human_email.go

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

* fix: pr changes

* fix: phone verification

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi
2021-01-15 09:32:59 +01:00
committed by GitHub
parent e5731b0d3b
commit 959530ddad
74 changed files with 1554 additions and 1519 deletions

View File

@@ -2,6 +2,8 @@ package command
import (
"context"
global_model "github.com/caos/zitadel/internal/model"
webauthn_helper "github.com/caos/zitadel/internal/webauthn"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
@@ -9,6 +11,7 @@ import (
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/telemetry/tracing"
iam_repo "github.com/caos/zitadel/internal/v2/repository/iam"
usr_repo "github.com/caos/zitadel/internal/v2/repository/user"
)
type CommandSide struct {
@@ -18,14 +21,18 @@ type CommandSide struct {
idpConfigSecretCrypto crypto.Crypto
userPasswordAlg crypto.HashAlgorithm
initializeUserCode crypto.Generator
emailVerificationCode crypto.Generator
phoneVerificationCode crypto.Generator
passwordVerificationCode crypto.Generator
machineKeyAlg crypto.EncryptionAlgorithm
machineKeySize int
userPasswordAlg crypto.HashAlgorithm
initializeUserCode crypto.Generator
emailVerificationCode crypto.Generator
phoneVerificationCode crypto.Generator
passwordVerificationCode crypto.Generator
machineKeyAlg crypto.EncryptionAlgorithm
machineKeySize int
//TODO: remove global model, or move to domain
multifactors global_model.Multifactors
applicationSecretGenerator crypto.Generator
webauthn *webauthn_helper.WebAuthN
}
type Config struct {
@@ -40,6 +47,7 @@ func StartCommandSide(config *Config) (repo *CommandSide, err error) {
iamDomain: config.SystemDefaults.Domain,
}
iam_repo.RegisterEventMappers(repo.eventstore)
usr_repo.RegisterEventMappers(repo.eventstore)
//TODO: simplify!!!!
repo.idpConfigSecretCrypto, err = crypto.NewAESCrypto(config.SystemDefaults.IDPConfigVerificationKey)
@@ -58,8 +66,23 @@ func StartCommandSide(config *Config) (repo *CommandSide, err error) {
repo.machineKeyAlg = userEncryptionAlgorithm
repo.machineKeySize = int(config.SystemDefaults.SecretGenerators.MachineKeySize)
aesOTPCrypto, err := crypto.NewAESCrypto(config.SystemDefaults.Multifactors.OTP.VerificationKey)
if err != nil {
return nil, err
}
repo.multifactors = global_model.Multifactors{
OTP: global_model.OTP{
CryptoMFA: aesOTPCrypto,
Issuer: config.SystemDefaults.Multifactors.OTP.Issuer,
},
}
passwordAlg := crypto.NewBCrypt(config.SystemDefaults.SecretGenerators.PasswordSaltCost)
repo.applicationSecretGenerator = crypto.NewHashGenerator(config.SystemDefaults.SecretGenerators.ClientSecretGenerator, passwordAlg)
web, err := webauthn_helper.StartServer(config.SystemDefaults.WebAuthN)
if err != nil {
return nil, err
}
repo.webauthn = web
return repo, nil
}

View File

@@ -9,7 +9,18 @@ import (
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) SetUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.User) error {
func (r *CommandSide) getOrg(ctx context.Context, orgID string) (*domain.Org, error) {
writeModel, err := r.getOrgWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
}
if writeModel.State == domain.OrgStateActive {
return nil, caos_errs.ThrowInternal(err, "COMMAND-4M9sf", "Errors.Org.NotFound")
}
return orgWriteModelToOrg(writeModel), nil
}
func (r *CommandSide) SetUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.Human) error {
orgAgg, userAgg, orgMemberAgg, err := r.setUpOrg(ctx, organisation, admin)
if err != nil {
return err
@@ -19,13 +30,13 @@ func (r *CommandSide) SetUpOrg(ctx context.Context, organisation *domain.Org, ad
return err
}
func (r *CommandSide) setUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.User) (*org.Aggregate, *user.Aggregate, *org.Aggregate, error) {
func (r *CommandSide) setUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.Human) (*org.Aggregate, *user.Aggregate, *org.Aggregate, error) {
orgAgg, _, err := r.addOrg(ctx, organisation)
if err != nil {
return nil, nil, nil, err
}
userAgg, _, err := r.addHuman(ctx, orgAgg.ID(), admin.UserName, admin.Human)
userAgg, _, err := r.addHuman(ctx, orgAgg.ID(), admin)
if err != nil {
return nil, nil, nil, err
}

View File

@@ -6,9 +6,10 @@ import (
func orgWriteModelToOrg(wm *OrgWriteModel) *domain.Org {
return &domain.Org{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
Name: wm.Name,
State: wm.State,
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
Name: wm.Name,
State: wm.State,
PrimaryDomain: wm.PrimaryDomain,
}
}

View File

@@ -10,8 +10,9 @@ import (
type OrgWriteModel struct {
eventstore.WriteModel
Name string
State domain.OrgState
Name string
State domain.OrgState
PrimaryDomain string
}
func NewOrgWriteModel(orgID string) *OrgWriteModel {
@@ -30,6 +31,8 @@ func (wm *OrgWriteModel) AppendEvents(events ...eventstore.EventReader) {
case *org.OrgAddedEvent,
*iam.LabelPolicyChangedEvent:
wm.WriteModel.AppendEvents(e)
case *org.DomainPrimarySetEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
@@ -42,6 +45,8 @@ func (wm *OrgWriteModel) Reduce() error {
wm.State = domain.OrgStateActive
case *org.OrgChangedEvent:
wm.Name = e.Name
case *org.DomainPrimarySetEvent:
wm.PrimaryDomain = e.Domain
}
}
return nil

View File

@@ -34,7 +34,7 @@ func (r *CommandSide) addOrgIAMPolicy(ctx context.Context, orgAgg *org.Aggregate
return err
}
if addedPolicy.State == domain.PolicyStateActive {
return caos_errs.ThrowAlreadyExists(nil, "ORG-5M0ds", "Errors.Org.OrgIAMPolicy.AlreadyExists")
return caos_errs.ThrowAlreadyExists(nil, "ORG-1M8ds", "Errors.Org.OrgIAMPolicy.AlreadyExists")
}
orgAgg.PushEvents(org.NewOrgIAMPolicyAddedEvent(ctx, policy.UserLoginMustBeDomain))
return nil

View File

@@ -105,20 +105,18 @@ func (r *CommandSide) SetupStep1(ctx context.Context, step1 *Step1) error {
Name: organisation.Name,
Domains: []*domain.OrgDomain{{Domain: organisation.Domain}},
},
&domain.User{
UserName: organisation.Owner.UserName,
Human: &domain.Human{
Profile: &domain.Profile{
FirstName: organisation.Owner.FirstName,
LastName: organisation.Owner.LastName,
},
Password: &domain.Password{
SecretString: organisation.Owner.Password,
},
Email: &domain.Email{
EmailAddress: organisation.Owner.Email,
IsEmailVerified: true,
},
&domain.Human{
Username: organisation.Owner.UserName,
Profile: &domain.Profile{
FirstName: organisation.Owner.FirstName,
LastName: organisation.Owner.LastName,
},
Password: &domain.Password{
SecretString: organisation.Owner.Password,
},
Email: &domain.Email{
EmailAddress: organisation.Owner.Email,
IsEmailVerified: true,
},
})
if err != nil {

View File

@@ -9,42 +9,6 @@ import (
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) AddUser(ctx context.Context, orgID string, user *domain.User) (*domain.User, error) {
if !user.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2N9fs", "Errors.User.Invalid")
}
if user.Human != nil {
human, err := r.AddHuman(ctx, orgID, user.UserName, user.Human)
if err != nil {
return nil, err
}
return &domain.User{UserName: user.UserName, Human: human}, nil
} else if user.Machine != nil {
machine, err := r.AddMachine(ctx, orgID, user.UserName, user.Machine)
if err != nil {
return nil, err
}
return &domain.User{UserName: user.UserName, Machine: machine}, nil
}
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-8K0df", "Errors.User.TypeUndefined")
}
func (r *CommandSide) RegisterUser(ctx context.Context, orgID string, user *domain.User) (*domain.User, error) {
if !user.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2N9fs", "Errors.User.Invalid")
}
if user.Human != nil {
human, err := r.RegisterHuman(ctx, orgID, user.UserName, user.Human, nil)
if err != nil {
return nil, err
}
return &domain.User{UserName: user.UserName, Human: human}, nil
}
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-8K0df", "Errors.User.TypeUndefined")
}
func (r *CommandSide) ChangeUsername(ctx context.Context, orgID, userID, userName string) error {
if orgID == "" || userID == "" || userName == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2N9fs", "Errors.IDMissing")
@@ -75,100 +39,84 @@ func (r *CommandSide) ChangeUsername(ctx context.Context, orgID, userID, userNam
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) DeactivateUser(ctx context.Context, userID, resourceOwner string) (*domain.User, error) {
func (r *CommandSide) DeactivateUser(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-m0gDf", "Errors.User.UserIDMissing")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-m0gDf", "Errors.User.UserIDMissing")
}
existingUser, err := r.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9ds", "Errors.User.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-3M9ds", "Errors.User.NotFound")
}
if existingUser.UserState == domain.UserStateInactive {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sf", "Errors.User.AlreadyInactive")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sf", "Errors.User.AlreadyInactive")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserDeactivatedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingUser, userAgg)
if err != nil {
return nil, err
}
return writeModelToUser(existingUser), nil
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) ReactivateUser(ctx context.Context, userID, resourceOwner string) (*domain.User, error) {
func (r *CommandSide) ReactivateUser(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M9ds", "Errors.User.UserIDMissing")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M9ds", "Errors.User.UserIDMissing")
}
existingUser, err := r.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-4M0sd", "Errors.User.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-4M0sd", "Errors.User.NotFound")
}
if existingUser.UserState != domain.UserStateInactive {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0sf", "Errors.User.NotInactive")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0sf", "Errors.User.NotInactive")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserReactivatedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingUser, userAgg)
if err != nil {
return nil, err
}
return writeModelToUser(existingUser), nil
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) LockUser(ctx context.Context, userID, resourceOwner string) (*domain.User, error) {
func (r *CommandSide) LockUser(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0sd", "Errors.User.UserIDMissing")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0sd", "Errors.User.UserIDMissing")
}
existingUser, err := r.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-5M9fs", "Errors.User.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-5M9fs", "Errors.User.NotFound")
}
if existingUser.UserState != domain.UserStateActive && existingUser.UserState != domain.UserStateInitial {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.ShouldBeActiveOrInitial")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.ShouldBeActiveOrInitial")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserLockedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingUser, userAgg)
if err != nil {
return nil, err
}
return writeModelToUser(existingUser), nil
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) UnlockUser(ctx context.Context, userID, resourceOwner string) (*domain.User, error) {
func (r *CommandSide) UnlockUser(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M0dse", "Errors.User.UserIDMissing")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M0dse", "Errors.User.UserIDMissing")
}
existingUser, err := r.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-M0dos", "Errors.User.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-M0dos", "Errors.User.NotFound")
}
if existingUser.UserState != domain.UserStateLocked {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.NotLocked")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.NotLocked")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserUnlockedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingUser, userAgg)
if err != nil {
return nil, err
}
return writeModelToUser(existingUser), nil
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) RemoveUser(ctx context.Context, userID, resourceOwner string) error {
@@ -201,3 +149,15 @@ func (r *CommandSide) userWriteModelByID(ctx context.Context, userID, resourceOw
}
return writeModel, nil
}
func (r *CommandSide) userReadModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserWriteModel(userID, resourceOwner)
err = r.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}

View File

@@ -4,17 +4,11 @@ import (
"github.com/caos/zitadel/internal/v2/domain"
)
func writeModelToUser(wm *UserWriteModel) *domain.User {
return &domain.User{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
UserName: wm.UserName,
State: wm.UserState,
}
}
func writeModelToHuman(wm *HumanWriteModel) *domain.Human {
return &domain.Human{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
Username: wm.UserName,
State: wm.UserState,
Profile: &domain.Profile{
FirstName: wm.FirstName,
LastName: wm.LastName,
@@ -82,3 +76,34 @@ func writeModelToMachine(wm *MachineWriteModel) *domain.Machine {
Description: wm.Description,
}
}
func readModelToU2FTokens(wm *HumanU2FTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
for i, token := range wm.WebAuthNTokens {
tokens[i] = writeModelToWebAuthN(token)
}
return tokens
}
func readModelToPasswordlessTokens(wm *HumanPasswordlessTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
for i, token := range wm.WebAuthNTokens {
tokens[i] = writeModelToWebAuthN(token)
}
return tokens
}
func writeModelToWebAuthN(wm *HumanWebAuthNWriteModel) *domain.WebAuthNToken {
return &domain.WebAuthNToken{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
WebAuthNTokenID: wm.WebauthNTokenID,
Challenge: wm.Challenge,
KeyID: wm.KeyID,
PublicKey: wm.PublicKey,
AttestationType: wm.AttestationType,
AAGUID: wm.AAGUID,
SignCount: wm.SignCount,
WebAuthNTokenName: wm.WebAuthNTokenName,
State: wm.State,
}
}

View File

@@ -2,14 +2,26 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore/v2"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) AddHuman(ctx context.Context, orgID, username string, human *domain.Human) (*domain.Human, error) {
userAgg, addedHuman, err := r.addHuman(ctx, orgID, username, human)
func (r *CommandSide) getHuman(ctx context.Context, userID, resourceowner string) (*domain.Human, error) {
writeModel, err := r.getHumanWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
if writeModel.UserState == domain.UserStateUnspecified || writeModel.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound")
}
return writeModelToHuman(writeModel), nil
}
func (r *CommandSide) AddHuman(ctx context.Context, orgID string, human *domain.Human) (*domain.Human, error) {
userAgg, addedHuman, err := r.addHuman(ctx, orgID, human)
if err != nil {
return nil, err
}
@@ -21,10 +33,34 @@ func (r *CommandSide) AddHuman(ctx context.Context, orgID, username string, huma
return writeModelToHuman(addedHuman), nil
}
func (r *CommandSide) addHuman(ctx context.Context, orgID, username string, human *domain.Human) (*user.Aggregate, *HumanWriteModel, error) {
func (r *CommandSide) addHuman(ctx context.Context, orgID string, human *domain.Human) (*user.Aggregate, *HumanWriteModel, error) {
if !human.IsValid() {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M90d", "Errors.User.Invalid")
}
return r.createHuman(ctx, orgID, human, nil, false)
}
func (r *CommandSide) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP) (*domain.Human, error) {
userAgg, addedHuman, err := r.registerHuman(ctx, orgID, human, externalIDP)
if err != nil {
return nil, err
}
err = r.eventstore.PushAggregate(ctx, addedHuman, userAgg)
if err != nil {
return nil, err
}
return writeModelToHuman(addedHuman), nil
}
func (r *CommandSide) registerHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP) (*user.Aggregate, *HumanWriteModel, error) {
if !human.IsValid() || externalIDP == nil && (human.Password == nil || human.SecretString == "") {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-9dk45", "Errors.User.Invalid")
}
return r.createHuman(ctx, orgID, human, externalIDP, true)
}
func (r *CommandSide) createHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, selfregister bool) (*user.Aggregate, *HumanWriteModel, error) {
userID, err := r.idGenerator.Next()
if err != nil {
return nil, nil, err
@@ -40,8 +76,8 @@ func (r *CommandSide) addHuman(ctx context.Context, orgID, username string, huma
}
addedHuman := NewHumanWriteModel(human.AggregateID, orgID)
//TODO: Check Unique Username
if err := human.CheckOrgIAMPolicy(username, orgIAMPolicy); err != nil {
//TODO: Check Unique Username or unique external idp
if err := human.CheckOrgIAMPolicy(human.Username, orgIAMPolicy); err != nil {
return nil, nil, err
}
human.SetNamesAsDisplayname()
@@ -50,6 +86,73 @@ func (r *CommandSide) addHuman(ctx context.Context, orgID, username string, huma
}
userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel)
var createEvent eventstore.EventPusher
if selfregister {
createEvent = createRegisterHumanEvent(ctx, human.Username, human)
} else {
createEvent = createAddHumanEvent(ctx, human.Username, human)
}
userAgg.PushEvents(createEvent)
if externalIDP != nil {
if !externalIDP.IsValid() {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4Dj9s", "Errors.User.ExternalIDP.Invalid")
}
//TODO: check if idpconfig exists
userAgg.PushEvents(user.NewHumanExternalIDPAddedEvent(ctx, externalIDP.IDPConfigID, externalIDP.DisplayName))
}
if human.IsInitialState() {
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return nil, nil, err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
}
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
}
if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified {
phoneCode, err := domain.NewPhoneCode(r.phoneVerificationCode)
if err != nil {
return nil, nil, err
}
user.NewHumanPhoneCodeAddedEvent(ctx, phoneCode.Code, phoneCode.Expiry)
} else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified {
userAgg.PushEvents(user.NewHumanPhoneVerifiedEvent(ctx))
}
return userAgg, addedHuman, nil
}
func (r *CommandSide) ResendInitialMail(ctx context.Context, userID, email, resourceowner string) (err error) {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.UserIDMissing")
}
existingEmail, err := r.emailWriteModel(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9df", "Errors.User.NotFound")
}
if existingEmail.UserState != domain.UserStateInitial {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.AlreadyInitialised")
}
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
if email != "" && existingEmail.Email != email {
changedEvent, _ := existingEmail.NewChangedEvent(ctx, email)
userAgg.PushEvents(changedEvent)
}
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
return r.eventstore.PushAggregate(ctx, existingEmail, userAgg)
}
func createAddHumanEvent(ctx context.Context, username string, human *domain.Human) *user.HumanAddedEvent {
addEvent := user.NewHumanAddedEvent(
ctx,
username,
@@ -75,60 +178,10 @@ func (r *CommandSide) addHuman(ctx context.Context, orgID, username string, huma
if human.Password != nil {
addEvent.AddPasswordData(human.SecretCrypto, human.ChangeRequired)
}
userAgg.PushEvents(addEvent)
if human.IsInitialState() {
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return nil, nil, err
}
user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry)
}
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
}
if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified {
phoneCode, err := domain.NewPhoneCode(r.phoneVerificationCode)
if err != nil {
return nil, nil, err
}
user.NewHumanPhoneCodeAddedEvent(ctx, phoneCode.Code, phoneCode.Expiry)
} else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified {
userAgg.PushEvents(user.NewHumanPhoneVerifiedEvent(ctx))
}
return userAgg, addedHuman, nil
return addEvent
}
func (r *CommandSide) RegisterHuman(ctx context.Context, orgID, username string, human *domain.Human, externalIDP *domain.ExternalIDP) (*domain.Human, error) {
if !human.IsValid() || externalIDP == nil && (human.Password == nil || human.SecretString == "") {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-9dk45", "Errors.User.Invalid")
}
userID, err := r.idGenerator.Next()
if err != nil {
return nil, err
}
human.AggregateID = userID
orgIAMPolicy, err := r.getOrgIAMPolicy(ctx, orgID)
if err != nil {
return nil, err
}
pwPolicy, err := r.GetOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, err
}
addedHuman := NewHumanWriteModel(human.AggregateID, orgID)
//TODO: Check Unique Username or unique external idp
if err := human.CheckOrgIAMPolicy(username, orgIAMPolicy); err != nil {
return nil, err
}
human.SetNamesAsDisplayname()
if err := human.HashPasswordIfExisting(pwPolicy, r.userPasswordAlg, true); err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel)
func createRegisterHumanEvent(ctx context.Context, username string, human *domain.Human) *user.HumanRegisteredEvent {
addEvent := user.NewHumanRegisteredEvent(
ctx,
username,
@@ -154,61 +207,14 @@ func (r *CommandSide) RegisterHuman(ctx context.Context, orgID, username string,
if human.Password != nil {
addEvent.AddPasswordData(human.SecretCrypto, human.ChangeRequired)
}
userAgg.PushEvents(addEvent)
//TODO: Add External IDP Event
if human.IsInitialState() {
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return nil, err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
}
return addEvent
}
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
}
if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified {
phoneCode, err := domain.NewPhoneCode(r.phoneVerificationCode)
if err != nil {
return nil, err
}
userAgg.PushEvents(user.NewHumanPhoneCodeAddedEvent(ctx, phoneCode.Code, phoneCode.Expiry))
} else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified {
userAgg.PushEvents(user.NewHumanPhoneVerifiedEvent(ctx))
}
err = r.eventstore.PushAggregate(ctx, addedHuman, userAgg)
func (r *CommandSide) getHumanWriteModelByID(ctx context.Context, userID, resourceowner string) (*HumanWriteModel, error) {
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, humanWriteModel)
if err != nil {
return nil, err
}
return writeModelToHuman(addedHuman), nil
}
func (r *CommandSide) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string) (err error) {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.UserIDMissing")
}
existingEmail, err := r.emailWriteModel(ctx, userID, resourceOwner)
if err != nil {
return err
}
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9df", "Errors.User.NotFound")
}
if existingEmail.UserState != domain.UserStateInitial {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.AlreadyInitialised")
}
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
if email != "" && existingEmail.Email != email {
changedEvent, _ := existingEmail.NewChangedEvent(ctx, email)
userAgg.PushEvents(changedEvent)
}
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
return r.eventstore.PushAggregate(ctx, existingEmail, userAgg)
return humanWriteModel, nil
}

View File

@@ -30,16 +30,7 @@ func NewHumanAddressWriteModel(userID, resourceOwner string) *HumanAddressWriteM
}
func (wm *HumanAddressWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanAddressChangedEvent:
wm.AppendEvents(e)
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanAddressWriteModel) Reduce() error {

View File

@@ -2,6 +2,8 @@ package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
@@ -45,6 +47,34 @@ func (r *CommandSide) ChangeHumanEmail(ctx context.Context, email *domain.Email)
return writeModelToEmail(existingEmail), nil
}
func (r *CommandSide) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
if code == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-çm0ds", "Errors.User.Code.Empty")
}
existingCode, err := r.emailWriteModel(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, r.emailVerificationCode)
if err == nil {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingCode, userAgg)
}
userAgg.PushEvents(user.NewHumanEmailVerificationFailedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingCode, userAgg)
logging.LogWithFields("COMMAND-Dg2z5", "userID", userAgg.ID()).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid")
}
func (r *CommandSide) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")

View File

@@ -2,10 +2,11 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
type HumanEmailWriteModel struct {
@@ -14,6 +15,10 @@ type HumanEmailWriteModel struct {
Email string
IsEmailVerified bool
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
UserState domain.UserState
}
@@ -27,18 +32,7 @@ func NewHumanEmailWriteModel(userID, resourceOwner string) *HumanEmailWriteModel
}
func (wm *HumanEmailWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanEmailChangedEvent:
wm.AppendEvents(e)
case *user.HumanEmailVerifiedEvent:
wm.AppendEvents(e)
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanEmailWriteModel) Reduce() error {
@@ -53,8 +47,14 @@ func (wm *HumanEmailWriteModel) Reduce() error {
case *user.HumanEmailChangedEvent:
wm.Email = e.EmailAddress
wm.IsEmailVerified = false
wm.Code = nil
case *user.HumanEmailCodeAddedEvent:
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
case *user.HumanEmailVerifiedEvent:
wm.IsEmailVerified = true
wm.Code = nil
if wm.UserState == domain.UserStateInitial {
wm.UserState = domain.UserStateActive
}

View File

@@ -22,7 +22,7 @@ func (r *CommandSide) removeHumanExternalIDP(ctx context.Context, externalIDP *d
return err
}
if existingExternalIDP.State == domain.ExternalIDPStateUnspecified || existingExternalIDP.State == domain.ExternalIDPStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-5M0ds", "Errors.User.ExternalIDP.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-1M9xR", "Errors.User.ExternalIDP.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingExternalIDP.WriteModel)
if !cascade {

View File

@@ -28,24 +28,7 @@ func NewHumanExternalIDPWriteModel(userID, idpConfigID, externalUserID, resource
}
func (wm *HumanExternalIDPWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanExternalIDPAddedEvent:
if wm.IDPConfigID == e.IDPConfigID && wm.ExternalUserID == e.UserID {
wm.AppendEvents(e)
}
case *user.HumanExternalIDPRemovedEvent:
if wm.IDPConfigID == e.IDPConfigID && wm.ExternalUserID == e.UserID {
wm.AppendEvents(e)
}
case *user.HumanExternalIDPCascadeRemovedEvent:
if wm.IDPConfigID == e.IDPConfigID && wm.ExternalUserID == e.UserID {
wm.AppendEvents(e)
}
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanExternalIDPWriteModel) Reduce() error {

View File

@@ -48,28 +48,10 @@ func NewHumanWriteModel(userID, resourceOwner string) *HumanWriteModel {
}
func (wm *HumanWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanAddedEvent,
*user.HumanRegisteredEvent,
*user.HumanProfileChangedEvent,
*user.HumanEmailChangedEvent,
*user.HumanEmailVerifiedEvent,
*user.HumanPhoneChangedEvent,
*user.HumanPhoneVerifiedEvent,
*user.HumanAddressChangedEvent,
*user.HumanPasswordChangedEvent,
*user.UserDeactivatedEvent,
*user.UserReactivatedEvent,
*user.UserLockedEvent,
*user.UserUnlockedEvent,
*user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
//TODO: Compute State? initial/active
//TODO: Compute OTPState? initial/active
func (wm *HumanWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {

View File

@@ -3,11 +3,87 @@ package command
import (
"context"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
human, err := r.getHuman(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
org, err := r.getOrg(ctx, human.ResourceOwner)
if err != nil {
return nil, err
}
orgPolicy, err := r.getOrgIAMPolicy(ctx, org.AggregateID)
if err != nil {
return nil, err
}
otpWriteModel, err := r.otpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
if otpWriteModel.State == domain.MFAStateReady {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
accountName := domain.GenerateLoginName(human.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain)
if accountName == "" {
accountName = human.EmailAddress
}
key, secret, err := domain.NewOTPKey(r.multifactors.OTP.Issuer, accountName, r.multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
userAgg.PushEvents(
user.NewHumanOTPAddedEvent(ctx, secret),
)
err = r.eventstore.PushAggregate(ctx, otpWriteModel, userAgg)
if err != nil {
return nil, err
}
return &domain.OTP{
ObjectRoot: models.ObjectRoot{
AggregateID: human.AggregateID,
},
SecretString: key.Secret(),
Url: key.URL(),
}, nil
}
func (r *CommandSide) CheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := r.otpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingOTP.State == domain.MFAStateUnspecified || existingOTP.State == domain.MFAStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotExisting")
}
if existingOTP.State == domain.MFAStateReady {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady")
}
if err := domain.VerifyMFAOTP(code, existingOTP.Secret, r.multifactors.OTP.CryptoMFA); err != nil {
return err
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
userAgg.PushEvents(
user.NewHumanOTPVerifiedEvent(ctx, userAgentID),
)
return r.eventstore.PushAggregate(ctx, existingOTP, userAgg)
}
func (r *CommandSide) RemoveHumanOTP(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
@@ -17,8 +93,8 @@ func (r *CommandSide) RemoveHumanOTP(ctx context.Context, userID, resourceOwner
if err != nil {
return err
}
if existingOTP.State == domain.OTPStateUnspecified || existingOTP.State == domain.OTPStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-5M0ds", "Errors.User.OTP.NotFound")
if existingOTP.State == domain.MFAStateUnspecified || existingOTP.State == domain.MFAStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-Hd9sd", "Errors.User.MFA.OTP.NotExisting")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
userAgg.PushEvents(

View File

@@ -10,9 +10,8 @@ import (
type HumanOTPWriteModel struct {
eventstore.WriteModel
State domain.MFAState
Secret *crypto.CryptoValue
State domain.OTPState
}
func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
@@ -25,16 +24,7 @@ func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
}
func (wm *HumanOTPWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanOTPAddedEvent:
wm.AppendEvents(e)
case *user.HumanOTPRemovedEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanOTPWriteModel) Reduce() error {
@@ -42,11 +32,13 @@ func (wm *HumanOTPWriteModel) Reduce() error {
switch e := event.(type) {
case *user.HumanOTPAddedEvent:
wm.Secret = e.Secret
wm.State = domain.OTPStateActive
wm.State = domain.MFAStateNotReady
case *user.HumanOTPVerifiedEvent:
wm.State = domain.MFAStateReady
case *user.HumanOTPRemovedEvent:
wm.State = domain.OTPStateRemoved
wm.State = domain.MFAStateRemoved
case *user.UserRemovedEvent:
wm.State = domain.OTPStateRemoved
wm.State = domain.MFAStateRemoved
}
}
return wm.WriteModel.Reduce()

View File

@@ -24,11 +24,11 @@ func (r *CommandSide) SetOneTimePassword(ctx context.Context, orgID, userID, pas
return r.changePassword(ctx, orgID, userID, "", password, existingPassword)
}
func (r *CommandSide) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID, resourceOwner string) (err error) {
func (r *CommandSide) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
existingPassword, err := r.passwordWriteModel(ctx, userID, resourceOwner)
existingPassword, err := r.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}

View File

@@ -26,18 +26,7 @@ func NewHumanPasswordWriteModel(userID, resourceOwner string) *HumanPasswordWrit
}
func (wm *HumanPasswordWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanPasswordChangedEvent:
wm.AppendEvents(e)
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.HumanEmailVerifiedEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanPasswordWriteModel) Reduce() error {

View File

@@ -2,7 +2,8 @@ package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
@@ -14,12 +15,12 @@ func (r *CommandSide) ChangeHumanPhone(ctx context.Context, phone *domain.Phone)
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid")
}
existingPhone, err := r.phoneWriteModel(ctx, phone.AggregateID, phone.ResourceOwner)
existingPhone, err := r.phoneWriteModelByID(ctx, phone.AggregateID, phone.ResourceOwner)
if err != nil {
return nil, err
}
if existingPhone.State == domain.PhoneStateUnspecified || existingPhone.State == domain.PhoneStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-5M0ds", "Errors.User.Phone.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-aM9cs", "Errors.User.Phone.NotFound")
}
changedEvent, hasChanged := existingPhone.NewChangedEvent(ctx, phone.PhoneNumber)
if !hasChanged {
@@ -46,12 +47,48 @@ func (r *CommandSide) ChangeHumanPhone(ctx context.Context, phone *domain.Phone)
return writeModelToPhone(existingPhone), nil
}
func (r *CommandSide) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceOwner string) error {
func (r *CommandSide) VerifyHumanPhone(ctx context.Context, userID, code, resourceowner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Km9ds", "Errors.User.UserIDMissing")
}
if code == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-wMe9f", "Errors.User.Code.Empty")
}
existingCode, err := r.phoneWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingCode.Code == nil || existingCode.State == domain.PhoneStateUnspecified || existingCode.State == domain.PhoneStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-Rsj8c", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, r.emailVerificationCode)
if err == nil {
userAgg.PushEvents(user.NewHumanPhoneVerifiedEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingCode, userAgg)
}
userAgg.PushEvents(user.NewHumanPhoneVerificationFailedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingCode, userAgg)
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, r.emailVerificationCode)
if err == nil {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingCode, userAgg)
}
userAgg.PushEvents(user.NewHumanEmailVerificationFailedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingCode, userAgg)
logging.LogWithFields("COMMAND-5M9ds", "userID", userAgg.ID()).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-sM0cs", "Errors.User.Code.Invalid")
}
func (r *CommandSide) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
existingPhone, err := r.phoneWriteModel(ctx, userID, resourceOwner)
existingPhone, err := r.phoneWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
@@ -75,12 +112,12 @@ func (r *CommandSide) RemoveHumanPhone(ctx context.Context, userID, resourceOwne
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.User.UserIDMissing")
}
existingPhone, err := r.phoneWriteModel(ctx, userID, resourceOwner)
existingPhone, err := r.phoneWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if existingPhone.State == domain.PhoneStateUnspecified || existingPhone.State == domain.PhoneStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-5M0ds", "Errors.User.Phone.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-p6rsc", "Errors.User.Phone.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel)
userAgg.PushEvents(
@@ -89,7 +126,7 @@ func (r *CommandSide) RemoveHumanPhone(ctx context.Context, userID, resourceOwne
return r.eventstore.PushAggregate(ctx, existingPhone, userAgg)
}
func (r *CommandSide) phoneWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPhoneWriteModel, err error) {
func (r *CommandSide) phoneWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPhoneWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@@ -2,10 +2,11 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
type HumanPhoneWriteModel struct {
@@ -14,6 +15,10 @@ type HumanPhoneWriteModel struct {
Phone string
IsPhoneVerified bool
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
State domain.PhoneState
}
@@ -27,20 +32,7 @@ func NewHumanPhoneWriteModel(userID, resourceOwner string) *HumanPhoneWriteModel
}
func (wm *HumanPhoneWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.HumanPhoneChangedEvent:
wm.AppendEvents(e)
case *user.HumanPhoneVerifiedEvent:
wm.AppendEvents(e)
case *user.HumanPhoneRemovedEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanPhoneWriteModel) Reduce() error {
@@ -49,8 +41,8 @@ func (wm *HumanPhoneWriteModel) Reduce() error {
case *user.HumanAddedEvent:
if e.PhoneNumber != "" {
wm.Phone = e.PhoneNumber
wm.State = domain.PhoneStateActive
}
wm.State = domain.PhoneStateActive
case *user.HumanRegisteredEvent:
if e.PhoneNumber != "" {
wm.Phone = e.PhoneNumber
@@ -60,8 +52,14 @@ func (wm *HumanPhoneWriteModel) Reduce() error {
wm.Phone = e.PhoneNumber
wm.IsPhoneVerified = false
wm.State = domain.PhoneStateActive
wm.Code = nil
case *user.HumanPhoneVerifiedEvent:
wm.IsPhoneVerified = true
wm.Code = nil
case *user.HumanPhoneCodeAddedEvent:
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
case *user.HumanPhoneRemovedEvent:
wm.State = domain.PhoneStateRemoved
case *user.UserRemovedEvent:

View File

@@ -19,7 +19,7 @@ func (r *CommandSide) ChangeHumanProfile(ctx context.Context, profile *domain.Pr
if existingProfile.UserState == domain.UserStateUnspecified || existingProfile.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.User.Profile.NotFound")
}
changedEvent, hasChanged := existingProfile.NewChangedEvent(ctx, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, domain.Gender(profile.Gender))
changedEvent, hasChanged := existingProfile.NewChangedEvent(ctx, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, profile.Gender)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.User.Profile.NotChanged")
}

View File

@@ -33,16 +33,7 @@ func NewHumanProfileWriteModel(userID, resourceOwner string) *HumanProfileWriteM
}
func (wm *HumanProfileWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanProfileChangedEvent:
wm.AppendEvents(e)
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanProfileWriteModel) Reduce() error {

View File

@@ -6,16 +6,193 @@ import (
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
usr_repo "github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) getHumanU2FTokens(ctx context.Context, userID, resourceowner string) ([]*domain.WebAuthNToken, error) {
tokenReadModel := NewHumanU2FTokensReadModel(userID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
if err != nil {
return nil, err
}
if tokenReadModel.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-4M0ds", "Errors.User.NotFound")
}
return readModelToU2FTokens(tokenReadModel), nil
}
func (r *CommandSide) getHumanPasswordlessTokens(ctx context.Context, userID, resourceowner string) ([]*domain.WebAuthNToken, error) {
tokenReadModel := NewHumanPasswordlessTokensReadModel(userID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
if err != nil {
return nil, err
}
if tokenReadModel.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Mv9sd", "Errors.User.NotFound")
}
return readModelToPasswordlessTokens(tokenReadModel), nil
}
func (r *CommandSide) AddHumanU2F(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
addWebAuthN, userAgg, webAuthN, err := r.addHumanWebAuthN(ctx, userID, resourceowner, isLoginUI, u2fTokens)
if err != nil {
return nil, err
}
userAgg.PushEvents(usr_repo.NewHumanU2FAddedEvent(ctx, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
err = r.eventstore.PushAggregate(ctx, addWebAuthN, userAgg)
if err != nil {
return nil, err
}
createdWebAuthN := writeModelToWebAuthN(addWebAuthN)
createdWebAuthN.CredentialCreationData = webAuthN.CredentialCreationData
createdWebAuthN.AllowedCredentialIDs = webAuthN.AllowedCredentialIDs
createdWebAuthN.UserVerification = webAuthN.UserVerification
return createdWebAuthN, nil
}
func (r *CommandSide) AddHumanPasswordless(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
passwordlessTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
addWebAuthN, userAgg, webAuthN, err := r.addHumanWebAuthN(ctx, userID, resourceowner, isLoginUI, passwordlessTokens)
if err != nil {
return nil, err
}
userAgg.PushEvents(usr_repo.NewHumanU2FAddedEvent(ctx, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
err = r.eventstore.PushAggregate(ctx, addWebAuthN, userAgg)
if err != nil {
return nil, err
}
createdWebAuthN := writeModelToWebAuthN(addWebAuthN)
createdWebAuthN.CredentialCreationData = webAuthN.CredentialCreationData
createdWebAuthN.AllowedCredentialIDs = webAuthN.AllowedCredentialIDs
createdWebAuthN.UserVerification = webAuthN.UserVerification
return createdWebAuthN, nil
}
func (r *CommandSide) addHumanWebAuthN(ctx context.Context, userID, resourceowner string, isLoginUI bool, tokens []*domain.WebAuthNToken) (*HumanWebAuthNWriteModel, *usr_repo.Aggregate, *domain.WebAuthNToken, error) {
if userID == "" || resourceowner == "" {
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
}
user, err := r.getHuman(ctx, userID, resourceowner)
if err != nil {
return nil, nil, nil, err
}
org, err := r.getOrg(ctx, user.ResourceOwner)
if err != nil {
return nil, nil, nil, err
}
orgPolicy, err := r.getOrgIAMPolicy(ctx, org.AggregateID)
if err != nil {
return nil, nil, nil, err
}
accountName := domain.GenerateLoginName(user.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain)
if accountName == "" {
accountName = user.EmailAddress
}
webAuthN, err := r.webauthn.BeginRegistration(user, accountName, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...)
if err != nil {
return nil, nil, nil, err
}
tokenID, err := r.idGenerator.Next()
if err != nil {
return nil, nil, nil, err
}
addWebAuthN, err := r.webauthNWriteModelByID(ctx, userID, tokenID, resourceowner)
if err != nil {
return nil, nil, nil, err
}
userAgg := UserAggregateFromWriteModel(&addWebAuthN.WriteModel)
return addWebAuthN, userAgg, webAuthN, nil
}
func (r *CommandSide) VerifyHumanU2F(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceowner)
if err != nil {
return err
}
verifyWebAuthN, userAgg, webAuthN, err := r.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, u2fTokens)
if err != nil {
return err
}
userAgg.PushEvents(
usr_repo.NewHumanU2FVerifiedEvent(
ctx,
verifyWebAuthN.WebauthNTokenID,
webAuthN.WebAuthNTokenName,
webAuthN.AttestationType,
webAuthN.KeyID,
webAuthN.PublicKey,
webAuthN.AAGUID,
webAuthN.SignCount,
),
)
return r.eventstore.PushAggregate(ctx, verifyWebAuthN, userAgg)
}
func (r *CommandSide) VerifyHumanPasswordless(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
u2fTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceowner)
if err != nil {
return err
}
verifyWebAuthN, userAgg, webAuthN, err := r.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, u2fTokens)
if err != nil {
return err
}
userAgg.PushEvents(
usr_repo.NewHumanU2FVerifiedEvent(
ctx,
verifyWebAuthN.WebauthNTokenID,
webAuthN.WebAuthNTokenName,
webAuthN.AttestationType,
webAuthN.KeyID,
webAuthN.PublicKey,
webAuthN.AAGUID,
webAuthN.SignCount,
),
)
return r.eventstore.PushAggregate(ctx, verifyWebAuthN, userAgg)
}
func (r *CommandSide) verifyHumanWebAuthN(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte, tokens []*domain.WebAuthNToken) (*HumanWebAuthNWriteModel, *usr_repo.Aggregate, *domain.WebAuthNToken, error) {
if userID == "" || resourceowner == "" {
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
}
user, err := r.getHuman(ctx, userID, resourceowner)
if err != nil {
return nil, nil, nil, err
}
_, token := domain.GetTokenToVerify(tokens)
webAuthN, err := r.webauthn.FinishRegistration(user, token, tokenName, credentialData, userAgentID != "")
if err != nil {
return nil, nil, nil, err
}
verifyWebAuthN, err := r.webauthNWriteModelByID(ctx, userID, token.WebAuthNTokenID, resourceowner)
if err != nil {
return nil, nil, nil, err
}
userAgg := UserAggregateFromWriteModel(&verifyWebAuthN.WriteModel)
return verifyWebAuthN, userAgg, webAuthN, nil
}
func (r *CommandSide) RemoveHumanU2F(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
event := user.NewHumanU2FRemovedEvent(ctx, webAuthNID)
event := usr_repo.NewHumanU2FRemovedEvent(ctx, webAuthNID)
return r.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
}
func (r *CommandSide) RemoveHumanPasswordless(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
event := user.NewHumanPasswordlessRemovedEvent(ctx, webAuthNID)
event := usr_repo.NewHumanPasswordlessRemovedEvent(ctx, webAuthNID)
return r.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
}
@@ -28,8 +205,8 @@ func (r *CommandSide) removeHumanWebAuthN(ctx context.Context, userID, webAuthNI
if err != nil {
return err
}
if existingWebAuthN.State == domain.WebAuthNStateUnspecified || existingWebAuthN.State == domain.WebAuthNStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-5M0ds", "Errors.User.ExternalIDP.NotFound")
if existingWebAuthN.State == domain.MFAStateUnspecified || existingWebAuthN.State == domain.MFAStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9ds", "Errors.User.ExternalIDP.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingWebAuthN.WriteModel)
userAgg.PushEvents(event)

View File

@@ -10,8 +10,16 @@ type HumanWebAuthNWriteModel struct {
eventstore.WriteModel
WebauthNTokenID string
Challenge string
State domain.WebAuthNState
KeyID []byte
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32
WebAuthNTokenName string
State domain.MFAState
}
func NewHumanWebAuthNWriteModel(userID, wbAuthNTokenID, resourceOwner string) *HumanWebAuthNWriteModel {
@@ -29,14 +37,14 @@ func (wm *HumanWebAuthNWriteModel) AppendEvents(events ...eventstore.EventReader
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.AppendEvents(e)
wm.WriteModel.AppendEvents(e)
}
case *user.HumanWebAuthNRemovedEvent:
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.AppendEvents(e)
wm.WriteModel.AppendEvents(e)
}
case *user.UserRemovedEvent:
wm.AppendEvents(e)
wm.WriteModel.AppendEvents(e)
}
}
}
@@ -45,19 +53,185 @@ func (wm *HumanWebAuthNWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
wm.WebauthNTokenID = e.WebAuthNTokenID
wm.State = domain.WebAuthNStateActive
wm.appendAddedEvent(e)
case *user.HumanWebAuthNVerifiedEvent:
wm.appendVerifiedEvent(e)
case *user.HumanWebAuthNRemovedEvent:
wm.State = domain.WebAuthNStateRemoved
wm.State = domain.MFAStateRemoved
case *user.UserRemovedEvent:
wm.State = domain.WebAuthNStateRemoved
wm.State = domain.MFAStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *HumanWebAuthNWriteModel) appendAddedEvent(e *user.HumanWebAuthNAddedEvent) {
wm.WebauthNTokenID = e.WebAuthNTokenID
wm.Challenge = e.Challenge
wm.State = domain.MFAStateNotReady
}
func (wm *HumanWebAuthNWriteModel) appendVerifiedEvent(e *user.HumanWebAuthNVerifiedEvent) {
wm.KeyID = e.KeyID
wm.PublicKey = e.PublicKey
wm.AttestationType = e.AttestationType
wm.AAGUID = e.AAGUID
wm.SignCount = e.SignCount
wm.WebAuthNTokenName = e.WebAuthNTokenName
wm.State = domain.MFAStateReady
}
func (wm *HumanWebAuthNWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner)
}
type HumanU2FTokensReadModel struct {
eventstore.WriteModel
WebAuthNTokens []*HumanWebAuthNWriteModel
UserState domain.UserState
}
func NewHumanU2FTokensReadModel(userID, resourceOwner string) *HumanU2FTokensReadModel {
return &HumanU2FTokensReadModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *HumanU2FTokensReadModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
wm.WriteModel.AppendEvents(e)
case *user.HumanWebAuthNVerifiedEvent:
wm.WriteModel.AppendEvents(e)
case *user.HumanWebAuthNRemovedEvent:
wm.WriteModel.AppendEvents(e)
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *HumanU2FTokensReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
token := &HumanWebAuthNWriteModel{}
token.appendAddedEvent(e)
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
case *user.HumanWebAuthNVerifiedEvent:
idx, token := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
token.appendVerifiedEvent(e)
case *user.HumanWebAuthNRemovedEvent:
idx, _ := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
copy(wm.WebAuthNTokens[idx:], wm.WebAuthNTokens[idx+1:])
wm.WebAuthNTokens[len(wm.WebAuthNTokens)-1] = nil
wm.WebAuthNTokens = wm.WebAuthNTokens[:len(wm.WebAuthNTokens)-1]
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (rm *HumanU2FTokensReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(rm.AggregateID).
ResourceOwner(rm.ResourceOwner).
EventTypes(
user.HumanU2FTokenAddedType,
user.HumanU2FTokenVerifiedType,
user.HumanU2FTokenRemovedType,
user.UserV1MFAOTPRemovedType)
}
func (wm *HumanU2FTokensReadModel) WebAuthNTokenByID(id string) (idx int, token *HumanWebAuthNWriteModel) {
for idx, token = range wm.WebAuthNTokens {
if token.WebauthNTokenID == id {
return idx, token
}
}
return -1, nil
}
type HumanPasswordlessTokensReadModel struct {
eventstore.WriteModel
WebAuthNTokens []*HumanWebAuthNWriteModel
UserState domain.UserState
}
func NewHumanPasswordlessTokensReadModel(userID, resourceOwner string) *HumanPasswordlessTokensReadModel {
return &HumanPasswordlessTokensReadModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *HumanPasswordlessTokensReadModel) AppendEvents(events ...eventstore.EventReader) {
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanPasswordlessTokensReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
token := &HumanWebAuthNWriteModel{}
token.appendAddedEvent(e)
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
case *user.HumanWebAuthNVerifiedEvent:
idx, token := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
token.appendVerifiedEvent(e)
case *user.HumanWebAuthNRemovedEvent:
idx, _ := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
copy(wm.WebAuthNTokens[idx:], wm.WebAuthNTokens[idx+1:])
wm.WebAuthNTokens[len(wm.WebAuthNTokens)-1] = nil
wm.WebAuthNTokens = wm.WebAuthNTokens[:len(wm.WebAuthNTokens)-1]
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (rm *HumanPasswordlessTokensReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(rm.AggregateID).
ResourceOwner(rm.ResourceOwner).
EventTypes(
user.HumanPasswordlessTokenAddedType,
user.HumanPasswordlessTokenVerifiedType,
user.HumanPasswordlessTokenRemovedType,
user.UserV1MFAOTPRemovedType)
}
func (wm *HumanPasswordlessTokensReadModel) WebAuthNTokenByID(id string) (idx int, token *HumanWebAuthNWriteModel) {
for idx, token = range wm.WebAuthNTokens {
if token.WebauthNTokenID == id {
return idx, token
}
}
return -1, nil
}

View File

@@ -8,9 +8,9 @@ import (
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) AddMachine(ctx context.Context, orgID, username string, machine *domain.Machine) (*domain.Machine, error) {
func (r *CommandSide) AddMachine(ctx context.Context, orgID string, machine *domain.Machine) (*domain.Machine, error) {
if !machine.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0ds", "Errors.User.Invalid")
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bm9Ds", "Errors.User.Invalid")
}
userID, err := r.idGenerator.Next()
if err != nil {
@@ -31,11 +31,12 @@ func (r *CommandSide) AddMachine(ctx context.Context, orgID, username string, ma
userAgg.PushEvents(
user.NewMachineAddedEvent(
ctx,
username,
machine.Username,
machine.Name,
machine.Description,
),
)
err = r.eventstore.PushAggregate(ctx, addedMachine, userAgg)
return writeModelToMachine(addedMachine), nil
}
@@ -64,7 +65,7 @@ func (r *CommandSide) ChangeMachine(ctx context.Context, machine *domain.Machine
func (r *CommandSide) machineWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *MachineWriteModel, err error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0ds", "Errors.User.UserIDMissing")
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Plof", "Errors.User.UserIDMissing")
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@@ -28,29 +28,10 @@ func NewMachineWriteModel(userID, resourceOwner string) *MachineWriteModel {
}
func (wm *MachineWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.MachineAddedEvent:
wm.AppendEvents(e)
case *user.UsernameChangedEvent:
wm.AppendEvents(e)
case *user.MachineChangedEvent:
wm.AppendEvents(e)
case *user.UserDeactivatedEvent:
wm.AppendEvents(e)
case *user.UserReactivatedEvent:
wm.AppendEvents(e)
case *user.UserLockedEvent:
wm.AppendEvents(e)
case *user.UserUnlockedEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
//TODO: Compute State? initial/active
//TODO: Compute OTPState? initial/active
func (wm *MachineWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {

View File

@@ -26,29 +26,10 @@ func NewUserWriteModel(userID, resourceOwner string) *UserWriteModel {
}
func (wm *UserWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanAddedEvent, *user.HumanRegisteredEvent:
wm.AppendEvents(e)
case *user.MachineAddedEvent:
wm.AppendEvents(e)
case *user.UsernameChangedEvent:
wm.AppendEvents(e)
case *user.UserDeactivatedEvent:
wm.AppendEvents(e)
case *user.UserReactivatedEvent:
wm.AppendEvents(e)
case *user.UserLockedEvent:
wm.AppendEvents(e)
case *user.UserUnlockedEvent:
wm.AppendEvents(e)
case *user.UserRemovedEvent:
wm.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
//TODO: Compute State? initial/active
//TODO: Compute OTPState? initial/active
func (wm *UserWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {

View File

@@ -11,6 +11,8 @@ import (
type Human struct {
es_models.ObjectRoot
Username string
State UserState
*Password
*Profile
*Email
@@ -24,6 +26,14 @@ type Human struct {
PasswordlessLogins []*WebAuthNLogin
}
func (h Human) GetUsername() string {
return h.Username
}
func (h Human) GetState() UserState {
return h.State
}
type InitUserCode struct {
es_models.ObjectRoot
@@ -91,3 +101,10 @@ func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {
Expiry: generator.Expiry(),
}, nil
}
func GenerateLoginName(username, domain string, appendDomain bool) string {
if !appendDomain {
return username
}
return username + "@" + domain
}

View File

@@ -2,7 +2,10 @@ package domain
import (
"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/pquerna/otp"
"github.com/pquerna/otp/totp"
)
type OTP struct {
@@ -14,16 +17,27 @@ type OTP struct {
State MFAState
}
type OTPState int32
const (
OTPStateUnspecified OTPState = iota
OTPStateActive
OTPStateRemoved
otpStateCount
)
func (s OTPState) Valid() bool {
return s >= 0 && s < otpStateCount
func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
if err != nil {
return nil, nil, err
}
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
if err != nil {
return nil, nil, err
}
return key, encryptedSecret, nil
}
func VerifyMFAOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
decrypt, err := crypto.DecryptString(secret, cryptoAlg)
if err != nil {
return err
}
valid := totp.Validate(code, decrypt)
if !valid {
return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode")
}
return nil
}

View File

@@ -39,16 +39,19 @@ const (
UserVerificationRequirementDiscouraged
)
type WebAuthNState int32
type AuthenticatorAttachment int32
const (
WebAuthNStateUnspecified WebAuthNState = iota
WebAuthNStateActive
WebAuthNStateRemoved
webAuthNStateCount
AuthenticatorAttachmentUnspecified AuthenticatorAttachment = iota
AuthenticatorAttachmentPlattform
AuthenticatorAttachmentCrossPlattform
)
func (s WebAuthNState) Valid() bool {
return s >= 0 && s < webAuthNStateCount
func GetTokenToVerify(tokens []*WebAuthNToken) (int, *WebAuthNToken) {
for i, u2f := range tokens {
if u2f.State == MFAStateNotReady {
return i, u2f
}
}
return -1, nil
}

View File

@@ -5,10 +5,20 @@ import "github.com/caos/zitadel/internal/eventstore/models"
type Machine struct {
models.ObjectRoot
Username string
State UserState
Name string
Description string
}
func (m Machine) GetUsername() string {
return m.Username
}
func (m Machine) GetState() UserState {
return m.State
}
func (sa *Machine) IsValid() bool {
return sa.Name != ""
}

View File

@@ -6,6 +6,7 @@ const (
MFAStateUnspecified MFAState = iota
MFAStateNotReady
MFAStateReady
MFAStateRemoved
stateCount
)

View File

@@ -12,6 +12,7 @@ type Org struct {
State OrgState
Name string
PrimaryDomain string
Domains []*OrgDomain
Members []*Member
OrgIamPolicy *OrgIAMPolicy
@@ -38,6 +39,7 @@ func (o *Org) nameForDomain(iamDomain string) string {
type OrgState int32
const (
OrgStateActive OrgState = iota
OrgStateUnspecified OrgState = iota
OrgStateActive
OrgStateInactive
)

View File

@@ -1,14 +1,8 @@
package domain
import es_models "github.com/caos/zitadel/internal/eventstore/models"
type User struct {
es_models.ObjectRoot
State UserState
UserName string
*Human
*Machine
type User interface {
GetUsername() string
GetState() UserState
}
type UserState int32
@@ -28,13 +22,3 @@ const (
func (f UserState) Valid() bool {
return f >= 0 && f < userStateCount
}
func (u *User) IsValid() bool {
if u.Human == nil && u.Machine == nil || u.UserName == "" {
return false
}
if u.Human != nil {
return u.Human.IsValid()
}
return u.Machine.IsValid()
}

View File

@@ -77,7 +77,7 @@ func readModelToLabelPolicy(readModel *IAMLabelPolicyReadModel) *model.LabelPoli
PrimaryColor: readModel.PrimaryColor,
SecondaryColor: readModel.SecondaryColor,
Default: true,
//TODO: State: int32,
//TODO: OTPState: int32,
}
}
@@ -89,7 +89,7 @@ func readModelToLoginPolicy(readModel *IAMLoginPolicyReadModel) *model.LoginPoli
AllowUsernamePassword: readModel.AllowUserNamePassword,
Default: true,
//TODO: IDPProviders: []*model.IDPProvider,
//TODO: State: int32,
//TODO: OTPState: int32,
}
}
func readModelToOrgIAMPolicy(readModel *IAMOrgIAMPolicyReadModel) *model.OrgIAMPolicy {
@@ -97,7 +97,7 @@ func readModelToOrgIAMPolicy(readModel *IAMOrgIAMPolicyReadModel) *model.OrgIAMP
ObjectRoot: readModelToObjectRoot(readModel.OrgIAMPolicyReadModel.ReadModel),
UserLoginMustBeDomain: readModel.UserLoginMustBeDomain,
Default: true,
//TODO: State: int32,
//TODO: OTPState: int32,
}
}
func readModelToPasswordAgePolicy(readModel *IAMPasswordAgePolicyReadModel) *model.PasswordAgePolicy {
@@ -105,7 +105,7 @@ func readModelToPasswordAgePolicy(readModel *IAMPasswordAgePolicyReadModel) *mod
ObjectRoot: readModelToObjectRoot(readModel.PasswordAgePolicyReadModel.ReadModel),
ExpireWarnDays: uint64(readModel.ExpireWarnDays),
MaxAgeDays: uint64(readModel.MaxAgeDays),
//TODO: State: int32,
//TODO: OTPState: int32,
}
}
func readModelToPasswordComplexityPolicy(readModel *IAMPasswordComplexityPolicyReadModel) *model.PasswordComplexityPolicy {
@@ -116,7 +116,7 @@ func readModelToPasswordComplexityPolicy(readModel *IAMPasswordComplexityPolicyR
HasSymbol: readModel.HasSymbol,
HasUppercase: readModel.HasUpperCase,
MinLength: uint64(readModel.MinLength),
//TODO: State: int32,
//TODO: OTPState: int32,
}
}
func readModelToPasswordLockoutPolicy(readModel *IAMPasswordLockoutPolicyReadModel) *model.PasswordLockoutPolicy {
@@ -124,7 +124,7 @@ func readModelToPasswordLockoutPolicy(readModel *IAMPasswordLockoutPolicyReadMod
ObjectRoot: readModelToObjectRoot(readModel.PasswordLockoutPolicyReadModel.ReadModel),
MaxAttempts: uint64(readModel.MaxAttempts),
ShowLockOutFailures: readModel.ShowLockOutFailures,
//TODO: State: int32,
//TODO: OTPState: int32,
}
}

View File

@@ -138,7 +138,6 @@ func IAMAggregateFromReadModel(rm *ReadModel) *iam.Aggregate {
iam.AggregateType,
rm.ResourceOwner,
iam.AggregateVersion,
rm.ProcessedSequence,
),
}
}

View File

@@ -59,7 +59,6 @@ func UserAggregateFromReadModel(rm *UserReadModel) *user.Aggregate {
user.AggregateType,
rm.ResourceOwner,
user.AggregateVersion,
rm.ProcessedSequence,
),
}
}

View File

@@ -19,23 +19,6 @@ type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(
id,
resourceOwner string,
previousSequence uint64,
) *Aggregate {
return &Aggregate{
Aggregate: *eventstore.NewAggregate(
id,
AggregateType,
resourceOwner,
AggregateVersion,
previousSequence,
),
}
}
func (a *Aggregate) PushStepStarted(ctx context.Context, step domain.Step) *Aggregate {
a.Aggregate = *a.PushEvents(NewSetupStepStartedEvent(ctx, step))
return a

View File

@@ -27,7 +27,7 @@ func NewIAMProjectSetEvent(ctx context.Context, projectID string) *ProjectSetEve
return &ProjectSetEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
SetupDoneEventType,
ProjectSetEventType,
),
ProjectID: projectID,
}

View File

@@ -16,20 +16,3 @@ const (
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(
id,
resourceOwner string,
previousSequence uint64,
) *Aggregate {
return &Aggregate{
Aggregate: *eventstore.NewAggregate(
id,
AggregateType,
resourceOwner,
AggregateVersion,
previousSequence,
),
}
}

View File

@@ -12,20 +12,3 @@ const (
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(
id,
resourceOwner string,
previousSequence uint64,
) *Aggregate {
return &Aggregate{
Aggregate: *eventstore.NewAggregate(
id,
AggregateType,
resourceOwner,
AggregateVersion,
previousSequence,
),
}
}

View File

@@ -12,20 +12,3 @@ const (
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(
id,
resourceOwner string,
previousSequence uint64,
) *Aggregate {
return &Aggregate{
Aggregate: *eventstore.NewAggregate(
id,
AggregateType,
resourceOwner,
AggregateVersion,
previousSequence,
),
}
}

View File

@@ -37,7 +37,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(UserV1MFAOTPCheckSucceededType, HumanOTPCheckSucceededEventMapper).
RegisterFilterEventMapper(UserV1MFAOTPCheckFailedType, HumanOTPCheckFailedEventMapper).
RegisterFilterEventMapper(UserLockedType, UserLockedEventMapper).
RegisterFilterEventMapper(UserUnlockedType, UserLockedEventMapper).
RegisterFilterEventMapper(UserUnlockedType, UserUnlockedEventMapper).
RegisterFilterEventMapper(UserDeactivatedType, UserDeactivatedEventMapper).
RegisterFilterEventMapper(UserReactivatedType, UserReactivatedEventMapper).
RegisterFilterEventMapper(UserRemovedType, UserRemovedEventMapper).

View File

@@ -52,18 +52,20 @@ func HumanOTPAddedEventMapper(event *repository.Event) (eventstore.EventReader,
type HumanOTPVerifiedEvent struct {
eventstore.BaseEvent `json:"-"`
UserAgentID string `json:"userAgentID,omitempty"`
}
func (e *HumanOTPVerifiedEvent) Data() interface{} {
return nil
}
func NewHumanOTPVerifiedEvent(ctx context.Context) *HumanOTPVerifiedEvent {
func NewHumanOTPVerifiedEvent(ctx context.Context, userAgentID string) *HumanOTPVerifiedEvent {
return &HumanOTPVerifiedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
HumanMFAOTPVerifiedType,
),
UserAgentID: userAgentID,
}
}

View File

@@ -32,9 +32,8 @@ const (
type HumanWebAuthNAddedEvent struct {
eventstore.BaseEvent `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
Challenge string `json:"challenge"`
State domain.MFAState `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
Challenge string `json:"challenge"`
}
func (e *HumanWebAuthNAddedEvent) Data() interface{} {
@@ -74,7 +73,6 @@ func NewHumanPasswordlessAddedEvent(
func WebAuthNAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanWebAuthNAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
State: domain.MFAStateNotReady,
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
@@ -86,14 +84,13 @@ func WebAuthNAddedEventMapper(event *repository.Event) (eventstore.EventReader,
type HumanWebAuthNVerifiedEvent struct {
eventstore.BaseEvent `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
KeyID []byte `json:"keyId"`
PublicKey []byte `json:"publicKey"`
AttestationType string `json:"attestationType"`
AAGUID []byte `json:"aaguid"`
SignCount uint32 `json:"signCount"`
WebAuthNTokenName string `json:"webAuthNTokenName"`
State domain.MFAState `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
KeyID []byte `json:"keyId"`
PublicKey []byte `json:"publicKey"`
AttestationType string `json:"attestationType"`
AAGUID []byte `json:"aaguid"`
SignCount uint32 `json:"signCount"`
WebAuthNTokenName string `json:"webAuthNTokenName"`
}
func (e *HumanWebAuthNVerifiedEvent) Data() interface{} {
@@ -153,7 +150,6 @@ func NewHumanPasswordlessVerifiedEvent(
func HumanWebAuthNVerifiedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webauthNVerified := &HumanWebAuthNVerifiedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
State: domain.MFAStateReady,
}
err := json.Unmarshal(event.Data, webauthNVerified)
if err != nil {
@@ -165,9 +161,8 @@ func HumanWebAuthNVerifiedEventMapper(event *repository.Event) (eventstore.Event
type HumanWebAuthNSignCountChangedEvent struct {
eventstore.BaseEvent `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
SignCount uint32 `json:"signCount"`
State domain.MFAState `json:"-"`
WebAuthNTokenID string `json:"webAuthNTokenId"`
SignCount uint32 `json:"signCount"`
}
func (e *HumanWebAuthNSignCountChangedEvent) Data() interface{} {