package command import ( "context" "time" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) type AddLoginPolicy struct { AllowUsernamePassword bool AllowRegister bool AllowExternalIDP bool IDPProviders []*AddLoginPolicyIDP ForceMFA bool ForceMFALocalOnly bool SecondFactors []domain.SecondFactorType MultiFactors []domain.MultiFactorType PasswordlessType domain.PasswordlessType HidePasswordReset bool IgnoreUnknownUsernames bool AllowDomainDiscovery bool DefaultRedirectURI string PasswordCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration MFAInitSkipLifetime time.Duration SecondFactorCheckLifetime time.Duration MultiFactorCheckLifetime time.Duration DisableLoginWithEmail bool DisableLoginWithPhone bool } type AddLoginPolicyIDP struct { ConfigID string Type domain.IdentityProviderType } type ChangeLoginPolicy struct { AllowUsernamePassword bool AllowRegister bool AllowExternalIDP bool ForceMFA bool ForceMFALocalOnly bool PasswordlessType domain.PasswordlessType HidePasswordReset bool IgnoreUnknownUsernames bool AllowDomainDiscovery bool DefaultRedirectURI string PasswordCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration MFAInitSkipLifetime time.Duration SecondFactorCheckLifetime time.Duration MultiFactorCheckLifetime time.Duration DisableLoginWithEmail bool DisableLoginWithPhone bool } func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, policy *AddLoginPolicy) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() orgAgg := org.NewAggregate(resourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddLoginPolicy(orgAgg, policy)) if err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } return pushedEventsToObjectDetails(pushedEvents), nil } func (c *Commands) orgLoginPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLoginPolicyWriteModel, error) { policyWriteModel := NewOrgLoginPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, policyWriteModel) if err != nil { return nil, err } return policyWriteModel, nil } func (c *Commands) getOrgLoginPolicy(ctx context.Context, orgID string) (*domain.LoginPolicy, error) { policy, err := c.orgLoginPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err } if policy.State == domain.PolicyStateActive { return writeModelToLoginPolicy(&policy.LoginPolicyWriteModel), nil } return c.getDefaultLoginPolicy(ctx) } func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *ChangeLoginPolicy) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeLoginPolicy(orgAgg, policy)) if err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } return pushedEventsToObjectDetails(pushedEvents), nil } func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { if orgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing") } existingPolicy := NewOrgLoginPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-GHB37", "Errors.Org.LoginPolicy.NotFound") } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewLoginPolicyRemovedEvent(ctx, orgAgg)) if err != nil { return nil, err } err = AppendAndReduce(existingPolicy, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&existingPolicy.WriteModel), nil } func (c *Commands) AddIDPToLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider) (*domain.IDPProvider, error) { if resourceOwner == "" { return nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") } if !idpProvider.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-9nf88", "Errors.Org.LoginPolicy.IDP.") } existingPolicy, err := c.orgLoginPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-Ffgw2", "Errors.Org.LoginPolicy.NotFound") } var exists bool if idpProvider.Type == domain.IdentityProviderTypeOrg { exists, err = ExistsOrgIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID, resourceOwner) } else { exists, err = ExistsInstanceIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID) } if !exists || err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "Org-3N9fs", "Errors.IDPConfig.NotExisting") } idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID) err = c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { return nil, err } if idpModel.State == domain.IdentityProviderStateActive { return nil, zerrors.ThrowAlreadyExists(nil, "Org-2B0ps", "Errors.Org.LoginPolicy.IDP.AlreadyExists") } orgAgg := OrgAggregateFromWriteModel(&idpModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewIdentityProviderAddedEvent(ctx, orgAgg, idpProvider.IDPConfigID, idpProvider.Type)) if err != nil { return nil, err } err = AppendAndReduce(idpModel, pushedEvents...) if err != nil { return nil, err } return writeModelToIDPProvider(&idpModel.IdentityProviderWriteModel), nil } func (c *Commands) RemoveIDPFromLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider) (*domain.ObjectDetails, error) { if resourceOwner == "" { return nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") } if !idpProvider.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-66m9s", "Errors.Org.LoginPolicy.IDP.Invalid") } existingPolicy, err := c.orgLoginPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-GVDfe", "Errors.Org.LoginPolicy.NotFound") } idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID) err = c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { return nil, err } if idpModel.State == domain.IdentityProviderStateUnspecified || idpModel.State == domain.IdentityProviderStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-39fjs", "Errors.Org.LoginPolicy.IDP.NotExisting") } orgAgg := OrgAggregateFromWriteModel(&idpModel.IdentityProviderWriteModel.WriteModel) events := c.removeIDPFromLoginPolicy(ctx, orgAgg, idpProvider.IDPConfigID, false) pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } err = AppendAndReduce(idpModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&idpModel.WriteModel), nil } func (c *Commands) removeIDPFromLoginPolicy(ctx context.Context, orgAgg *eventstore.Aggregate, idpConfigID string, cascade bool, cascadeExternalIDPs ...*domain.UserIDPLink) []eventstore.Command { var events []eventstore.Command if cascade { events = append(events, org.NewIdentityProviderCascadeRemovedEvent(ctx, orgAgg, idpConfigID)) } else { events = append(events, org.NewIdentityProviderRemovedEvent(ctx, orgAgg, idpConfigID)) } for _, idp := range cascadeExternalIDPs { event, _, err := c.removeUserIDPLink(ctx, idp, true) if err != nil { logging.LogWithFields("COMMAND-n8RRf", "userid", idp.AggregateID, "idpconfigid", idp.IDPConfigID).WithError(err).Warn("could not cascade remove external idp") continue } events = append(events, event) } return events } func (c *Commands) AddSecondFactorToLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType, orgID string) (domain.SecondFactorType, *domain.ObjectDetails, error) { if orgID == "" { return domain.SecondFactorTypeUnspecified, nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") } if !secondFactor.Valid() { return domain.SecondFactorTypeUnspecified, nil, zerrors.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) addedEvent, err := c.addSecondFactorToLoginPolicy(ctx, secondFactorModel, secondFactor) if err != nil { return domain.SecondFactorTypeUnspecified, nil, err } pushedEvents, err := c.eventstore.Push(ctx, addedEvent) if err != nil { return domain.SecondFactorTypeUnspecified, nil, err } err = AppendAndReduce(secondFactorModel, pushedEvents...) if err != nil { return domain.SecondFactorTypeUnspecified, nil, err } return secondFactorModel.MFAType, writeModelToObjectDetails(&secondFactorModel.WriteModel), nil } func (c *Commands) addSecondFactorToLoginPolicy(ctx context.Context, secondFactorModel *OrgSecondFactorWriteModel, secondFactor domain.SecondFactorType) (*org.LoginPolicySecondFactorAddedEvent, error) { err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) if err != nil { return nil, err } if secondFactorModel.State == domain.FactorStateActive { return nil, zerrors.ThrowAlreadyExists(nil, "Org-2B0ps", "Errors.Org.LoginPolicy.MFA.AlreadyExists") } orgAgg := OrgAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) return org.NewLoginPolicySecondFactorAddedEvent(ctx, orgAgg, secondFactor), nil } func (c *Commands) RemoveSecondFactorFromLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType, orgID string) (*domain.ObjectDetails, error) { if orgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "Org-fM0gs", "Errors.ResourceOwnerMissing") } if !secondFactor.Valid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-55n8s", "Errors.Org.LoginPolicy.MFA.Unspecified") } secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) removedEvent, err := c.removeSecondFactorFromLoginPolicy(ctx, secondFactorModel, secondFactor) if err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, removedEvent) if err != nil { return nil, err } err = AppendAndReduce(secondFactorModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&secondFactorModel.WriteModel), nil } func (c *Commands) removeSecondFactorFromLoginPolicy(ctx context.Context, secondFactorModel *OrgSecondFactorWriteModel, secondFactor domain.SecondFactorType) (*org.LoginPolicySecondFactorRemovedEvent, error) { err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) if err != nil { return nil, err } if secondFactorModel.State == domain.FactorStateUnspecified || secondFactorModel.State == domain.FactorStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-3M9od", "Errors.Org.LoginPolicy.MFA.NotExisting") } orgAgg := OrgAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) return org.NewLoginPolicySecondFactorRemovedEvent(ctx, orgAgg, secondFactor), nil } func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (domain.MultiFactorType, *domain.ObjectDetails, error) { if orgID == "" { return domain.MultiFactorTypeUnspecified, nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") } if !multiFactor.Valid() { return domain.MultiFactorTypeUnspecified, nil, zerrors.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) addedEvent, err := c.addMultiFactorToLoginPolicy(ctx, multiFactorModel, multiFactor) if err != nil { return domain.MultiFactorTypeUnspecified, nil, err } pushedEvents, err := c.eventstore.Push(ctx, addedEvent) if err != nil { return domain.MultiFactorTypeUnspecified, nil, err } err = AppendAndReduce(multiFactorModel, pushedEvents...) if err != nil { return domain.MultiFactorTypeUnspecified, nil, err } return multiFactorModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil } func (c *Commands) addMultiFactorToLoginPolicy(ctx context.Context, multiFactorModel *OrgMultiFactorWriteModel, multiFactor domain.MultiFactorType) (*org.LoginPolicyMultiFactorAddedEvent, error) { err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) if err != nil { return nil, err } if multiFactorModel.State == domain.FactorStateActive { return nil, zerrors.ThrowAlreadyExists(nil, "Org-3M9od", "Errors.Org.LoginPolicy.MFA.AlreadyExists") } orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.WriteModel) return org.NewLoginPolicyMultiFactorAddedEvent(ctx, orgAgg, multiFactor), nil } func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (*domain.ObjectDetails, error) { if orgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") } if !multiFactor.Valid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) removedEvent, err := c.removeMultiFactorFromLoginPolicy(ctx, multiFactorModel, multiFactor) if err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, removedEvent) if err != nil { return nil, err } err = AppendAndReduce(multiFactorModel, pushedEvents...) if err != nil { return nil, err } return writeModelToObjectDetails(&multiFactorModel.WriteModel), nil } func (c *Commands) removeMultiFactorFromLoginPolicy(ctx context.Context, multiFactorModel *OrgMultiFactorWriteModel, multiFactor domain.MultiFactorType) (*org.LoginPolicyMultiFactorRemovedEvent, error) { err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) if err != nil { return nil, err } if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved { return nil, zerrors.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting") } orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel) return org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor), nil } func (c *Commands) orgLoginPolicyAuthFactorsWriteModel(ctx context.Context, orgID string) (_ *OrgAuthFactorsAllowedWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() writeModel := NewOrgAuthFactorsAllowedWriteModel(ctx, orgID) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } return writeModel, nil } func prepareAddLoginPolicy(a *org.Aggregate, policy *AddLoginPolicy) preparation.Validation { return func() (preparation.CreateCommands, error) { if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { return nil, zerrors.ThrowInvalidArgument(nil, "Org-WSfdq", "Errors.Org.LoginPolicy.RedirectURIInvalid") } for _, factor := range policy.SecondFactors { if !factor.Valid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-SFeea", "Errors.Org.LoginPolicy.MFA.Unspecified") } } for _, factor := range policy.MultiFactors { if !factor.Valid() { return nil, zerrors.ThrowInvalidArgument(nil, "Org-WSfrg", "Errors.Org.LoginPolicy.MFA.Unspecified") } } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if exists, err := exists(ctx, filter, NewOrgLoginPolicyWriteModel(a.ID)); exists || err != nil { return nil, zerrors.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists") } for _, idp := range policy.IDPProviders { exists, err := ExistsIDPOnOrgOrInstance(ctx, filter, authz.GetInstance(ctx).InstanceID(), a.ResourceOwner, idp.ConfigID) if !exists || err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "Org-FEd32", "Errors.IDPConfig.NotExisting") } } cmds := make([]eventstore.Command, 0, len(policy.SecondFactors)+len(policy.MultiFactors)+len(policy.IDPProviders)+1) cmds = append(cmds, org.NewLoginPolicyAddedEvent(ctx, &a.Aggregate, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.ForceMFALocalOnly, policy.HidePasswordReset, policy.IgnoreUnknownUsernames, policy.AllowDomainDiscovery, policy.DisableLoginWithEmail, policy.DisableLoginWithPhone, policy.PasswordlessType, policy.DefaultRedirectURI, policy.PasswordCheckLifetime, policy.ExternalLoginCheckLifetime, policy.MFAInitSkipLifetime, policy.SecondFactorCheckLifetime, policy.MultiFactorCheckLifetime, )) for _, factor := range policy.SecondFactors { cmds = append(cmds, org.NewLoginPolicySecondFactorAddedEvent(ctx, &a.Aggregate, factor)) } for _, factor := range policy.MultiFactors { cmds = append(cmds, org.NewLoginPolicyMultiFactorAddedEvent(ctx, &a.Aggregate, factor)) } for _, idp := range policy.IDPProviders { cmds = append(cmds, org.NewIdentityProviderAddedEvent(ctx, &a.Aggregate, idp.ConfigID, idp.Type)) } return cmds, nil }, nil } } func prepareChangeLoginPolicy(a *org.Aggregate, policy *ChangeLoginPolicy) preparation.Validation { return func() (preparation.CreateCommands, error) { if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { return nil, zerrors.ThrowInvalidArgument(nil, "Org-Sfd21", "Errors.Org.LoginPolicy.RedirectURIInvalid") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { wm := NewOrgLoginPolicyWriteModel(a.ID) if err := queryAndReduce(ctx, filter, wm); err != nil { return nil, err } if !wm.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound") } changedEvent, hasChanged := wm.NewChangedEvent(ctx, &a.Aggregate, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.ForceMFALocalOnly, policy.HidePasswordReset, policy.IgnoreUnknownUsernames, policy.AllowDomainDiscovery, policy.DisableLoginWithEmail, policy.DisableLoginWithPhone, policy.PasswordlessType, policy.DefaultRedirectURI, policy.PasswordCheckLifetime, policy.ExternalLoginCheckLifetime, policy.MFAInitSkipLifetime, policy.SecondFactorCheckLifetime, policy.MultiFactorCheckLifetime) if !hasChanged { return nil, zerrors.ThrowPreconditionFailed(nil, "Org-5M9vdd", "Errors.Org.LoginPolicy.NotChanged") } return []eventstore.Command{changedEvent}, nil }, nil } }