diff --git a/console/src/app/modules/features/features.component.html b/console/src/app/modules/features/features.component.html index 8aaa7a3c92..dfa764d507 100644 --- a/console/src/app/modules/features/features.component.html +++ b/console/src/app/modules/features/features.component.html @@ -66,6 +66,15 @@ +
+ {{'FEATURES.DATA.LOGINPOLICYPASSWORDRESET' | translate}} + + + +
+
{{'FEATURES.DATA.LOGINPOLICYREGISTRATION' | translate}} @@ -132,4 +141,4 @@ type="submit" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}
- \ No newline at end of file + diff --git a/console/src/app/modules/features/features.component.ts b/console/src/app/modules/features/features.component.ts index 1df129fe20..5429adbbea 100644 --- a/console/src/app/modules/features/features.component.ts +++ b/console/src/app/modules/features/features.component.ts @@ -152,6 +152,7 @@ export class FeaturesComponent implements OnDestroy { req.setOrgId(this.org.id); req.setLoginPolicyUsernameLogin(this.features.loginPolicyUsernameLogin); + req.setLoginPolicyPasswordReset(this.features.loginPolicyPasswordReset); req.setLoginPolicyRegistration(this.features.loginPolicyRegistration); req.setLoginPolicyIdp(this.features.loginPolicyIdp); req.setLoginPolicyFactors(this.features.loginPolicyFactors); @@ -170,6 +171,7 @@ export class FeaturesComponent implements OnDestroy { // update Default org iam policy? const dreq = new SetDefaultFeaturesRequest(); dreq.setLoginPolicyUsernameLogin(this.features.loginPolicyUsernameLogin); + dreq.setLoginPolicyPasswordReset(this.features.loginPolicyPasswordReset); dreq.setLoginPolicyRegistration(this.features.loginPolicyRegistration); dreq.setLoginPolicyIdp(this.features.loginPolicyIdp); dreq.setLoginPolicyFactors(this.features.loginPolicyFactors); diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index bf17798f60..39ef180416 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -95,6 +95,26 @@ +
+ + {{'POLICY.DATA.HIDEPASSWORDRESET' | translate}} + + + + {{'FEATURES.NOTAVAILABLE' | translate: ({value: + 'login_policy.hide_password_reset'})}} + + + + + + {{'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate}} + + +
+
@@ -201,4 +221,4 @@ - \ No newline at end of file + diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index 9ce4e8697d..cc95cc6979 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -143,6 +143,7 @@ export class LoginPolicyComponent implements OnDestroy { mgmtreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword); mgmtreq.setForceMfa(this.loginData.forceMfa); mgmtreq.setPasswordlessType(this.loginData.passwordlessType); + mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); if ((this.loginData as LoginPolicy.AsObject).isDefault) { return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq); } else { @@ -155,6 +156,7 @@ export class LoginPolicyComponent implements OnDestroy { adminreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword); adminreq.setForceMfa(this.loginData.forceMfa); adminreq.setPasswordlessType(this.loginData.passwordlessType); + adminreq.setHidePasswordReset(this.loginData.hidePasswordReset); return (this.service as AdminService).updateLoginPolicy(adminreq); } diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index b81156cd52..0f66f1b3e9 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -607,6 +607,7 @@ "DATA": { "AUDITLOGRETENTION": "Audit Log Retention", "LOGINPOLICYUSERNAMELOGIN": "Login Richtlinie: Login mit Username erlauben - benutzerdefiniert", + "LOGINPOLICYPASSWORDRESET": "Login Richtlinie: Passwort vergessen Link nicht anzeigen - benutzerdefiniert", "LOGINPOLICYREGISTRATION": "Login Richtlinie: Registration erlauben - benutzerdefiniert", "LOGINPOLICYIDP": "Login Richtlinie: Identity Providers - benutzerdefiniert", "LOGINPOLICYFACTORS": "Login Richtlinie: Mltifaktoren - benutzerdefiniert", @@ -683,7 +684,9 @@ "ALLOWEXTERNALIDP_DESC": "Der Login wird für die darunter liegenden Identity Provider erlaubt.", "ALLOWREGISTER_DESC": "Ist die Option gewählt, erscheint im Login ein zusätzlicher Schritt zum Registrieren eines Benutzers.", "FORCEMFA": "Mfa erzwingen", - "FORCEMFA_DESC": "Ist die Option gewählt, müssen Benutzer einen zweiten Faktor für den Login verwenden." + "FORCEMFA_DESC": "Ist die Option gewählt, müssen Benutzer einen zweiten Faktor für den Login verwenden.", + "HIDEPASSWORDRESET": "Passwort vergessen, nicht anzeigen", + "FORCEMFA_DESC": "Ist die Option gewählt, ist es nicht möglich im Login das Passwort zurück zusetzen via Passwort vergessen Link." }, "RESET": "Richtlinie zurücksetzen", "CREATECUSTOM": "Benutzerdefinierte Richtlinie erstellen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index cba5af891b..84bf81802d 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -607,6 +607,7 @@ "DATA": { "AUDITLOGRETENTION": "Audit Log Retention", "LOGINPOLICYUSERNAMELOGIN": "Login Policy: Allow login with Username - custom", + "LOGINPOLICYPASSWORDRESET": "Login Policy: Hide reset password link - custom", "LOGINPOLICYREGISTRATION": "Login Policy: Allow self registration - custom", "LOGINPOLICYIDP": "Login Policy: Identity Provider - custom", "LOGINPOLICYFACTORS": "Login Policy: Multifactors - custom", @@ -683,7 +684,9 @@ "ALLOWEXTERNALIDP_DESC": "The login is allowed for the underlying identity providers", "ALLOWREGISTER_DESC": "If the option is selected, an additional step for registering a user appears in the login.", "FORCEMFA": "Force MFA", - "FORCEMFA_DESC": "If the option is selected, users have to configure a second factor for login." + "FORCEMFA_DESC": "If the option is selected, users have to configure a second factor for login.", + "HIDEPASSWORDRESET": "Hide Password reset", + "FORCEMFA_DESC": "If the option is selected, the user can't reset his password in the login process." }, "RESET": "Reset Policy", "CREATECUSTOM": "Create Custom Policy", diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 0d1c3cdaec..4258869e95 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -1421,6 +1421,7 @@ This is an empty response | password_complexity_policy | bool | - | | | label_policy | bool | - | | | custom_domain | bool | - | | +| login_policy_password_reset | bool | - | | @@ -1456,6 +1457,7 @@ This is an empty response | password_complexity_policy | bool | - | | | label_policy | bool | - | | | custom_domain | bool | - | | +| login_policy_password_reset | bool | - | | @@ -1696,6 +1698,7 @@ This is an empty response | allow_external_idp | bool | - | | | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| +| hide_password_reset | bool | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index 98e4bc6cfc..a8a0a374cc 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -1803,6 +1803,7 @@ Change OIDC identity provider configuration of the organisation | allow_external_idp | bool | - | | | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| +| hide_password_reset | bool | - | | @@ -4988,6 +4989,7 @@ This is an empty request | allow_external_idp | bool | - | | | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| +| hide_password_reset | bool | - | | diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index d286e9df85..25ae3a7769 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -37,6 +37,7 @@ title: zitadel/policy.proto | force_mfa | bool | - | | | passwordless_type | PasswordlessType | - | | | is_default | bool | - | | +| hide_password_reset | bool | - | | diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index eb0613fec6..bd7950235f 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -69,6 +69,7 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) LoginPolicyPasswordless: req.LoginPolicyPasswordless, LoginPolicyRegistration: req.LoginPolicyRegistration, LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin, + LoginPolicyPasswordReset: req.LoginPolicyPasswordReset, PasswordComplexityPolicy: req.PasswordComplexityPolicy, LabelPolicy: req.LabelPolicy, CustomDomain: req.CustomDomain, @@ -87,6 +88,7 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. LoginPolicyPasswordless: req.LoginPolicyPasswordless, LoginPolicyRegistration: req.LoginPolicyRegistration, LoginPolicyUsernameLogin: req.LoginPolicyUsernameLogin, + LoginPolicyPasswordReset: req.LoginPolicyPasswordReset, PasswordComplexityPolicy: req.PasswordComplexityPolicy, LabelPolicy: req.LabelPolicy, CustomDomain: req.CustomDomain, diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go index fa7737fdf3..b2af7a00e1 100644 --- a/internal/api/grpc/admin/login_policy_converter.go +++ b/internal/api/grpc/admin/login_policy_converter.go @@ -15,6 +15,7 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log AllowExternalIDP: p.AllowExternalIdp, ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), + HidePasswordReset: p.HidePasswordReset, } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index e4dc2dceba..17055cc662 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -21,6 +21,7 @@ func FeaturesFromModel(features *features_model.FeaturesView) *features_pb.Featu LoginPolicyPasswordless: features.LoginPolicyPasswordless, LoginPolicyRegistration: features.LoginPolicyRegistration, LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin, + LoginPolicyPasswordReset: features.LoginPolicyPasswordReset, PasswordComplexityPolicy: features.PasswordComplexityPolicy, LabelPolicy: features.LabelPolicy, CustomDomain: features.CustomDomain, diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go index ef74d57cb0..a85d15e8bb 100644 --- a/internal/api/grpc/management/policy_login_converter.go +++ b/internal/api/grpc/management/policy_login_converter.go @@ -15,6 +15,7 @@ func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi AllowExternalIDP: p.AllowExternalIdp, ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), + HidePasswordReset: p.HidePasswordReset, } } @@ -25,6 +26,7 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai AllowExternalIDP: p.AllowExternalIdp, ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), + HidePasswordReset: p.HidePasswordReset, } } diff --git a/internal/api/grpc/policy/login_policy.go b/internal/api/grpc/policy/login_policy.go index ca58a3df83..48c57ffe1c 100644 --- a/internal/api/grpc/policy/login_policy.go +++ b/internal/api/grpc/policy/login_policy.go @@ -14,6 +14,7 @@ func ModelLoginPolicyToPb(policy *model.LoginPolicyView) *policy_pb.LoginPolicy AllowExternalIdp: policy.AllowExternalIDP, ForceMfa: policy.ForceMFA, PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType), + HidePasswordReset: policy.HidePasswordReset, } } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index ba2b370c34..a937553a3f 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -163,6 +163,10 @@ func checkLoginPolicyFeatures(features *features_view_model.FeaturesView, requir if !features.LoginPolicyUsernameLogin { return MissingFeatureErr(requiredFeature) } + case domain.FeatureLoginPolicyPasswordReset: + if !features.LoginPolicyPasswordReset { + return MissingFeatureErr(requiredFeature) + } default: if !features.LoginPolicyFactors && !features.LoginPolicyIDP && !features.LoginPolicyPasswordless && !features.LoginPolicyRegistration && !features.LoginPolicyUsernameLogin { return MissingFeatureErr(requiredFeature) diff --git a/internal/command/features_model.go b/internal/command/features_model.go index 5483b67621..679c64db82 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -21,6 +21,7 @@ type FeaturesWriteModel struct { LoginPolicyPasswordless bool LoginPolicyRegistration bool LoginPolicyUsernameLogin bool + LoginPolicyPasswordReset bool PasswordComplexityPolicy bool LabelPolicy bool CustomDomain bool @@ -61,6 +62,9 @@ func (wm *FeaturesWriteModel) Reduce() error { if e.LoginPolicyUsernameLogin != nil { wm.LoginPolicyUsernameLogin = *e.LoginPolicyUsernameLogin } + if e.LoginPolicyPasswordReset != nil { + wm.LoginPolicyPasswordReset = *e.LoginPolicyPasswordReset + } if e.PasswordComplexityPolicy != nil { wm.PasswordComplexityPolicy = *e.PasswordComplexityPolicy } diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index 020306069b..9fb5f82d1a 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -39,6 +39,7 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy { AllowUsernamePassword: wm.AllowUserNamePassword, AllowRegister: wm.AllowRegister, AllowExternalIDP: wm.AllowExternalIDP, + HidePasswordReset: wm.HidePasswordReset, ForceMFA: wm.ForceMFA, PasswordlessType: wm.PasswordlessType, } diff --git a/internal/command/iam_policy_login.go b/internal/command/iam_policy_login.go index 170793a00c..ccf1052823 100644 --- a/internal/command/iam_policy_login.go +++ b/internal/command/iam_policy_login.go @@ -48,7 +48,7 @@ func (c *Commands) addDefaultLoginPolicy(ctx context.Context, iamAgg *eventstore return nil, caos_errs.ThrowAlreadyExists(nil, "IAM-2B0ps", "Errors.IAM.LoginPolicy.AlreadyExists") } - return iam_repo.NewLoginPolicyAddedEvent(ctx, iamAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType), nil + return iam_repo.NewLoginPolicyAddedEvent(ctx, iamAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.HidePasswordReset, policy.PasswordlessType), nil } func (c *Commands) ChangeDefaultLoginPolicy(ctx context.Context, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { @@ -77,7 +77,7 @@ func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, iamAgg *eventst if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "IAM-M0sif", "Errors.IAM.LoginPolicy.NotFound") } - changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, iamAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType) + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, iamAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.HidePasswordReset, policy.PasswordlessType) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged") } diff --git a/internal/command/iam_policy_login_model.go b/internal/command/iam_policy_login_model.go index 621f0e5e05..003566ad36 100644 --- a/internal/command/iam_policy_login_model.go +++ b/internal/command/iam_policy_login_model.go @@ -58,7 +58,8 @@ func (wm *IAMLoginPolicyWriteModel) NewChangedEvent( allowUsernamePassword, allowRegister, allowExternalIDP, - forceMFA bool, + forceMFA, + hidePasswordReset bool, passwordlessType domain.PasswordlessType, ) (*iam.LoginPolicyChangedEvent, bool) { @@ -78,6 +79,9 @@ func (wm *IAMLoginPolicyWriteModel) NewChangedEvent( if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType { changes = append(changes, policy.ChangePasswordlessType(passwordlessType)) } + if wm.HidePasswordReset != hidePasswordReset { + changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/iam_policy_login_test.go b/internal/command/iam_policy_login_test.go index 6bc560c764..77119d7b67 100644 --- a/internal/command/iam_policy_login_test.go +++ b/internal/command/iam_policy_login_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "testing" + "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -12,7 +14,6 @@ import ( "github.com/caos/zitadel/internal/repository/user" "github.com/stretchr/testify/assert" - "testing" ) func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { @@ -46,6 +47,7 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { true, false, false, + false, domain.PasswordlessTypeAllowed, ), ), @@ -79,6 +81,7 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -93,6 +96,7 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -106,6 +110,7 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -180,6 +185,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -193,6 +199,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -213,6 +220,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -220,7 +228,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { expectPush( []*repository.Event{ eventFromEventPusher( - newDefaultLoginPolicyChangedEvent(context.Background(), false, false, false, false, domain.PasswordlessTypeNotAllowed), + newDefaultLoginPolicyChangedEvent(context.Background(), false, false, false, false, false, domain.PasswordlessTypeNotAllowed), ), }, ), @@ -233,6 +241,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { AllowUsernamePassword: false, AllowExternalIDP: false, ForceMFA: false, + HidePasswordReset: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, }, }, @@ -246,6 +255,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { AllowUsernamePassword: false, AllowExternalIDP: false, ForceMFA: false, + HidePasswordReset: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, }, }, @@ -345,6 +355,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -496,6 +507,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -537,6 +549,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -583,6 +596,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -637,6 +651,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -1181,7 +1196,7 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) { } } -func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA bool, passwordlessType domain.PasswordlessType) *iam.LoginPolicyChangedEvent { +func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset bool, passwordlessType domain.PasswordlessType) *iam.LoginPolicyChangedEvent { event, _ := iam.NewLoginPolicyChangedEvent(ctx, &iam.NewAggregate().Aggregate, []policy.LoginPolicyChanges{ @@ -1189,6 +1204,7 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow policy.ChangeAllowExternalIDP(allowExternalIDP), policy.ChangeForceMFA(forceMFA), policy.ChangeAllowUserNamePassword(allowUsernamePassword), + policy.ChangeHidePasswordReset(hidePasswordReset), policy.ChangePasswordlessType(passwordlessType), }, ) diff --git a/internal/command/org_features.go b/internal/command/org_features.go index b1c6cdb8af..918ddf13e7 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -31,6 +31,7 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea features.LoginPolicyPasswordless, features.LoginPolicyRegistration, features.LoginPolicyUsernameLogin, + features.LoginPolicyPasswordReset, features.PasswordComplexityPolicy, features.LabelPolicy, features.CustomDomain, @@ -165,7 +166,10 @@ func (c *Commands) setAllowedLoginPolicy(ctx context.Context, orgID string, feat 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 !features.LoginPolicyPasswordReset && defaultPolicy.HidePasswordReset != existingPolicy.HidePasswordReset { + policy.HidePasswordReset = defaultPolicy.HidePasswordReset + } + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, OrgAggregateFromWriteModel(&existingPolicy.WriteModel), policy.AllowUserNamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.HidePasswordReset, policy.PasswordlessType) if hasChanged { events = append(events, changedEvent) } diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index 014d36234f..ed9dbb57ca 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -67,6 +67,7 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( loginPolicyPasswordless, loginPolicyRegistration, loginPolicyUsernameLogin, + loginPolicyPasswordReset, passwordComplexityPolicy, labelPolicy, customDomain bool, @@ -104,6 +105,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( if wm.LoginPolicyUsernameLogin != loginPolicyUsernameLogin { changes = append(changes, features.ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin)) } + if wm.LoginPolicyPasswordReset != loginPolicyPasswordReset { + changes = append(changes, features.ChangeLoginPolicyPasswordReset(loginPolicyPasswordReset)) + } if wm.PasswordComplexityPolicy != passwordComplexityPolicy { changes = append(changes, features.ChangePasswordComplexityPolicy(passwordComplexityPolicy)) } diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index eab0cb72a8..028642448e 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -54,6 +54,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -87,6 +88,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -111,6 +113,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeAllowed, ), ), @@ -191,6 +194,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -217,6 +221,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeAllowed, ), ), @@ -325,6 +330,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -351,6 +357,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeAllowed, ), ), @@ -469,6 +476,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -495,6 +503,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeAllowed, ), ), @@ -623,6 +632,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { LoginPolicyPasswordless: false, LoginPolicyRegistration: false, LoginPolicyUsernameLogin: false, + LoginPolicyPasswordReset: false, PasswordComplexityPolicy: false, LabelPolicy: false, CustomDomain: false, @@ -653,6 +663,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -664,6 +675,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, ), ), @@ -678,6 +690,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -790,7 +803,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, domain.MultiFactorTypeU2FWithPIN), ), eventFromEventPusher( - newLoginPolicyChangedEvent(context.Background(), "org1", true, true, true, true, domain.PasswordlessTypeAllowed), + newLoginPolicyChangedEvent(context.Background(), "org1", true, true, true, true, true, domain.PasswordlessTypeAllowed), ), eventFromEventPusher( org.NewPasswordComplexityPolicyRemovedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate), @@ -920,6 +933,7 @@ func TestCommandSide_RemoveOrgFeatures(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeAllowed, ), ), diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index d65501b5a3..9460898592 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -42,6 +42,7 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, + policy.HidePasswordReset, policy.PasswordlessType)) if err != nil { return nil, err @@ -81,7 +82,16 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType) + changedEvent, hasChanged := existingPolicy.NewChangedEvent( + ctx, + orgAgg, + policy.AllowUsernamePassword, + policy.AllowRegister, + policy.AllowExternalIDP, + policy.ForceMFA, + policy.HidePasswordReset, + policy.PasswordlessType) + if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5M9vdd", "Errors.Org.LoginPolicy.NotChanged") } @@ -118,6 +128,9 @@ func (c *Commands) checkLoginPolicyAllowed(ctx context.Context, resourceOwner st if defaultPolicy.AllowUsernamePassword != policy.AllowUsernamePassword { requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyUsernameLogin) } + if defaultPolicy.HidePasswordReset != policy.HidePasswordReset { + requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyPasswordReset) + } return authz.CheckOrgFeatures(ctx, c.tokenVerifier, resourceOwner, requiredFeatures...) } diff --git a/internal/command/org_policy_login_model.go b/internal/command/org_policy_login_model.go index e23c1e478b..cc87572004 100644 --- a/internal/command/org_policy_login_model.go +++ b/internal/command/org_policy_login_model.go @@ -61,7 +61,8 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( allowUsernamePassword, allowRegister, allowExternalIDP, - forceMFA bool, + forceMFA, + hidePasswordReset bool, passwordlessType domain.PasswordlessType, ) (*org.LoginPolicyChangedEvent, bool) { @@ -78,6 +79,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( if wm.ForceMFA != forceMFA { changes = append(changes, policy.ChangeForceMFA(forceMFA)) } + if wm.HidePasswordReset != hidePasswordReset { + changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset)) + } if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType { changes = append(changes, policy.ChangePasswordlessType(passwordlessType)) } diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go index e78501ee9a..ddd73b38af 100644 --- a/internal/command/org_policy_login_test.go +++ b/internal/command/org_policy_login_test.go @@ -70,6 +70,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -105,6 +106,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -141,6 +143,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -154,6 +157,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -170,6 +174,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -183,6 +188,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -285,6 +291,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -297,6 +304,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -332,6 +340,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -344,6 +353,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -359,6 +369,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, + HidePasswordReset: true, PasswordlessType: domain.PasswordlessTypeAllowed, }, }, @@ -379,6 +390,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -391,6 +403,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, ), ), @@ -398,7 +411,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { expectPush( []*repository.Event{ eventFromEventPusher( - newLoginPolicyChangedEvent(context.Background(), "org1", false, false, false, false, domain.PasswordlessTypeNotAllowed), + newLoginPolicyChangedEvent(context.Background(), "org1", false, false, false, false, false, domain.PasswordlessTypeNotAllowed), ), }, ), @@ -426,6 +439,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { AllowUsernamePassword: false, AllowExternalIDP: false, ForceMFA: false, + HidePasswordReset: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, }, }, @@ -512,6 +526,7 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -655,6 +670,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -839,6 +855,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -882,6 +899,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -932,6 +950,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -990,6 +1009,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, ), ), @@ -1600,7 +1620,7 @@ func TestCommandSide_RemoveMultiFactorLoginPolicy(t *testing.T) { } } -func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa bool, passwordlessType domain.PasswordlessType) *org.LoginPolicyChangedEvent { +func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset bool, passwordlessType domain.PasswordlessType) *org.LoginPolicyChangedEvent { event, _ := org.NewLoginPolicyChangedEvent(ctx, &org.NewAggregate(orgID, orgID).Aggregate, []policy.LoginPolicyChanges{ @@ -1608,6 +1628,7 @@ func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassw policy.ChangeAllowRegister(register), policy.ChangeAllowExternalIDP(externalIDP), policy.ChangeForceMFA(mfa), + policy.ChangeHidePasswordReset(passwordReset), policy.ChangePasswordlessType(passwordlessType), }, ) diff --git a/internal/command/policy_login_model.go b/internal/command/policy_login_model.go index affb9fac6f..3b3a8bfcfe 100644 --- a/internal/command/policy_login_model.go +++ b/internal/command/policy_login_model.go @@ -13,6 +13,7 @@ type LoginPolicyWriteModel struct { AllowRegister bool AllowExternalIDP bool ForceMFA bool + HidePasswordReset bool PasswordlessType domain.PasswordlessType State domain.PolicyState } @@ -26,6 +27,7 @@ func (wm *LoginPolicyWriteModel) Reduce() error { wm.AllowExternalIDP = e.AllowExternalIDP wm.ForceMFA = e.ForceMFA wm.PasswordlessType = e.PasswordlessType + wm.HidePasswordReset = e.HidePasswordReset wm.State = domain.PolicyStateActive case *policy.LoginPolicyChangedEvent: if e.AllowRegister != nil { @@ -40,6 +42,9 @@ func (wm *LoginPolicyWriteModel) Reduce() error { if e.ForceMFA != nil { wm.ForceMFA = *e.ForceMFA } + if e.HidePasswordReset != nil { + wm.HidePasswordReset = *e.HidePasswordReset + } if e.PasswordlessType != nil { wm.PasswordlessType = *e.PasswordlessType } diff --git a/internal/domain/features.go b/internal/domain/features.go index 5357b12172..17a29369ef 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -13,6 +13,7 @@ const ( FeatureLoginPolicyPasswordless = FeatureLoginPolicy + ".passwordless" FeatureLoginPolicyRegistration = FeatureLoginPolicy + ".registration" FeatureLoginPolicyUsernameLogin = FeatureLoginPolicy + ".username_login" + FeatureLoginPolicyPasswordReset = FeatureLoginPolicy + ".password_reset" FeaturePasswordComplexityPolicy = "password_complexity_policy" FeatureLabelPolicy = "label_policy" FeatureCustomDomain = "custom_domain" @@ -33,6 +34,7 @@ type Features struct { LoginPolicyPasswordless bool LoginPolicyRegistration bool LoginPolicyUsernameLogin bool + LoginPolicyPasswordReset bool PasswordComplexityPolicy bool LabelPolicy bool CustomDomain bool diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index dd6c57a0e5..0ad43afb0d 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -14,6 +14,7 @@ type LoginPolicy struct { SecondFactors []SecondFactorType MultiFactors []MultiFactorType PasswordlessType PasswordlessType + HidePasswordReset bool } type IDPProvider struct { diff --git a/internal/features/model/features_view.go b/internal/features/model/features_view.go index 704eb8f405..0fb0473dc6 100644 --- a/internal/features/model/features_view.go +++ b/internal/features/model/features_view.go @@ -23,6 +23,7 @@ type FeaturesView struct { LoginPolicyPasswordless bool LoginPolicyRegistration bool LoginPolicyUsernameLogin bool + LoginPolicyPasswordReset bool PasswordComplexityPolicy bool LabelPolicy bool CustomDomain bool @@ -45,6 +46,9 @@ func (f *FeaturesView) FeatureList() []string { if f.LoginPolicyUsernameLogin { list = append(list, domain.FeatureLoginPolicyUsernameLogin) } + if f.LoginPolicyPasswordReset { + list = append(list, domain.FeatureLoginPolicyPasswordReset) + } if f.PasswordComplexityPolicy { list = append(list, domain.FeaturePasswordComplexityPolicy) } diff --git a/internal/features/repository/view/model/features.go b/internal/features/repository/view/model/features.go index 44e67ec329..bc71f7034f 100644 --- a/internal/features/repository/view/model/features.go +++ b/internal/features/repository/view/model/features.go @@ -36,6 +36,7 @@ type FeaturesView struct { LoginPolicyPasswordless bool `json:"loginPolicyPasswordless" gorm:"column:login_policy_passwordless"` LoginPolicyRegistration bool `json:"loginPolicyRegistration" gorm:"column:login_policy_registration"` LoginPolicyUsernameLogin bool `json:"loginPolicyUsernameLogin" gorm:"column:login_policy_username_login"` + LoginPolicyPasswordReset bool `json:"loginPolicyPasswordReset" gorm:"column:login_policy_password_reset"` PasswordComplexityPolicy bool `json:"passwordComplexityPolicy" gorm:"column:password_complexity_policy"` LabelPolicy bool `json:"labelPolicy" gorm:"column:label_policy"` CustomDomain bool `json:"customDomain" gorm:"column:custom_domain"` @@ -58,6 +59,7 @@ func FeaturesToModel(features *FeaturesView) *features_model.FeaturesView { LoginPolicyPasswordless: features.LoginPolicyPasswordless, LoginPolicyRegistration: features.LoginPolicyRegistration, LoginPolicyUsernameLogin: features.LoginPolicyUsernameLogin, + LoginPolicyPasswordReset: features.LoginPolicyPasswordReset, PasswordComplexityPolicy: features.PasswordComplexityPolicy, LabelPolicy: features.LabelPolicy, CustomDomain: features.CustomDomain, diff --git a/internal/iam/model/login_policy_view.go b/internal/iam/model/login_policy_view.go index b9e8f745e6..2c1a998ea0 100644 --- a/internal/iam/model/login_policy_view.go +++ b/internal/iam/model/login_policy_view.go @@ -1,9 +1,10 @@ package model import ( + "time" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1/models" - "time" ) type LoginPolicyView struct { @@ -12,6 +13,7 @@ type LoginPolicyView struct { AllowRegister bool AllowExternalIDP bool ForceMFA bool + HidePasswordReset bool PasswordlessType PasswordlessType SecondFactors []SecondFactorType MultiFactors []MultiFactorType @@ -80,6 +82,7 @@ func (p *LoginPolicyView) ToLoginPolicyDomain() *domain.LoginPolicy { AllowRegister: p.AllowRegister, AllowExternalIDP: p.AllowExternalIDP, ForceMFA: p.ForceMFA, + HidePasswordReset: p.HidePasswordReset, PasswordlessType: passwordLessTypeToDomain(p.PasswordlessType), SecondFactors: secondFactorsToDomain(p.SecondFactors), MultiFactors: multiFactorsToDomain(p.MultiFactors), diff --git a/internal/iam/repository/view/model/login_policy.go b/internal/iam/repository/view/model/login_policy.go index f180d14b11..3fdff7904d 100644 --- a/internal/iam/repository/view/model/login_policy.go +++ b/internal/iam/repository/view/model/login_policy.go @@ -2,13 +2,16 @@ package model import ( "encoding/json" - org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" - "github.com/lib/pq" "time" + "github.com/lib/pq" + + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/iam/model" @@ -29,6 +32,7 @@ type LoginPolicyView struct { AllowUsernamePassword bool `json:"allowUsernamePassword" gorm:"column:allow_username_password"` AllowExternalIDP bool `json:"allowExternalIdp" gorm:"column:allow_external_idp"` ForceMFA bool `json:"forceMFA" gorm:"column:force_mfa"` + HidePasswordReset bool `json:"hidePasswordReset" gorm:"column:hide_password_reset"` PasswordlessType int32 `json:"passwordlessType" gorm:"column:passwordless_type"` SecondFactors pq.Int64Array `json:"-" gorm:"column:second_factors"` MultiFactors pq.Int64Array `json:"-" gorm:"column:multi_factors"` @@ -47,6 +51,7 @@ func LoginPolicyViewFromModel(policy *model.LoginPolicyView) *LoginPolicyView { AllowExternalIDP: policy.AllowExternalIDP, AllowUsernamePassword: policy.AllowUsernamePassword, ForceMFA: policy.ForceMFA, + HidePasswordReset: policy.HidePasswordReset, PasswordlessType: int32(policy.PasswordlessType), SecondFactors: secondFactorsFromModel(policy.SecondFactors), MultiFactors: multiFactorsFromModel(policy.MultiFactors), @@ -80,6 +85,7 @@ func LoginPolicyViewToModel(policy *LoginPolicyView) *model.LoginPolicyView { AllowExternalIDP: policy.AllowExternalIDP, AllowUsernamePassword: policy.AllowUsernamePassword, ForceMFA: policy.ForceMFA, + HidePasswordReset: policy.HidePasswordReset, PasswordlessType: model.PasswordlessType(policy.PasswordlessType), SecondFactors: secondFactorsToModel(policy.SecondFactors), MultiFactors: multiFactorsToToModel(policy.MultiFactors), diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index d9d935b041..65a3ddfaa6 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -29,6 +29,7 @@ type FeaturesSetEvent struct { LoginPolicyPasswordless *bool `json:"loginPolicyPasswordless,omitempty"` LoginPolicyRegistration *bool `json:"loginPolicyRegistration,omitempty"` LoginPolicyUsernameLogin *bool `json:"loginPolicyUsernameLogin,omitempty"` + LoginPolicyPasswordReset *bool `json:"loginPolicyPasswordReset,omitempty"` PasswordComplexityPolicy *bool `json:"passwordComplexityPolicy,omitempty"` LabelPolicy *bool `json:"labelPolicy,omitempty"` CustomDomain *bool `json:"customDomain,omitempty"` @@ -120,6 +121,12 @@ func ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin bool) func(event *F } } +func ChangeLoginPolicyPasswordReset(loginPolicyPasswordReset bool) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.LoginPolicyPasswordReset = &loginPolicyPasswordReset + } +} + func ChangePasswordComplexityPolicy(passwordComplexityPolicy bool) func(event *FeaturesSetEvent) { return func(e *FeaturesSetEvent) { e.PasswordComplexityPolicy = &passwordComplexityPolicy diff --git a/internal/repository/iam/policy_login.go b/internal/repository/iam/policy_login.go index 163098c49e..1662468b8a 100644 --- a/internal/repository/iam/policy_login.go +++ b/internal/repository/iam/policy_login.go @@ -24,7 +24,8 @@ func NewLoginPolicyAddedEvent( allowUsernamePassword, allowRegister, allowExternalIDP, - forceMFA bool, + forceMFA, + hidePasswordReset bool, passwordlessType domain.PasswordlessType, ) *LoginPolicyAddedEvent { return &LoginPolicyAddedEvent{ @@ -37,6 +38,7 @@ func NewLoginPolicyAddedEvent( allowRegister, allowExternalIDP, forceMFA, + hidePasswordReset, passwordlessType), } } diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go index d2d64feaef..f278be6a1c 100644 --- a/internal/repository/org/policy_login.go +++ b/internal/repository/org/policy_login.go @@ -25,7 +25,8 @@ func NewLoginPolicyAddedEvent( allowUsernamePassword, allowRegister, allowExternalIDP, - forceMFA bool, + forceMFA, + hidePasswordReset bool, passwordlessType domain.PasswordlessType, ) *LoginPolicyAddedEvent { return &LoginPolicyAddedEvent{ @@ -38,6 +39,7 @@ func NewLoginPolicyAddedEvent( allowRegister, allowExternalIDP, forceMFA, + hidePasswordReset, passwordlessType), } } diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go index dbcfbfe9a7..8c7037bb2c 100644 --- a/internal/repository/policy/login.go +++ b/internal/repository/policy/login.go @@ -2,6 +2,7 @@ package policy import ( "encoding/json" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -22,6 +23,7 @@ type LoginPolicyAddedEvent struct { AllowRegister bool `json:"allowRegister,omitempty"` AllowExternalIDP bool `json:"allowExternalIdp,omitempty"` ForceMFA bool `json:"forceMFA,omitempty"` + HidePasswordReset bool `json:"hidePasswordReset,omitempty"` PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"` } @@ -38,7 +40,8 @@ func NewLoginPolicyAddedEvent( allowUserNamePassword, allowRegister, allowExternalIDP, - forceMFA bool, + forceMFA, + hidePasswordReset bool, passwordlessType domain.PasswordlessType, ) *LoginPolicyAddedEvent { return &LoginPolicyAddedEvent{ @@ -48,6 +51,7 @@ func NewLoginPolicyAddedEvent( AllowUserNamePassword: allowUserNamePassword, ForceMFA: forceMFA, PasswordlessType: passwordlessType, + HidePasswordReset: hidePasswordReset, } } @@ -71,6 +75,7 @@ type LoginPolicyChangedEvent struct { AllowRegister *bool `json:"allowRegister,omitempty"` AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"` ForceMFA *bool `json:"forceMFA,omitempty"` + HidePasswordReset *bool `json:"hidePasswordReset,omitempty"` PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"` } @@ -133,6 +138,12 @@ func ChangePasswordlessType(passwordlessType domain.PasswordlessType) func(*Logi } } +func ChangeHidePasswordReset(hidePasswordReset bool) func(*LoginPolicyChangedEvent) { + return func(e *LoginPolicyChangedEvent) { + e.HidePasswordReset = &hidePasswordReset + } +} + func LoginPolicyChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { e := &LoginPolicyChangedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/ui/login/handler/password_handler.go b/internal/ui/login/handler/password_handler.go index 91a07ffd52..af84565116 100644 --- a/internal/ui/login/handler/password_handler.go +++ b/internal/ui/login/handler/password_handler.go @@ -21,7 +21,15 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq * errMessage = l.getErrorMessage(r, err) } data := l.getUserData(r, authReq, "Password", errType, errMessage) - l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplPassword], data, nil) + funcs := map[string]interface{}{ + "showPasswordReset": func() bool { + if authReq.LoginPolicy != nil { + return !authReq.LoginPolicy.HidePasswordReset + } + return true + }, + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplPassword], data, funcs) } func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index 7e6f2b6f61..322587d5d2 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -153,6 +153,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str "hasUsernamePasswordLogin": func() bool { return false }, + "showPasswordReset": func() bool { + return true + }, "hasExternalLogin": func() bool { return false }, diff --git a/internal/ui/login/static/templates/password.html b/internal/ui/login/static/templates/password.html index 17b4159b4c..c2b815f613 100644 --- a/internal/ui/login/static/templates/password.html +++ b/internal/ui/login/static/templates/password.html @@ -21,9 +21,11 @@ {{template "error-message" .}} + {{ if showPasswordReset }} {{t "Actions.ForgotPassword"}} + {{ end }}
diff --git a/migrations/cockroach/V1.46__password_reset.sql b/migrations/cockroach/V1.46__password_reset.sql new file mode 100644 index 0000000000..48c9746466 --- /dev/null +++ b/migrations/cockroach/V1.46__password_reset.sql @@ -0,0 +1,9 @@ +ALTER TABLE adminapi.features ADD COLUMN login_policy_password_reset BOOLEAN; +ALTER TABLE auth.features ADD COLUMN login_policy_password_reset BOOLEAN; +ALTER TABLE authz.features ADD COLUMN login_policy_password_reset BOOLEAN; +ALTER TABLE management.features ADD COLUMN login_policy_password_reset BOOLEAN; + + +ALTER TABLE auth.login_policies ADD COLUMN hide_password_reset BOOLEAN; +ALTER TABLE adminapi.login_policies ADD COLUMN hide_password_reset BOOLEAN; +ALTER TABLE management.login_policies ADD COLUMN hide_password_reset BOOLEAN; diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index b942d3af5c..cfc2801802 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2194,6 +2194,7 @@ message SetDefaultFeaturesRequest { bool password_complexity_policy = 11; bool label_policy = 12; bool custom_domain = 13; + bool login_policy_password_reset = 14; } message SetDefaultFeaturesResponse { @@ -2224,6 +2225,7 @@ message SetOrgFeaturesRequest { bool password_complexity_policy = 12; bool label_policy = 13; bool custom_domain = 14; + bool login_policy_password_reset = 15; } message SetOrgFeaturesResponse { @@ -2421,6 +2423,11 @@ message UpdateLoginPolicyRequest { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "defines if passwordless is allowed for users" }]; + bool hide_password_reset = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if password reset link should be shown in the login screen" + } + ]; } message UpdateLoginPolicyResponse { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index fa23d94f17..6ed939cc39 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -21,6 +21,7 @@ message Features { bool password_complexity_policy = 10; bool label_policy = 11; bool custom_domain = 12; + bool login_policy_password_reset = 13; } message FeatureTier { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index f8d241298a..906bdeb292 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3373,6 +3373,7 @@ message AddCustomLoginPolicyRequest { bool allow_external_idp = 3; bool force_mfa = 4; zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}]; + bool hide_password_reset = 6; } message AddCustomLoginPolicyResponse { @@ -3385,6 +3386,7 @@ message UpdateCustomLoginPolicyRequest { bool allow_external_idp = 3; bool force_mfa = 4; zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}]; + bool hide_password_reset = 6; } message UpdateCustomLoginPolicyResponse { diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index ae98bb1b14..611f3acf86 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -77,6 +77,11 @@ message LoginPolicy { description: "defines if the organisation's admin changed the policy" } ]; + bool hide_password_reset = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if password reset link should be shown in the login screen" + } + ]; } enum SecondFactorType {