mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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(
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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) }()
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) }()
|
||||
|
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 != ""
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ const (
|
||||
MFAStateUnspecified MFAState = iota
|
||||
MFAStateNotReady
|
||||
MFAStateReady
|
||||
MFAStateRemoved
|
||||
|
||||
stateCount
|
||||
)
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -138,7 +138,6 @@ func IAMAggregateFromReadModel(rm *ReadModel) *iam.Aggregate {
|
||||
iam.AggregateType,
|
||||
rm.ResourceOwner,
|
||||
iam.AggregateVersion,
|
||||
rm.ProcessedSequence,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@@ -59,7 +59,6 @@ func UserAggregateFromReadModel(rm *UserReadModel) *user.Aggregate {
|
||||
user.AggregateType,
|
||||
rm.ResourceOwner,
|
||||
user.AggregateVersion,
|
||||
rm.ProcessedSequence,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -27,7 +27,7 @@ func NewIAMProjectSetEvent(ctx context.Context, projectID string) *ProjectSetEve
|
||||
return &ProjectSetEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
SetupDoneEventType,
|
||||
ProjectSetEventType,
|
||||
),
|
||||
ProjectID: projectID,
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@@ -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).
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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{} {
|
||||
|
Reference in New Issue
Block a user