diff --git a/internal/command/org_features.go b/internal/command/org_features.go index 900db19cd7..7aef8488df 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -2,12 +2,17 @@ package command import ( "context" + "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/org" ) func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, features *domain.Features) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Features-G5tg", "Errors.ResourceOwnerMissing") + } existingFeatures := NewOrgFeaturesWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures) if err != nil { @@ -33,7 +38,13 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") } - pushedEvents, err := c.eventstore.PushEvents(ctx, setEvent) + events, err := c.ensureOrgSettingsToFeatures(ctx, resourceOwner, features) + if err != nil { + return nil, err + } + events = append(events, setEvent) + + pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { return nil, err } @@ -45,6 +56,9 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea } func (c *Commands) RemoveOrgFeatures(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { + if orgID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Features-G5tg", "Errors.ResourceOwnerMissing") + } existingFeatures := NewOrgFeaturesWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures) if err != nil { @@ -53,9 +67,19 @@ func (c *Commands) RemoveOrgFeatures(ctx context.Context, orgID string) (*domain if existingFeatures.State == domain.FeaturesStateUnspecified || existingFeatures.State == domain.FeaturesStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "Features-Bg32G", "Errors.Features.NotFound") } + removedEvent := org.NewFeaturesRemovedEvent(ctx, OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel)) - orgAgg := OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewFeaturesRemovedEvent(ctx, orgAgg)) + features, err := c.getDefaultFeatures(ctx) + if err != nil { + return nil, err + } + events, err := c.ensureOrgSettingsToFeatures(ctx, orgID, features) + if err != nil { + return nil, err + } + + events = append(events, removedEvent) + pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { return nil, err } @@ -65,3 +89,130 @@ func (c *Commands) RemoveOrgFeatures(ctx context.Context, orgID string) (*domain } return writeModelToObjectDetails(&existingFeatures.WriteModel), nil } + +func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string, features *domain.Features) ([]eventstore.EventPusher, error) { + events, err := c.setAllowedLoginPolicy(ctx, orgID, features) + if err != nil { + return nil, err + } + if !features.PasswordComplexityPolicy { + removePasswordComplexityEvent, err := c.removePasswordComplexityPolicyIfExists(ctx, orgID) + if err != nil { + return nil, err + } + if removePasswordComplexityEvent != nil { + events = append(events, removePasswordComplexityEvent) + } + } + if !features.LabelPolicy { + removeLabelPolicyEvent, err := c.removeLabelPolicyIfExists(ctx, orgID) + if err != nil { + return nil, err + } + if removeLabelPolicyEvent != nil { + events = append(events, removeLabelPolicyEvent) + } + } + return events, nil +} + +func (c *Commands) setAllowedLoginPolicy(ctx context.Context, orgID string, features *domain.Features) ([]eventstore.EventPusher, error) { + events := make([]eventstore.EventPusher, 0) + existingPolicy, err := c.orgLoginPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, nil + } + defaultPolicy, err := c.getDefaultLoginPolicy(ctx) + if err != nil { + return nil, err + } + policy := *existingPolicy + if !features.LoginPolicyFactors { + if defaultPolicy.ForceMFA != existingPolicy.ForceMFA { + policy.ForceMFA = defaultPolicy.ForceMFA + } + authFactorsEvents, err := c.setDefaultAuthFactorsInCustomLoginPolicy(ctx, orgID) + if err != nil { + return nil, err + } + events = append(events, authFactorsEvents...) + } + if !features.LoginPolicyIDP { + if defaultPolicy.AllowExternalIDP != existingPolicy.AllowExternalIDP { + policy.AllowExternalIDP = defaultPolicy.AllowExternalIDP + } + //TODO: handle idps + } + if !features.LoginPolicyRegistration && defaultPolicy.AllowRegister != existingPolicy.AllowRegister { + policy.AllowRegister = defaultPolicy.AllowRegister + } + if !features.LoginPolicyPasswordless && defaultPolicy.PasswordlessType != existingPolicy.PasswordlessType { + policy.PasswordlessType = defaultPolicy.PasswordlessType + } + if !features.LoginPolicyUsernameLogin && defaultPolicy.AllowUsernamePassword != existingPolicy.AllowUserNamePassword { + policy.AllowUserNamePassword = defaultPolicy.AllowUsernamePassword + } + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, OrgAggregateFromWriteModel(&existingPolicy.WriteModel), policy.AllowUserNamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType) + if hasChanged { + events = append(events, changedEvent) + } + return events, nil +} + +func (c *Commands) setDefaultAuthFactorsInCustomLoginPolicy(ctx context.Context, orgID string) ([]eventstore.EventPusher, error) { + orgAuthFactors, err := c.orgLoginPolicyAuthFactorsWriteModel(ctx, orgID) + if err != nil { + return nil, err + } + events := make([]eventstore.EventPusher, 0) + for factor, state := range orgAuthFactors.SecondFactors { + if state.IAM == state.Org { + continue + } + secondFactorWriteModel := orgAuthFactors.ToSecondFactorWriteModel(factor) + if state.IAM == domain.FactorStateActive { + event, err := c.addSecondFactorToLoginPolicy(ctx, secondFactorWriteModel, factor) + if err != nil { + return nil, err + } + if event != nil { + events = append(events, event) + } + continue + } + event, err := c.removeSecondFactorFromLoginPolicy(ctx, secondFactorWriteModel, factor) + if err != nil { + return nil, err + } + if event != nil { + events = append(events, event) + } + } + for factor, state := range orgAuthFactors.MultiFactors { + if state.IAM == state.Org { + continue + } + multiFactorWriteModel := orgAuthFactors.ToMultiFactorWriteModel(factor) + if state.IAM == domain.FactorStateActive { + event, err := c.addMultiFactorToLoginPolicy(ctx, multiFactorWriteModel, factor) + if err != nil { + return nil, err + } + if event != nil { + events = append(events, event) + } + continue + } + event, err := c.removeMultiFactorFromLoginPolicy(ctx, multiFactorWriteModel, factor) + if err != nil { + return nil, err + } + if event != nil { + events = append(events, event) + } + } + return events, nil +} diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go new file mode 100644 index 0000000000..9f37bf4099 --- /dev/null +++ b/internal/command/org_features_test.go @@ -0,0 +1,514 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/features" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestCommandSide_SetOrgFeatures(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + features *domain.Features + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + features: &domain.Features{ + TierName: "Test", + State: domain.FeaturesStateActive, + AuditLogRetention: time.Hour, + LoginPolicyFactors: false, + LoginPolicyIDP: false, + LoginPolicyPasswordless: false, + LoginPolicyRegistration: false, + LoginPolicyUsernameLogin: false, + PasswordComplexityPolicy: false, + LabelPolicy: false, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no change, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + features: &domain.Features{ + TierName: "Test", + State: domain.FeaturesStateActive, + AuditLogRetention: time.Hour, + LoginPolicyFactors: false, + LoginPolicyIDP: false, + LoginPolicyPasswordless: false, + LoginPolicyRegistration: false, + LoginPolicyUsernameLogin: false, + PasswordComplexityPolicy: false, + LabelPolicy: false, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "set with default policies, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + false, + false, + false, + false, + domain.PasswordlessTypeAllowed, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 8, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "primary", + "secondary", + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + features: &domain.Features{ + TierName: "Test", + State: domain.FeaturesStateActive, + AuditLogRetention: time.Hour, + LoginPolicyFactors: false, + LoginPolicyIDP: false, + LoginPolicyPasswordless: false, + LoginPolicyRegistration: false, + LoginPolicyUsernameLogin: false, + PasswordComplexityPolicy: false, + LabelPolicy: false, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "set with custom policies, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + //NewOrgFeaturesWriteModel + expectFilter(), + //begin ensureOrgSettingsToFeatures + //begin setAllowedLoginPolicy + //orgLoginPolicyWriteModelByID + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewLoginPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + false, + false, + false, + false, + domain.PasswordlessTypeNotAllowed, + ), + ), + ), + //getDefaultLoginPolicy + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + //begin setDefaultAuthFactorsInCustomLoginPolicy + //orgLoginPolicyAuthFactorsWriteModel + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicySecondFactorAddedEvent(context.Background(), &iam.NewAggregate().Aggregate, domain.SecondFactorTypeU2F), + ), + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorAddedEvent(context.Background(), &iam.NewAggregate().Aggregate, domain.MultiFactorTypeU2FWithPIN), + ), + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.SecondFactorTypeOTP), + ), + ), + //addSecondFactorToLoginPolicy + expectFilter(), + //removeSecondFactorFromLoginPolicy + expectFilter(), + //addMultiFactorToLoginPolicy + expectFilter(), + //end setDefaultAuthFactorsInCustomLoginPolicy + //orgPasswordComplexityPolicyWriteModelByID + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 8, + false, + false, + false, + false, + ), + ), + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 7, + false, + false, + false, + false, + ), + ), + ), + //orgLabelPolicyWriteModelByID + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "primary", + "secondary", + false, + ), + ), + eventFromEventPusher( + org.NewLabelPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "custom", + "secondary", + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.SecondFactorTypeU2F), + ), + eventFromEventPusher( + org.NewLoginPolicySecondFactorRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.SecondFactorTypeOTP), + ), + eventFromEventPusher( + org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.MultiFactorTypeU2FWithPIN), + ), + eventFromEventPusher( + newLoginPolicyChangedEvent(context.Background(), "org1", true, true, true, true, domain.PasswordlessTypeAllowed), + ), + eventFromEventPusher( + org.NewPasswordComplexityPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), + ), + eventFromEventPusher( + org.NewLabelPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), + ), + eventFromEventPusher( + newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + features: &domain.Features{ + TierName: "Test", + State: domain.FeaturesStateActive, + AuditLogRetention: time.Hour, + LoginPolicyFactors: false, + LoginPolicyIDP: false, + LoginPolicyPasswordless: false, + LoginPolicyRegistration: false, + LoginPolicyUsernameLogin: false, + PasswordComplexityPolicy: false, + LabelPolicy: false, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.SetOrgFeatures(tt.args.ctx, tt.args.resourceOwner, tt.args.features) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveOrgFeatures(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no features set, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove with default policies, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour)), + ), + expectFilter( + eventFromEventPusher( + newIAMFeaturesSetEvent(context.Background(), "Default", domain.FeaturesStateActive, time.Hour)), + ), + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + false, + false, + false, + false, + domain.PasswordlessTypeAllowed, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 8, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + "primary", + "secondary", + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewFeaturesRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveOrgFeatures(tt.args.ctx, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newIAMFeaturesSetEvent(ctx context.Context, tierName string, state domain.FeaturesState, auditLog time.Duration) *iam.FeaturesSetEvent { + event, _ := iam.NewFeaturesSetEvent( + ctx, + &iam.NewAggregate().Aggregate, + []features.FeaturesChanges{ + features.ChangeTierName(tierName), + features.ChangeState(state), + features.ChangeAuditLogRetention(auditLog), + }, + ) + return event +} + +func newFeaturesSetEvent(ctx context.Context, orgID string, tierName string, state domain.FeaturesState, auditLog time.Duration) *org.FeaturesSetEvent { + event, _ := org.NewFeaturesSetEvent( + ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []features.FeaturesChanges{ + features.ChangeTierName(tierName), + features.ChangeState(state), + features.ChangeAuditLogRetention(auditLog), + }, + ) + return event +} diff --git a/internal/command/org_idp_config.go b/internal/command/org_idp_config.go index 3e77326e0f..9abf73e3e1 100644 --- a/internal/command/org_idp_config.go +++ b/internal/command/org_idp_config.go @@ -142,22 +142,9 @@ func (c *Commands) RemoveIDPConfig(ctx context.Context, idpID, orgID string, cas if err != nil { return nil, err } - - if existingIDP.State == domain.IDPConfigStateRemoved || existingIDP.State == domain.IDPConfigStateUnspecified { - return nil, caos_errs.ThrowNotFound(nil, "Org-Yx9vd", "Errors.Org.IDPConfig.NotExisting") - } - if existingIDP.State != domain.IDPConfigStateInactive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5Mo0d", "Errors.Org.IDPConfig.NotInactive") - } - - orgAgg := OrgAggregateFromWriteModel(&existingIDP.WriteModel) - events := []eventstore.EventPusher{ - org_repo.NewIDPConfigRemovedEvent(ctx, orgAgg, idpID, existingIDP.Name), - } - - if cascadeRemoveProvider { - removeIDPEvents := c.removeIDPProviderFromLoginPolicy(ctx, orgAgg, idpID, true, cascadeExternalIDPs...) - events = append(events, removeIDPEvents...) + events, err := c.removeIDPConfig(ctx, existingIDP, cascadeRemoveProvider, cascadeExternalIDPs...) + if err != nil { + return nil, err } pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { @@ -170,6 +157,26 @@ func (c *Commands) RemoveIDPConfig(ctx context.Context, idpID, orgID string, cas return writeModelToObjectDetails(&existingIDP.IDPConfigWriteModel.WriteModel), nil } +func (c *Commands) removeIDPConfig(ctx context.Context, existingIDP *OrgIDPConfigWriteModel, cascadeRemoveProvider bool, cascadeExternalIDPs ...*domain.ExternalIDP) ([]eventstore.EventPusher, error) { + if existingIDP.State == domain.IDPConfigStateRemoved || existingIDP.State == domain.IDPConfigStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "Org-Yx9vd", "Errors.Org.IDPConfig.NotExisting") + } + if existingIDP.State != domain.IDPConfigStateInactive { + return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5Mo0d", "Errors.Org.IDPConfig.NotInactive") + } + + orgAgg := OrgAggregateFromWriteModel(&existingIDP.WriteModel) + events := []eventstore.EventPusher{ + org_repo.NewIDPConfigRemovedEvent(ctx, orgAgg, existingIDP.AggregateID, existingIDP.Name), + } + + if cascadeRemoveProvider { + removeIDPEvents := c.removeIDPProviderFromLoginPolicy(ctx, orgAgg, existingIDP.AggregateID, true, cascadeExternalIDPs...) + events = append(events, removeIDPEvents...) + } + return events, nil +} + func (c *Commands) getOrgIDPConfigByID(ctx context.Context, idpID, orgID string) (*domain.IDPConfig, error) { config, err := c.orgIDPConfigWriteModelByID(ctx, idpID, orgID) if err != nil { diff --git a/internal/command/org_policy_label.go b/internal/command/org_policy_label.go index 479e5d38a2..2c0a2b9af1 100644 --- a/internal/command/org_policy_label.go +++ b/internal/command/org_policy_label.go @@ -74,15 +74,11 @@ func (c *Commands) RemoveLabelPolicy(ctx context.Context, orgID string) (*domain return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing") } existingPolicy := NewOrgLabelPolicyWriteModel(orgID) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + removeEvent, err := c.removeLabelPolicy(ctx, existingPolicy) if err != nil { return nil, err } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LabelPolicy.NotFound") - } - orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLabelPolicyRemovedEvent(ctx, orgAgg)) + pushedEvents, err := c.eventstore.PushEvents(ctx, removeEvent) if err != nil { return nil, err } @@ -92,3 +88,36 @@ func (c *Commands) RemoveLabelPolicy(ctx context.Context, orgID string) (*domain } return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } + +func (c *Commands) removeLabelPolicy(ctx context.Context, existingPolicy *OrgLabelPolicyWriteModel) (*org.LabelPolicyRemovedEvent, error) { + err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LabelPolicy.NotFound") + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewLabelPolicyRemovedEvent(ctx, orgAgg), nil +} + +func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string) (*org.LabelPolicyRemovedEvent, error) { + existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingPolicy.State != domain.PolicyStateActive { + return nil, nil + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewLabelPolicyRemovedEvent(ctx, orgAgg), nil +} + +func (c *Commands) orgLabelPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLabelPolicyWriteModel, error) { + policy := NewOrgLabelPolicyWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, policy) + if err != nil { + return nil, err + } + return policy, nil +} diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index 3663127b67..d65501b5a3 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -11,6 +11,7 @@ import ( caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/telemetry/tracing" ) func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { @@ -238,18 +239,12 @@ func (c *Commands) AddSecondFactorToLoginPolicy(ctx context.Context, secondFacto return domain.SecondFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) - err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) + addedEvent, err := c.addSecondFactorToLoginPolicy(ctx, secondFactorModel, secondFactor) if err != nil { return domain.SecondFactorTypeUnspecified, nil, err } - if secondFactorModel.State == domain.FactorStateActive { - return domain.SecondFactorTypeUnspecified, nil, caos_errs.ThrowAlreadyExists(nil, "Org-2B0ps", "Errors.Org.LoginPolicy.MFA.AlreadyExists") - } - - orgAgg := OrgAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) - - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicySecondFactorAddedEvent(ctx, orgAgg, secondFactor)) + pushedEvents, err := c.eventstore.PushEvents(ctx, addedEvent) if err != nil { return domain.SecondFactorTypeUnspecified, nil, err } @@ -261,6 +256,20 @@ func (c *Commands) AddSecondFactorToLoginPolicy(ctx context.Context, secondFacto 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, caos_errs.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, caos_errs.ThrowInvalidArgument(nil, "Org-fM0gs", "Errors.ResourceOwnerMissing") @@ -269,16 +278,12 @@ func (c *Commands) RemoveSecondFactorFromLoginPolicy(ctx context.Context, second return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55n8s", "Errors.Org.LoginPolicy.MFA.Unspecified") } secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) - err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) + removedEvent, err := c.removeSecondFactorFromLoginPolicy(ctx, secondFactorModel, secondFactor) if err != nil { return nil, err } - if secondFactorModel.State == domain.FactorStateUnspecified || secondFactorModel.State == domain.FactorStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "Org-3M9od", "Errors.Org.LoginPolicy.MFA.NotExisting") - } - orgAgg := OrgAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicySecondFactorRemovedEvent(ctx, orgAgg, secondFactor)) + pushedEvents, err := c.eventstore.PushEvents(ctx, removedEvent) if err != nil { return nil, err } @@ -289,6 +294,18 @@ func (c *Commands) RemoveSecondFactorFromLoginPolicy(ctx context.Context, second 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, caos_errs.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, caos_errs.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") @@ -297,17 +314,12 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) - err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) + addedEvent, err := c.addMultiFactorToLoginPolicy(ctx, multiFactorModel, multiFactor) if err != nil { return domain.MultiFactorTypeUnspecified, nil, err } - if multiFactorModel.State == domain.FactorStateActive { - return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowAlreadyExists(nil, "Org-3M9od", "Errors.Org.LoginPolicy.MFA.AlreadyExists") - } - orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.WriteModel) - - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorAddedEvent(ctx, orgAgg, multiFactor)) + pushedEvents, err := c.eventstore.PushEvents(ctx, addedEvent) if err != nil { return domain.MultiFactorTypeUnspecified, nil, err } @@ -318,6 +330,19 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor return multiFactorModel.MultiFactorWriteModel.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, caos_errs.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, caos_errs.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") @@ -326,16 +351,12 @@ func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFa return nil, caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") } multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) - err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) + removedEvent, err := c.removeMultiFactorFromLoginPolicy(ctx, multiFactorModel, multiFactor) if err != nil { return nil, err } - if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting") - } - orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor)) + pushedEvents, err := c.eventstore.PushEvents(ctx, removedEvent) if err != nil { return nil, err } @@ -345,3 +366,28 @@ func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFa } 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, caos_errs.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(orgID) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/org_policy_login_factors_model.go b/internal/command/org_policy_login_factors_model.go index 4d432e59ca..f7b942d30d 100644 --- a/internal/command/org_policy_login_factors_model.go +++ b/internal/command/org_policy_login_factors_model.go @@ -3,6 +3,7 @@ package command import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/iam" "github.com/caos/zitadel/internal/repository/org" ) @@ -93,3 +94,99 @@ func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder { org.LoginPolicyMultiFactorAddedEventType, org.LoginPolicyMultiFactorRemovedEventType) } + +func NewOrgAuthFactorsAllowedWriteModel(orgID string) *OrgAuthFactorsAllowedWriteModel { + return &OrgAuthFactorsAllowedWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + SecondFactors: map[domain.SecondFactorType]*factorState{}, + MultiFactors: map[domain.MultiFactorType]*factorState{}, + } +} + +type OrgAuthFactorsAllowedWriteModel struct { + eventstore.WriteModel + SecondFactors map[domain.SecondFactorType]*factorState + MultiFactors map[domain.MultiFactorType]*factorState +} + +type factorState struct { + IAM domain.FactorState + Org domain.FactorState +} + +func (wm *OrgAuthFactorsAllowedWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *iam.LoginPolicySecondFactorAddedEvent: + wm.ensureSecondFactor(e.MFAType) + wm.SecondFactors[e.MFAType].IAM = domain.FactorStateActive + case *iam.LoginPolicySecondFactorRemovedEvent: + wm.ensureSecondFactor(e.MFAType) + wm.SecondFactors[e.MFAType].IAM = domain.FactorStateRemoved + case *org.LoginPolicySecondFactorAddedEvent: + wm.ensureSecondFactor(e.MFAType) + wm.SecondFactors[e.MFAType].Org = domain.FactorStateActive + case *org.LoginPolicySecondFactorRemovedEvent: + wm.ensureSecondFactor(e.MFAType) + wm.SecondFactors[e.MFAType].Org = domain.FactorStateRemoved + case *iam.LoginPolicyMultiFactorAddedEvent: + wm.ensureMultiFactor(e.MFAType) + wm.MultiFactors[e.MFAType].IAM = domain.FactorStateActive + case *iam.LoginPolicyMultiFactorRemovedEvent: + wm.ensureMultiFactor(e.MFAType) + wm.MultiFactors[e.MFAType].IAM = domain.FactorStateRemoved + case *org.LoginPolicyMultiFactorAddedEvent: + wm.ensureMultiFactor(e.MFAType) + wm.MultiFactors[e.MFAType].Org = domain.FactorStateActive + case *org.LoginPolicyMultiFactorRemovedEvent: + wm.ensureMultiFactor(e.MFAType) + wm.MultiFactors[e.MFAType].Org = domain.FactorStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *OrgAuthFactorsAllowedWriteModel) ensureSecondFactor(secondFactor domain.SecondFactorType) { + _, ok := wm.SecondFactors[secondFactor] + if !ok { + wm.SecondFactors[secondFactor] = &factorState{} + } +} + +func (wm *OrgAuthFactorsAllowedWriteModel) ensureMultiFactor(multiFactor domain.MultiFactorType) { + _, ok := wm.MultiFactors[multiFactor] + if !ok { + wm.MultiFactors[multiFactor] = &factorState{} + } +} + +func (wm *OrgAuthFactorsAllowedWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType, org.AggregateType). + AggregateIDs(domain.IAMID, wm.WriteModel.AggregateID). + EventTypes( + iam.LoginPolicySecondFactorAddedEventType, + iam.LoginPolicySecondFactorRemovedEventType, + iam.LoginPolicyMultiFactorAddedEventType, + iam.LoginPolicyMultiFactorRemovedEventType, + org.LoginPolicySecondFactorAddedEventType, + org.LoginPolicySecondFactorRemovedEventType, + org.LoginPolicyMultiFactorAddedEventType, + org.LoginPolicyMultiFactorRemovedEventType) +} + +func (wm *OrgAuthFactorsAllowedWriteModel) ToSecondFactorWriteModel(factor domain.SecondFactorType) *OrgSecondFactorWriteModel { + orgSecondFactorWriteModel := NewOrgSecondFactorWriteModel(wm.AggregateID, factor) + orgSecondFactorWriteModel.ProcessedSequence = wm.ProcessedSequence + orgSecondFactorWriteModel.State = wm.SecondFactors[factor].Org + return orgSecondFactorWriteModel +} + +func (wm *OrgAuthFactorsAllowedWriteModel) ToMultiFactorWriteModel(factor domain.MultiFactorType) *OrgMultiFactorWriteModel { + orgMultiFactorWriteModel := NewOrgMultiFactorWriteModel(wm.AggregateID, factor) + orgMultiFactorWriteModel.ProcessedSequence = wm.ProcessedSequence + orgMultiFactorWriteModel.State = wm.MultiFactors[factor].Org + return orgMultiFactorWriteModel +} diff --git a/internal/command/org_policy_password_complexity.go b/internal/command/org_policy_password_complexity.go index 9bba17d644..910d3cfa6f 100644 --- a/internal/command/org_policy_password_complexity.go +++ b/internal/command/org_policy_password_complexity.go @@ -9,8 +9,7 @@ import ( ) func (c *Commands) getOrgPasswordComplexityPolicy(ctx context.Context, orgID string) (*domain.PasswordComplexityPolicy, error) { - policy := NewOrgPasswordComplexityPolicyWriteModel(orgID) - err := c.eventstore.FilterToQueryReducer(ctx, policy) + policy, err := c.orgPasswordComplexityPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err } @@ -20,6 +19,15 @@ func (c *Commands) getOrgPasswordComplexityPolicy(ctx context.Context, orgID str return c.getDefaultPasswordComplexityPolicy(ctx) } +func (c *Commands) orgPasswordComplexityPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgPasswordComplexityPolicyWriteModel, error) { + policy := NewOrgPasswordComplexityPolicyWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, policy) + if err != nil { + return nil, err + } + return policy, nil +} + func (c *Commands) AddPasswordComplexityPolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordComplexityPolicy) (*domain.PasswordComplexityPolicy, error) { if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-7ufEs", "Errors.ResourceOwnerMissing") @@ -96,15 +104,11 @@ func (c *Commands) RemovePasswordComplexityPolicy(ctx context.Context, orgID str return nil, caos_errs.ThrowInvalidArgument(nil, "Org-J8fsf", "Errors.ResourceOwnerMissing") } existingPolicy := NewOrgPasswordComplexityPolicyWriteModel(orgID) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + event, err := c.removePasswordComplexityPolicy(ctx, existingPolicy) if err != nil { return nil, err } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "ORG-ADgs2", "Errors.Org.PasswordComplexityPolicy.NotFound") - } - orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewPasswordComplexityPolicyRemovedEvent(ctx, orgAgg)) + pushedEvents, err := c.eventstore.PushEvents(ctx, event) if err != nil { return nil, err } @@ -114,3 +118,27 @@ func (c *Commands) RemovePasswordComplexityPolicy(ctx context.Context, orgID str } return writeModelToObjectDetails(&existingPolicy.PasswordComplexityPolicyWriteModel.WriteModel), nil } + +func (c *Commands) removePasswordComplexityPolicy(ctx context.Context, existingPolicy *OrgPasswordComplexityPolicyWriteModel) (*org.PasswordComplexityPolicyRemovedEvent, error) { + err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + if err != nil { + return nil, err + } + if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "ORG-ADgs2", "Errors.Org.PasswordComplexityPolicy.NotFound") + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewPasswordComplexityPolicyRemovedEvent(ctx, orgAgg), nil +} + +func (c *Commands) removePasswordComplexityPolicyIfExists(ctx context.Context, orgID string) (*org.PasswordComplexityPolicyRemovedEvent, error) { + existingPolicy, err := c.orgPasswordComplexityPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingPolicy.State != domain.PolicyStateActive { + return nil, nil + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewPasswordComplexityPolicyRemovedEvent(ctx, orgAgg), nil +}