diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 1b2c76665f..907b186e4f 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -2507,6 +2507,7 @@ This is an empty request | metadata_user | bool | - | | | custom_text_message | bool | - | | | custom_text_login | bool | - | | +| lockout_policy | bool | - | | @@ -2695,6 +2696,7 @@ This is an empty request | metadata_user | bool | - | | | custom_text_message | bool | - | | | custom_text_login | bool | - | | +| lockout_policy | bool | - | | diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index ae74ec7341..92f666f056 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -78,6 +78,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) MetadataUser: req.MetadataUser, CustomTextLogin: req.CustomTextLogin || req.CustomText, CustomTextMessage: req.CustomTextMessage, + LockoutPolicy: req.LockoutPolicy, } } @@ -102,5 +103,6 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. MetadataUser: req.MetadataUser, CustomTextLogin: req.CustomTextLogin || req.CustomText, CustomTextMessage: req.CustomTextMessage, + LockoutPolicy: req.LockoutPolicy, } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index e46d388db1..c0cd69acf9 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -32,6 +32,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu CustomTextMessage: features.CustomTextMessage, CustomTextLogin: features.CustomTextLogin, MetadataUser: features.MetadataUser, + LockoutPolicy: features.LockoutPolicy, } } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 123c95c160..ea937b4ca4 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -169,6 +169,12 @@ func checkFeatures(features *features_view_model.FeaturesView, requiredFeatures } continue } + if requiredFeature == domain.FeatureLockoutPolicy { + if !features.LockoutPolicy { + return MissingFeatureErr(requiredFeature) + } + continue + } if requiredFeature == domain.FeatureMetadataUser { if !features.MetadataUser { return MissingFeatureErr(requiredFeature) diff --git a/internal/command/features_model.go b/internal/command/features_model.go index d8471a59c8..f7813bf872 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -30,6 +30,7 @@ type FeaturesWriteModel struct { MetadataUser bool CustomTextMessage bool CustomTextLogin bool + LockoutPolicy bool } func (wm *FeaturesWriteModel) Reduce() error { @@ -94,6 +95,9 @@ func (wm *FeaturesWriteModel) Reduce() error { if e.CustomTextLogin != nil { wm.CustomTextLogin = *e.CustomTextLogin } + if e.LockoutPolicy != nil { + wm.LockoutPolicy = *e.LockoutPolicy + } case *features.FeaturesRemovedEvent: wm.State = domain.FeaturesStateRemoved } diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index 5260055332..0c33d5b585 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -53,6 +53,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM features.MetadataUser, features.CustomTextMessage, features.CustomTextLogin, + features.LockoutPolicy, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 4e90206e78..372008ab40 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -70,7 +70,8 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( privacyPolicy, metadataUser, customTextMessage, - customTextLogin bool, + customTextLogin, + lockoutPolicy bool, ) (*iam.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -129,6 +130,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( if wm.CustomTextLogin != customTextLogin { changes = append(changes, features.ChangeCustomTextLogin(customTextLogin)) } + if wm.LockoutPolicy != lockoutPolicy { + changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/org_features.go b/internal/command/org_features.go index 7ce0953587..6a62fbd562 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -44,6 +44,7 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea features.MetadataUser, features.CustomTextMessage, features.CustomTextLogin, + features.LockoutPolicy, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") @@ -157,6 +158,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string events = append(events, removePrivacyPolicyEvent) } } + if !features.LockoutPolicy { + removeLockoutPolicyEvent, err := c.removeLockoutPolicyIfExists(ctx, orgID) + if err != nil { + return nil, err + } + if removeLockoutPolicyEvent != nil { + events = append(events, removeLockoutPolicyEvent) + } + } if !features.MetadataUser { removeOrgUserMetadatas, err := c.removeUserMetadataFromOrg(ctx, orgID) if err != nil { diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index 2086f897a8..5011743901 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -77,7 +77,8 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( privacyPolicy, metadataUser, customTextMessage, - customTextLogin bool, + customTextLogin, + lockoutPolicy bool, ) (*org.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -139,6 +140,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( if wm.CustomTextLogin != customTextLogin { changes = append(changes, features.ChangeCustomTextLogin(customTextLogin)) } + if wm.LockoutPolicy != lockoutPolicy { + changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy)) + } if len(changes) == 0 { return nil, false diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index 405d0caf66..839ee41b28 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -264,6 +264,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ @@ -296,6 +306,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { CustomTextLogin: false, PrivacyPolicy: false, MetadataUser: false, + LockoutPolicy: false, }, }, res: res{ @@ -450,6 +461,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ @@ -486,6 +507,8 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyWatermark: false, CustomDomain: false, MetadataUser: false, + PrivacyPolicy: false, + LockoutPolicy: false, }, }, res: res{ @@ -647,6 +670,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ @@ -686,6 +719,8 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyWatermark: false, CustomDomain: false, MetadataUser: false, + PrivacyPolicy: false, + LockoutPolicy: false, }, }, res: res{ @@ -854,6 +889,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ @@ -896,6 +941,8 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyWatermark: false, CustomDomain: false, MetadataUser: false, + PrivacyPolicy: false, + LockoutPolicy: false, }, }, res: res{ @@ -1116,6 +1163,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + org.NewLockoutPolicyAddedEvent( + context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ @@ -1146,6 +1203,9 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { eventFromEventPusher( org.NewPrivacyPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), ), + eventFromEventPusher( + org.NewLockoutPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), + ), eventFromEventPusher( newFeaturesSetEvent(context.Background(), "org1", "Test", domain.FeaturesStateActive, time.Hour), ), @@ -1172,6 +1232,8 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LabelPolicyWatermark: false, CustomDomain: false, MetadataUser: false, + PrivacyPolicy: false, + LockoutPolicy: false, }, }, res: res{ @@ -1305,6 +1367,16 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter( eventFromEventPusher( user.NewMetadataSetEvent( @@ -1349,6 +1421,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { CustomTextLogin: false, PrivacyPolicy: false, MetadataUser: false, + LockoutPolicy: false, }, }, res: res{ @@ -1551,6 +1624,16 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + iam.NewLockoutPolicyAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + 5, + false, + ), + ), + ), expectFilter(), expectPush( []*repository.Event{ diff --git a/internal/command/org_policy_password_lockout.go b/internal/command/org_policy_lockout.go similarity index 74% rename from internal/command/org_policy_password_lockout.go rename to internal/command/org_policy_lockout.go index 395b94cc08..073f53f827 100644 --- a/internal/command/org_policy_password_lockout.go +++ b/internal/command/org_policy_lockout.go @@ -11,8 +11,7 @@ func (c *Commands) AddLockoutPolicy(ctx context.Context, resourceOwner string, p if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-8fJif", "Errors.ResourceOwnerMissing") } - addedPolicy := NewOrgLockoutPolicyWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) + addedPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err } @@ -36,8 +35,7 @@ func (c *Commands) ChangeLockoutPolicy(ctx context.Context, resourceOwner string if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3J9fs", "Errors.ResourceOwnerMissing") } - existingPolicy := NewOrgLockoutPolicyWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err } @@ -66,8 +64,7 @@ func (c *Commands) RemoveLockoutPolicy(ctx context.Context, orgID string) error if orgID == "" { return caos_errs.ThrowInvalidArgument(nil, "Org-4J9fs", "Errors.ResourceOwnerMissing") } - existingPolicy := NewOrgLockoutPolicyWriteModel(orgID) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID) if err != nil { return err } @@ -79,3 +76,24 @@ func (c *Commands) RemoveLockoutPolicy(ctx context.Context, orgID string) error _, err = c.eventstore.PushEvents(ctx, org.NewLockoutPolicyRemovedEvent(ctx, orgAgg)) return err } + +func (c *Commands) removeLockoutPolicyIfExists(ctx context.Context, orgID string) (*org.LockoutPolicyRemovedEvent, error) { + existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingPolicy.State != domain.PolicyStateActive { + return nil, nil + } + orgAgg := OrgAggregateFromWriteModel(&existingPolicy.WriteModel) + return org.NewLockoutPolicyRemovedEvent(ctx, orgAgg), nil +} + +func (c *Commands) orgLockoutPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLockoutPolicyWriteModel, error) { + policy := NewOrgLockoutPolicyWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, policy) + if err != nil { + return nil, err + } + return policy, nil +} diff --git a/internal/command/org_policy_password_lockout_model.go b/internal/command/org_policy_lockout_model.go similarity index 100% rename from internal/command/org_policy_password_lockout_model.go rename to internal/command/org_policy_lockout_model.go diff --git a/internal/command/org_policy_password_lockout_test.go b/internal/command/org_policy_lockout_test.go similarity index 100% rename from internal/command/org_policy_password_lockout_test.go rename to internal/command/org_policy_lockout_test.go diff --git a/internal/domain/features.go b/internal/domain/features.go index 8ffddd04fb..a87e1db3d0 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -20,6 +20,7 @@ const ( FeatureLabelPolicyWatermark = FeatureLabelPolicy + ".watermark" FeatureCustomDomain = "custom_domain" FeaturePrivacyPolicy = "privacy_policy" + FeatureLockoutPolicy = "lockout_policy" FeatureMetadata = "metadata" FeatureCustomText = "custom_text" FeatureCustomTextMessage = FeatureCustomText + ".message" @@ -51,6 +52,7 @@ type Features struct { CustomTextLogin bool PrivacyPolicy bool MetadataUser bool + LockoutPolicy bool } type FeaturesState int32 diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index 4a435cdcfe..e5f83203fc 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -32,6 +32,7 @@ type FeaturesView struct { MetadataUser bool CustomTextMessage bool CustomTextLogin bool + LockoutPolicy bool } func (f *FeaturesView) FeatureList() []string { @@ -78,6 +79,9 @@ func (f *FeaturesView) FeatureList() []string { if f.CustomTextLogin { list = append(list, domain.FeatureCustomTextLogin) } + if f.LockoutPolicy { + list = append(list, domain.FeatureLockoutPolicy) + } return list } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index b3efde1ff6..108c2462a6 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -46,6 +46,7 @@ type FeaturesView struct { MetadataUser bool `json:"metadataUser" gorm:"column:metadata_user"` CustomTextMessage bool `json:"customTextMessage" gorm:"column:custom_text_message"` CustomTextLogin bool `json:"customTextLogin" gorm:"column:custom_text_login"` + LockoutPolicy bool `json:"lockoutPolicy" gorm:"column:lockout_policy"` } func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { @@ -74,6 +75,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { MetadataUser: features.MetadataUser, CustomTextMessage: features.CustomTextMessage, CustomTextLogin: features.CustomTextLogin, + LockoutPolicy: features.LockoutPolicy, } } diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index 1355b8e31e..4fc64bc94f 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -39,6 +39,7 @@ type FeaturesSetEvent struct { MetadataUser *bool `json:"metadataUser,omitempty"` CustomTextMessage *bool `json:"customTextMessage,omitempty"` CustomTextLogin *bool `json:"customTextLogin,omitempty"` + LockoutPolicy *bool `json:"lockoutPolicy,omitempty"` } func (e *FeaturesSetEvent) Data() interface{} { @@ -181,6 +182,12 @@ func ChangeCustomTextLogin(customTextLogin bool) func(event *FeaturesSetEvent) { } } +func ChangeLockoutPolicy(lockoutPolicy bool) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.LockoutPolicy = &lockoutPolicy + } +} + func FeaturesSetEventMapper(event *repository.Event) (eventstore.EventReader, error) { e := &FeaturesSetEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/migrations/cockroach/V1.69__lockout_policy_feature.sql b/migrations/cockroach/V1.69__lockout_policy_feature.sql new file mode 100644 index 0000000000..7ab1f850d7 --- /dev/null +++ b/migrations/cockroach/V1.69__lockout_policy_feature.sql @@ -0,0 +1,4 @@ +ALTER TABLE adminapi.features ADD COLUMN lockout_policy BOOLEAN; +ALTER TABLE auth.features ADD COLUMN lockout_policy BOOLEAN; +ALTER TABLE authz.features ADD COLUMN lockout_policy BOOLEAN; +ALTER TABLE management.features ADD COLUMN lockout_policy BOOLEAN; \ No newline at end of file diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 5800b523b1..6407ef0078 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2615,6 +2615,7 @@ message SetDefaultFeaturesRequest { bool metadata_user = 19; bool custom_text_message = 20; bool custom_text_login = 21; + bool lockout_policy = 22; } message SetDefaultFeaturesResponse { @@ -2653,6 +2654,7 @@ message SetOrgFeaturesRequest { bool metadata_user = 20; bool custom_text_message = 21; bool custom_text_login = 22; + bool lockout_policy = 23; } message SetOrgFeaturesResponse { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index 92cdc44695..02ba262ce7 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -29,6 +29,7 @@ message Features { bool metadata_user = 18; bool custom_text_message = 19; bool custom_text_login = 20; + bool lockout_policy = 21; } message FeatureTier {