diff --git a/src/System Application/App/Password/src/ChangePassword.Report.al b/src/System Application/App/Password/src/ChangePassword.Report.al index 2ca90d7cfa..07bf125c4d 100644 --- a/src/System Application/App/Password/src/ChangePassword.Report.al +++ b/src/System Application/App/Password/src/ChangePassword.Report.al @@ -27,11 +27,11 @@ report 9810 "Change Password" trigger OnInitReport() var User: Record User; - PasswordDialogManagement: Codeunit "Password Dialog Management"; + PasswordDialogImpl: Codeunit "Password Dialog Impl."; Password: SecretText; OldPassword: SecretText; begin - PasswordDialogManagement.OpenChangePasswordDialog(OldPassword, Password); + PasswordDialogImpl.OpenChangePasswordDialog(OldPassword, Password); if Password.IsEmpty() then exit; diff --git a/src/System Application/App/Password/src/PasswordDialog.Page.al b/src/System Application/App/Password/src/PasswordDialog.Page.al index 6a6a659802..02b91c2664 100644 --- a/src/System Application/App/Password/src/PasswordDialog.Page.al +++ b/src/System Application/App/Password/src/PasswordDialog.Page.al @@ -27,6 +27,10 @@ page 9810 "Password Dialog" ExtendedDatatype = Masked; ToolTip = 'Specifies the current password, before the user defines a new one.'; Visible = ShowOldPassword; + trigger OnValidate() + begin + PasswordDialogImpl.ValidateOldPasswordMatch(CurrentPasswordToCompare, OldPasswordValue); + end; } field(Password; PasswordValue) { @@ -39,6 +43,8 @@ page 9810 "Password Dialog" begin if RequiresPasswordValidation then PasswordDialogImpl.ValidatePasswordStrength(PasswordValue); + + PasswordDialogImpl.ValidateNewPasswordUniqueness(CurrentPasswordToCompare, PasswordValue); end; } field(ConfirmPassword; ConfirmPasswordValue) @@ -91,6 +97,7 @@ page 9810 "Password Dialog" ConfirmPasswordValue: Text; [NonDebuggable] OldPasswordValue: Text; + CurrentPasswordToCompare: SecretText; ShowOldPassword: Boolean; ValidPassword: Boolean; RequiresPasswordValidation: Boolean; @@ -150,6 +157,16 @@ page 9810 "Password Dialog" Password := OldPasswordValue; end; + /// + /// Set the old password value to compare with typed on the page. + /// + /// Old password to compare. + [Scope('OnPrem')] + procedure SetCurrentPasswordToCompareSecretValue(CurrentPasswordSecret: SecretText) + begin + CurrentPasswordToCompare := CurrentPasswordSecret; + end; + /// /// Enables the Change password mode, it makes the old password field on the page visible. /// diff --git a/src/System Application/App/Password/src/PasswordDialogImpl.Codeunit.al b/src/System Application/App/Password/src/PasswordDialogImpl.Codeunit.al index 3b700c7e76..afecdde6c4 100644 --- a/src/System Application/App/Password/src/PasswordDialogImpl.Codeunit.al +++ b/src/System Application/App/Password/src/PasswordDialogImpl.Codeunit.al @@ -15,6 +15,8 @@ codeunit 9811 "Password Dialog Impl." PasswordMismatchErr: Label 'The passwords that you entered do not match.'; PasswordTooSimpleErr: Label 'The password that you entered does not meet the minimum requirements. It must be at least %1 characters long and contain at least one uppercase letter, one lowercase letter, one number and one special character. It must not have a sequence of 3 or more ascending, descending or repeating characters.', Comment = '%1: The minimum number of characters required in the password'; ConfirmBlankPasswordQst: Label 'Do you want to exit without entering a password?'; + PasswordSameAsNewErr: Label 'The new password cannot be the same as the current password.'; + CurrentPasswordMismatchErr: Label 'The current password does not match the entered password.'; procedure ValidatePasswordStrength(Password: SecretText) var @@ -79,6 +81,16 @@ codeunit 9811 "Password Dialog Impl." end; end; + procedure OpenPasswordChangeDialog(CurrentPassword: SecretText; var NewPassword: SecretText) + var + PasswordDialog: Page "Password Dialog"; + begin + PasswordDialog.EnableChangePassword(); + PasswordDialog.SetCurrentPasswordToCompareSecretValue(CurrentPassword); + if PasswordDialog.RunModal() = Action::OK then + NewPassword := PasswordDialog.GetPasswordSecretValue(); + end; + [NonDebuggable] procedure ValidatePassword(RequiresPasswordConfirmation: Boolean; RequiresPasswordValidation: Boolean; Password: SecretText; ConfirmPassword: SecretText): Boolean begin @@ -92,5 +104,23 @@ codeunit 9811 "Password Dialog Impl." exit(false); exit(true); end; + + [NonDebuggable] + procedure ValidateOldPasswordMatch(CurrentPasswordToCompare: SecretText; OldPasswordEntered: SecretText) + begin + if CurrentPasswordToCompare.IsEmpty() then + exit; + if CurrentPasswordToCompare.Unwrap() <> OldPasswordEntered.Unwrap() then + Error(CurrentPasswordMismatchErr); + end; + + [NonDebuggable] + procedure ValidateNewPasswordUniqueness(CurrentPasswordToCompare: SecretText; NewPassword: SecretText) + begin + if CurrentPasswordToCompare.IsEmpty() then + exit; + if CurrentPasswordToCompare.Unwrap() = NewPassword.Unwrap() then + Error(PasswordSameAsNewErr); + end; } diff --git a/src/System Application/App/Password/src/PasswordDialogManagement.Codeunit.al b/src/System Application/App/Password/src/PasswordDialogManagement.Codeunit.al index a660d45df9..54dd0e9d26 100644 --- a/src/System Application/App/Password/src/PasswordDialogManagement.Codeunit.al +++ b/src/System Application/App/Password/src/PasswordDialogManagement.Codeunit.al @@ -77,6 +77,18 @@ codeunit 9810 "Password Dialog Management" PasswordDialogImpl.OpenChangePasswordDialog(OldPassword, Password); #pragma warning restore AL0432 end; + + /// + /// Opens a dialog for the user to change a password and returns the old and new typed passwords if there is no validation error, + /// otherwise an empty text are returned. + /// + /// Out parameter, the old password user typed on the dialog. + /// Out parameter, the new password user typed on the dialog. + [Obsolete('Replaced by OpenPasswordChangeDialog without out OldPassword param', '25.0')] + procedure OpenChangePasswordDialog(var OldPassword: SecretText; var Password: SecretText) + begin + PasswordDialogImpl.OpenChangePasswordDialog(OldPassword, Password); + end; #endif /// @@ -113,14 +125,14 @@ codeunit 9810 "Password Dialog Management" end; /// - /// Opens a dialog for the user to change a password and returns the old and new typed passwords if there is no validation error, + /// Opens a dialog for the user to change a password and returns the new typed password if there is no validation error, /// otherwise an empty text are returned. /// - /// Out parameter, the old password user typed on the dialog. - /// Out parameter, the new password user typed on the dialog. - procedure OpenChangePasswordDialog(var OldPassword: SecretText; var Password: SecretText) + /// In parameter, the current password to compare with the password user typed on the dialog. + /// Out parameter, the new password user typed on the dialog. + procedure OpenPasswordChangeDialog(CurrentPassword: SecretText; var NewPassword: SecretText) begin - PasswordDialogImpl.OpenChangePasswordDialog(OldPassword, Password); + PasswordDialogImpl.OpenPasswordChangeDialog(CurrentPassword, NewPassword); end; /// diff --git a/src/System Application/Test Library/Password/app.json b/src/System Application/Test Library/Password/app.json index fe513ed383..333fdba0c2 100644 --- a/src/System Application/Test Library/Password/app.json +++ b/src/System Application/Test Library/Password/app.json @@ -23,8 +23,12 @@ ], "idRanges": [ { - "from": 135033, - "to": 135033 + "from": 132528, + "to": 132528 + }, + { + "from": 135033, + "to": 135033 } ], "platform": "25.0.0.0", diff --git a/src/System Application/Test Library/Password/src/LibraryPassword.Codeunit.al b/src/System Application/Test Library/Password/src/LibraryPassword.Codeunit.al new file mode 100644 index 0000000000..183bb8e616 --- /dev/null +++ b/src/System Application/Test Library/Password/src/LibraryPassword.Codeunit.al @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.TestLibraries.Security.AccessControl; + +using System.Security.AccessControl; + +codeunit 132528 "Library - Password" +{ + var + PasswordDialogImpl: Codeunit "Password Dialog Impl."; + + /// + /// Opens a dialog for the user to change a password and returns the old and new typed passwords if there is no validation error, + /// otherwise an empty text are returned. Used for OnPrem report "Change Password" + /// + /// Out parameter, the old password user typed on the dialog. + /// Out parameter, the new password user typed on the dialog. + procedure OpenChangePasswordDialog(var OldPassword: SecretText; var Password: SecretText) + begin + PasswordDialogImpl.OpenChangePasswordDialog(OldPassword, Password); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/Password/src/PasswordDialogTest.Codeunit.al b/src/System Application/Test/Password/src/PasswordDialogTest.Codeunit.al index 1753ef9bee..63265da1ca 100644 --- a/src/System Application/Test/Password/src/PasswordDialogTest.Codeunit.al +++ b/src/System Application/Test/Password/src/PasswordDialogTest.Codeunit.al @@ -26,8 +26,11 @@ codeunit 135033 "Password Dialog Test" DisablePasswordValidation: Boolean; PasswordMissmatch: Boolean; ValidPasswordTxt: Label 'Some Password 2!'; + AnotherValidPasswordTxt: Label 'Some Password 3!'; InValidPasswordTxt: Label 'Some Password'; AnotherPasswordTxt: Label 'Another Password'; + PasswordSameAsNewErr: Label 'The new password cannot be the same as the current password.'; + CurrentPasswordMismatchErr: Label 'The current password does not match the entered password.'; [Test] [HandlerFunctions('PasswordDialogModalPageHandler')] @@ -207,20 +210,64 @@ codeunit 135033 "Password Dialog Test" [HandlerFunctions('ChangePasswordDialogModalPageHandler')] procedure OpenChangePasswordDialogTest(); var + LibraryPassword: Codeunit "Library - Password"; Password: SecretText; OldPassword: SecretText; begin // [SCENARIO] Open Password dialog in change password mode. + // Used for "Change Password" OnPrem report. + // Internal implementation to support OnPrem Database.ChangeUserPassword PermissionsMock.Set('All Objects'); // [WHEN] The password dialog is opened in change password mode. - PasswordDialogManagement.OpenChangePasswordDialog(OldPassword, Password); + LibraryPassword.OpenChangePasswordDialog(OldPassword, Password); // [THEN] The Old and New passwords are retrieved. Assert.AreEqual(InValidPasswordTxt, GetPasswordValue(OldPassword), 'A diferrent password was expected.'); Assert.AreEqual(ValidPasswordTxt, GetPasswordValue(Password), 'A diferrent password was expected.') end; + [Test] + [HandlerFunctions('ChangePasswordDialogModalPageHandler')] + procedure OpenPasswordChangeDialogTest(); + var + Password: SecretText; + CurrentPassword: SecretText; + begin + // [SCENARIO] Open Password dialog in change password mode. + PermissionsMock.Set('All Objects'); + + // [WHEN] The password dialog is opened in change password mode. + PasswordDialogManagement.OpenPasswordChangeDialog(CurrentPassword, Password); + + // [THEN] The New passwords was retrieved. + Assert.AreEqual('', GetPasswordValue(CurrentPassword), 'A diferrent password was expected.'); + Assert.AreEqual(ValidPasswordTxt, GetPasswordValue(Password), 'A diferrent password was expected.') + end; + + [Test] + [HandlerFunctions('ChangePasswordDialogWithCurrentPasswordModalPageHandler')] + procedure OpenPasswordChangeDialogWithCurrentPasswordTest(); + var + Password: SecretText; + CurrentPassword: SecretText; + begin + // [SCENARIO] Open Password dialog in change password mode. + // The old password has been passed on. + PermissionsMock.Set('All Objects'); + + // [GIVEN] The old password is for comparison that the old password matches the entered user and + // the new password does not match the old password. + CurrentPassword := SecretStrSubstNo(ValidPasswordTxt); + + // [WHEN] The password dialog is opened in change password mode. + PasswordDialogManagement.OpenPasswordChangeDialog(CurrentPassword, Password); + + // [THEN] The Old and New passwords are retrieved. + Assert.AreEqual(ValidPasswordTxt, GetPasswordValue(CurrentPassword), 'A diferrent password was expected.'); + Assert.AreEqual(AnotherValidPasswordTxt, GetPasswordValue(Password), 'A diferrent password was expected.') + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Password Dialog Management", 'OnSetMinPasswordLength', '', true, true)] local procedure OnSetMinimumPAsswordLength(var MinPasswordLength: Integer); begin @@ -243,14 +290,29 @@ codeunit 135033 "Password Dialog Test" [ModalPageHandler] procedure ChangePasswordDialogModalPageHandler(var PasswordDialog: TestPage "Password Dialog"); begin - Assert.IsTrue(PasswordDialog.OldPassword.Visible(), 'Old Password Field should not be visible.'); - Assert.IsTrue(PasswordDialog.ConfirmPassword.Visible(), 'Confirm Password Field should not be visible.'); + Assert.IsTrue(PasswordDialog.OldPassword.Visible(), 'Old Password Field should be visible.'); + Assert.IsTrue(PasswordDialog.ConfirmPassword.Visible(), 'Confirm Password Field should be visible.'); PasswordDialog.OldPassword.SetValue(InValidPasswordTxt); PasswordDialog.Password.SetValue(ValidPasswordTxt); PasswordDialog.ConfirmPassword.SetValue(ValidPasswordTxt); PasswordDialog.OK().Invoke(); end; + [ModalPageHandler] + procedure ChangePasswordDialogWithCurrentPasswordModalPageHandler(var PasswordDialog: TestPage "Password Dialog"); + begin + Assert.IsTrue(PasswordDialog.OldPassword.Visible(), 'Old Password Field should be visible.'); + Assert.IsTrue(PasswordDialog.ConfirmPassword.Visible(), 'Confirm Password Field should be visible.'); + asserterror PasswordDialog.OldPassword.SetValue(InValidPasswordTxt); + Assert.ExpectedError(CurrentPasswordMismatchErr); + PasswordDialog.OldPassword.SetValue(ValidPasswordTxt); + asserterror PasswordDialog.Password.SetValue(ValidPasswordTxt); + Assert.ExpectedError(PasswordSameAsNewErr); + PasswordDialog.Password.SetValue(AnotherValidPasswordTxt); + PasswordDialog.ConfirmPassword.SetValue(AnotherValidPasswordTxt); + PasswordDialog.OK().Invoke(); + end; + [ConfirmHandler] procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean); begin diff --git a/src/System Application/Test/System Initialization/src/SystemInitializationTest.Codeunit.al b/src/System Application/Test/System Initialization/src/SystemInitializationTest.Codeunit.al index 20188ee021..b98fb2a202 100644 --- a/src/System Application/Test/System Initialization/src/SystemInitializationTest.Codeunit.al +++ b/src/System Application/Test/System Initialization/src/SystemInitializationTest.Codeunit.al @@ -33,15 +33,15 @@ codeunit 130045 "System Initialization Test" CompanyTriggers: Codeunit "Company Triggers"; PasswordDialogManagement: Codeunit "Password Dialog Management"; PermissionsMock: Codeunit "Permissions Mock"; - OldPassword: SecretText; + CurrentPassword: SecretText; NewPassword: SecretText; begin PermissionsMock.Set('System Init Exec'); // [WHEN] Calling CompanyTriggers.OnCompanyOpen() CompanyTriggers.OnCompanyOpenCompleted(); - // [THEN] Calling PasswordDialog.OpenChangePasswordDialog should NOT results in an error - PasswordDialogManagement.OpenChangePasswordDialog(OldPassword, NewPassword); + // [THEN] Calling PasswordDialog.OpenPasswordChangeDialog should NOT results in an error + PasswordDialogManagement.OpenPasswordChangeDialog(CurrentPassword, NewPassword); end; [ModalPageHandler]