From 0d47b7524d4d07a9faa255e950fa971f0e7e57a8 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 14 Mar 2025 11:10:17 +0100 Subject: [PATCH 1/2] Introduce UserPasswordResettingNotification --- .../Security/SecurityControllerBase.cs | 4 ++ .../UserPasswordResettingNotification.cs | 13 ++++++ src/Umbraco.Core/Services/IUserService.cs | 2 + src/Umbraco.Core/Services/UserService.cs | 40 +++++++++++++++---- 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs index 09b493c48412..1482517a15dc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs @@ -21,6 +21,10 @@ protected IActionResult UserOperationStatusResult(UserOperationStatus status, Er .WithTitle("The password reset token was invalid") .WithDetail("The specified password reset token was either used already or wrong.") .Build()), + UserOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder + .WithTitle("Cancelled by notification") + .WithDetail("A notification handler prevented the user operation.") + .Build()), UserOperationStatus.UnknownFailure => BadRequest(problemDetailsBuilder .WithTitle("Unknown failure") .WithDetail(errorMessageResult?.Error?.ErrorMessage ?? "The error was unknown") diff --git a/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs new file mode 100644 index 000000000000..9edf9ce26513 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordResettingNotification : CancelableObjectNotification +{ + public UserPasswordResettingNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + + public IUser User => Target; +} diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index cd600a083db9..bcb47438fceb 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -79,6 +79,8 @@ public interface IUserService : IMembershipUserService Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model); + Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) => ChangePasswordAsync(performingUser.Key, model); + Task ClearAvatarAsync(Guid userKey); Task, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index e92a5b0cd0f2..e43e6049ba35 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1180,7 +1180,7 @@ private UserOperationStatus ValidateUserUpdateModel(IUser existingUser, UserUpda return keys; } - public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) + public async Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) { IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -1197,12 +1197,6 @@ public async Task> ChangePass return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel()); } - IUser? performingUser = await userStore.GetAsync(performingUserKey); - if (performingUser is null) - { - return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); - } - // require old password for self change when outside of invite or resetByToken flows if (performingUser.UserState != UserState.Invited && performingUser.Username == user.Username && string.IsNullOrEmpty(model.OldPassword) && string.IsNullOrEmpty(model.ResetPasswordToken)) { @@ -1242,6 +1236,21 @@ public async Task> ChangePass return Attempt.SucceedWithStatus(UserOperationStatus.Success, result.Result ?? new PasswordChangedModel()); } + + + public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) + { + IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); + IUser? performingUser = await userStore.GetAsync(performingUserKey); + if (performingUser is null) + { + return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); + } + + return await ChangePasswordAsync(performingUser, model); + } + public async Task?, UserOperationStatus>> GetAllAsync(Guid performingUserKey, int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -2183,6 +2192,23 @@ public async Task> CreateInit public async Task> ResetPasswordAsync(Guid userKey, string token, string password) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + + EventMessages evtMsgs = EventMessagesFactory.Get(); + IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); + + IUser? user = await userStore.GetAsync(userKey); + if (user is null) + { + return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel()); + } + + var savingNotification = new UserPasswordResettingNotification(user, evtMsgs); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(UserOperationStatus.CancelledByNotification, new PasswordChangedModel()); + } Attempt changePasswordAttempt = await ChangePasswordAsync(userKey, new ChangeUserPasswordModel From e073efcd6462a6add75c7e7e7720d555fa3165b1 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 9 Apr 2025 06:27:52 +0200 Subject: [PATCH 2/2] Removed changes to IUserService interface. --- src/Umbraco.Core/Services/IUserService.cs | 2 - src/Umbraco.Core/Services/UserService.cs | 46 +++++++++++------------ 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index bcb47438fceb..cd600a083db9 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -79,8 +79,6 @@ public interface IUserService : IMembershipUserService Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model); - Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) => ChangePasswordAsync(performingUser.Key, model); - Task ClearAvatarAsync(Guid userKey); Task, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index f8100ba887ea..8fe2d4bbc72b 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1180,7 +1180,21 @@ private UserOperationStatus ValidateUserUpdateModel(IUser existingUser, UserUpda return keys; } - public async Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) + /// + public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) + { + IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); + IUser? performingUser = await userStore.GetAsync(performingUserKey); + if (performingUser is null) + { + return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); + } + + return await ChangePasswordAsync(performingUser, model); + } + + private async Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) { IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -1220,12 +1234,13 @@ public async Task> ChangePass IBackOfficePasswordChanger passwordChanger = serviceScope.ServiceProvider.GetRequiredService(); Attempt result = await passwordChanger.ChangeBackOfficePassword( new ChangeBackOfficeUserPasswordModel - { - NewPassword = model.NewPassword, - OldPassword = model.OldPassword, - User = user, - ResetPasswordToken = model.ResetPasswordToken, - }, performingUser); + { + NewPassword = model.NewPassword, + OldPassword = model.OldPassword, + User = user, + ResetPasswordToken = model.ResetPasswordToken, + }, + performingUser); if (result.Success is false) { @@ -1236,21 +1251,6 @@ public async Task> ChangePass return Attempt.SucceedWithStatus(UserOperationStatus.Success, result.Result ?? new PasswordChangedModel()); } - - - public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) - { - IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); - IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); - IUser? performingUser = await userStore.GetAsync(performingUserKey); - if (performingUser is null) - { - return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); - } - - return await ChangePasswordAsync(performingUser, model); - } - public async Task?, UserOperationStatus>> GetAllAsync(Guid performingUserKey, int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -2212,7 +2212,7 @@ public async Task> ResetPassw } Attempt changePasswordAttempt = - await ChangePasswordAsync(userKey, new ChangeUserPasswordModel + await ChangePasswordAsync(user, new ChangeUserPasswordModel { NewPassword = password, UserKey = userKey,