diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ChangePasswordUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ChangePasswordUserController.cs index fa7c99f935..9d6619c00a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ChangePasswordUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ChangePasswordUserController.cs @@ -1,13 +1,17 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; @@ -15,35 +19,46 @@ namespace Umbraco.Cms.Api.Management.Controllers.User; public class ChangePasswordUserController : UserControllerBase { private readonly IUserService _userService; - private readonly IUmbracoMapper _mapper; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IAuthorizationService _authorizationService; public ChangePasswordUserController( IUserService userService, - IUmbracoMapper mapper, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IAuthorizationService authorizationService) { _userService = userService; - _mapper = mapper; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _authorizationService = authorizationService; } - [HttpPost("change-password/{id:guid}")] + [HttpPost("{id:guid}/change-password")] [MapToApiVersion("1.0")] - [ProducesErrorResponseType(typeof(ChangePasswordUserResponseModel))] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task ChangePassword(Guid id, ChangePasswordUserRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + var passwordModel = new ChangeUserPasswordModel { NewPassword = model.NewPassword, - OldPassword = model.OldPassword, UserKey = id, }; Attempt response = await _userService.ChangePasswordAsync(CurrentUserKey(_backOfficeSecurityAccessor), passwordModel); return response.Success - ? Ok(_mapper.Map(response.Result)) + ? Ok() : UserOperationStatusResult(response.Status, response.Result); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUserController.cs index 95b8bbd58f..d3f439adbe 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUserController.cs @@ -1,5 +1,6 @@ using Asp.Versioning; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Core.Services; @@ -23,6 +24,9 @@ public class ClearAvatarUserController : UserControllerBase [MapToApiVersion("1.0")] [HttpDelete("avatar/{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task ClearAvatar(Guid id) { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs index 155b712fb7..8753c327b8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs @@ -24,6 +24,7 @@ public class CreateInitialPasswordUserController : UserControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task CreateInitialPassword(CreateInitialPasswordUserRequestModel model) { Attempt response = await _userService.CreateInitialPasswordAsync(model.User.Id, model.Token, model.Password); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateUserController.cs index 1f540f664b..285dc041bc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateUserController.cs @@ -35,8 +35,9 @@ public class CreateUserController : UserControllerBase [HttpPost] [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(CreateUserResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Create(CreateUserRequestModel model) { UserCreateModel createModel = await _presentationFactory.CreateCreationModelAsync(model); @@ -44,7 +45,7 @@ public class CreateUserController : UserControllerBase Attempt result = await _userService.CreateAsync(CurrentUserKey(_backOfficeSecurityAccessor), createModel, true); return result.Success - ? Ok(_mapper.Map(result.Result)) + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result.CreatedUser!.Key) : UserOperationStatusResult(result.Status, result.Result); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/ChangePasswordCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/ChangePasswordCurrentUserController.cs index 5a29e278e3..f3ee849457 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/ChangePasswordCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/ChangePasswordCurrentUserController.cs @@ -1,8 +1,8 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.ViewModels.User; +using Umbraco.Cms.Api.Management.ViewModels.User.Current; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -16,22 +16,20 @@ public class ChangePasswordCurrentUserController : CurrentUserControllerBase { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IUserService _userService; - private readonly IUmbracoMapper _mapper; public ChangePasswordCurrentUserController( IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IUserService userService, - IUmbracoMapper mapper) + IUserService userService) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _userService = userService; - _mapper = mapper; } [HttpPost("change-password")] [MapToApiVersion("1.0")] - [ProducesErrorResponseType(typeof(ChangePasswordUserResponseModel))] - public async Task ChangePassword(ChangePasswordUserRequestModel model) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task ChangePassword(ChangePasswordCurrentUserRequestModel model) { Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor); @@ -45,7 +43,7 @@ public class ChangePasswordCurrentUserController : CurrentUserControllerBase Attempt response = await _userService.ChangePasswordAsync(userKey, changeModel); return response.Success - ? Ok(_mapper.Map(response.Result)) + ? Ok() : UserOperationStatusResult(response.Status, response.Result); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetDocumentPermissionsCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetDocumentPermissionsCurrentUserController.cs index 668f825534..bbc53306a5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetDocumentPermissionsCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetDocumentPermissionsCurrentUserController.cs @@ -31,6 +31,7 @@ public class GetDocumentPermissionsCurrentUserController : CurrentUserController [MapToApiVersion("1.0")] [HttpGet("permissions/document")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetPermissions([FromQuery(Name = "id")] HashSet ids) { Attempt, UserOperationStatus> permissionsAttempt = await _userService.GetDocumentPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetMediaPermissionsCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetMediaPermissionsCurrentUserController.cs index 558a21cfb3..d3ee9bf5dd 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetMediaPermissionsCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetMediaPermissionsCurrentUserController.cs @@ -30,7 +30,8 @@ public class GetMediaPermissionsCurrentUserController : CurrentUserControllerBas [MapToApiVersion("1.0")] [HttpGet("permissions/media")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(UserPermissionsResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetPermissions([FromQuery(Name = "id")] HashSet ids) { Attempt, UserOperationStatus> permissionsAttempt = await _userService.GetMediaPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetPermissionsCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetPermissionsCurrentUserController.cs index ed34029ccb..2c664b5b5f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetPermissionsCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetPermissionsCurrentUserController.cs @@ -30,7 +30,7 @@ public class GetPermissionsCurrentUserController : CurrentUserControllerBase [MapToApiVersion("1.0")] [HttpGet("permissions")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(UserPermissionsResponseModel), StatusCodes.Status200OK)] public async Task GetPermissions([FromQuery(Name = "id")] HashSet ids) { Attempt, UserOperationStatus> permissionsAttempt = await _userService.GetPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids.ToArray()); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs index 2b764cc6b6..531f1eb0f9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs @@ -32,6 +32,7 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase [MapToApiVersion("1.0")] [HttpPost("avatar")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task SetAvatar(SetAvatarRequestModel model) { Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUserController.cs index 558267f9c9..b89dea3531 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUserController.cs @@ -1,5 +1,6 @@ using Asp.Versioning; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Core.Security; @@ -29,6 +30,9 @@ public class DeleteUserController : UserControllerBase [MapToApiVersion("1.0")] [HttpDelete("{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task DeleteUser(Guid id) { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUserController.cs index 4a820202e9..485170b47b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUserController.cs @@ -33,6 +33,7 @@ public class DisableUserController : UserControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task DisableUsers(DisableUserRequestModel model) { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUserController.cs index 1f62b57ec3..3ed051d3c6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUserController.cs @@ -33,6 +33,7 @@ public class EnableUserController : UserControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task EnableUsers(EnableUserRequestModel model) { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/GetAllUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/GetAllUserController.cs index 703f680613..7c7be52a14 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/GetAllUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/GetAllUserController.cs @@ -34,6 +34,7 @@ public class GetAllUserController : UserControllerBase [HttpGet] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetAll(int skip = 0, int take = 100) { Attempt?, UserOperationStatus> attempt = await _userService.GetAllAsync(CurrentUserKey(_backOfficeSecurityAccessor), skip, take); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/InviteUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/InviteUsersController.cs index 082be71063..572efc2efb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/InviteUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/InviteUsersController.cs @@ -33,6 +33,8 @@ public class InviteUserController : UserControllerBase [HttpPost("invite")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Invite(InviteUserRequestModel model) { UserInviteModel userInvite = await _userPresentationFactory.CreateInviteModelAsync(model); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ResendInviteUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ResendInviteUserController.cs index 043a456ad0..8209b75227 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ResendInviteUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ResendInviteUserController.cs @@ -30,6 +30,7 @@ public class ResendInviteUserController : UserControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ResendInvite(ResendInviteUserRequestModel model) { UserResendInviteModel resendInviteModel = await _userPresentationFactory.CreateResendInviteModelAsync(model); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ResetPasswordUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ResetPasswordUserController.cs new file mode 100644 index 0000000000..df1f6f8480 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ResetPasswordUserController.cs @@ -0,0 +1,61 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; +using Umbraco.Cms.Api.Management.ViewModels.User; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.User; + +[ApiVersion("1.0")] +public class ResetPasswordUserController : UserControllerBase +{ + private readonly IUserService _userService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUmbracoMapper _mapper; + private readonly IAuthorizationService _authorizationService; + + public ResetPasswordUserController( + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper mapper, + IAuthorizationService authorizationService) + { + _userService = userService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mapper = mapper; + _authorizationService = authorizationService; + } + + [HttpPost("{id:guid}/reset-password")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ResetPasswordUserResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task ResetPassword(Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + Attempt response = await _userService.ResetPasswordAsync(CurrentUserKey(_backOfficeSecurityAccessor), id); + + return response.Success + ? Ok(_mapper.Map(response.Result)) + : UserOperationStatusResult(response.Status, response.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUserController.cs index 58972a5c7e..4ec1aa91ce 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUserController.cs @@ -27,6 +27,7 @@ public class SetAvatarUserController : UserControllerBase [HttpPost("avatar/{id:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task SetAvatar(Guid id, SetAvatarRequestModel model) { AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserController.cs index a0daf50234..a4d3761cb1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserController.cs @@ -1,6 +1,9 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -8,6 +11,8 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; @@ -17,20 +22,37 @@ public class UpdateUserController : UserControllerBase private readonly IUserService _userService; private readonly IUserPresentationFactory _userPresentationFactory; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IAuthorizationService _authorizationService; public UpdateUserController( IUserService userService, - IUserPresentationFactory userPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IUserPresentationFactory userPresentationFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IAuthorizationService authorizationService) { _userService = userService; _userPresentationFactory = userPresentationFactory; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _authorizationService = authorizationService; } [HttpPut("{id:guid}")] [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Update(Guid id, UpdateUserRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + // We have to use an intermediate save model, and cannot map it directly to an IUserModel // This is because we need to compare the updated values with what the user already has, for audit purposes. UserUpdateModel updateModel = await _userPresentationFactory.CreateUpdateModelAsync(id, model); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs index dd3f116973..90ba76eec1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs @@ -61,9 +61,9 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB .WithTitle("Cannot delete") .WithDetail("A user cannot delete itself.") .Build()), - UserOperationStatus.OldPasswordRequired => BadRequest(problemDetailsBuilder + UserOperationStatus.SelfOldPasswordRequired => BadRequest(problemDetailsBuilder .WithTitle("Old password required") - .WithDetail("The old password is required to change the password of the specified user.") + .WithDetail("The old password is required to change your own password.") .Build()), UserOperationStatus.InvalidAvatar => BadRequest(problemDetailsBuilder .WithTitle("Invalid avatar") @@ -117,6 +117,10 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB .WithTitle("Invalid user state") .WithDetail("The target user is not in the invite state.") .Build()), + UserOperationStatus.SelfPasswordResetNotAllowed => BadRequest(problemDetailsBuilder + .WithTitle("Self password reset not allowed") + .WithDetail("It is not allowed to reset the password for the account you are logged in to.") + .Build()), UserOperationStatus.Forbidden => Forbidden(), _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder .WithTitle("Unknown user operation status.") diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUserController.cs index e1ba251974..1c193e1780 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUserController.cs @@ -23,6 +23,7 @@ public class VerifyInviteUserController : UserControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task Invite(VerifyInviteUserRequestModel model) { Attempt result = await _userService.VerifyInviteAsync(model.User.Id, model.Token); diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 80cc6a3372..d95488c728 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -78,6 +78,7 @@ public class UserPresentationFactory : IUserPresentationFactory { var createModel = new UserCreateModel { + Id = requestModel.Id, Email = requestModel.Email, Name = requestModel.Name, UserName = requestModel.UserName, diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Users/UsersViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Users/UsersViewModelsMapDefinition.cs index 5841473881..c33d9666a0 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Users/UsersViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Users/UsersViewModelsMapDefinition.cs @@ -11,7 +11,7 @@ public class UsersViewModelsMapDefinition : IMapDefinition { public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((_, _) => new ChangePasswordUserResponseModel(), Map); + mapper.Define((_, _) => new ResetPasswordUserResponseModel(), Map); mapper.Define((_, _) => new CreateUserResponseModel { User = new() }, Map); mapper.Define((_, _) => new LinkedLoginViewModel { ProviderKey = string.Empty, ProviderName = string.Empty }, Map); } @@ -34,7 +34,7 @@ public class UsersViewModelsMapDefinition : IMapDefinition } // Umbraco.Code.MapAll - private void Map(PasswordChangedModel source, ChangePasswordUserResponseModel target, MapperContext context) + private void Map(PasswordChangedModel source, ResetPasswordUserResponseModel target, MapperContext context) { target.ResetPassword = source.ResetPassword; } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index fa5c523723..0f7eef1fb8 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -27589,8 +27589,38 @@ } }, "responses": { - "200": { - "description": "Success", + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27606,35 +27636,23 @@ "content": { "application/json": { "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/CreateUserResponseModel" - } - ] + "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/CreateUserResponseModel" - } - ] + "$ref": "#/components/schemas/ProblemDetails" } }, "text/plain": { "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/CreateUserResponseModel" - } - ] + "$ref": "#/components/schemas/ProblemDetails" } } } }, - "400": { - "description": "Bad Request", + "404": { + "description": "Not Found", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27830,6 +27848,26 @@ } } }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -27956,6 +27994,70 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -28044,8 +28146,87 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ @@ -28279,6 +28460,321 @@ ] } }, + "/umbraco/management/api/v1/user/{id}/change-password": { + "post": { + "tags": [ + "User" + ], + "operationId": "PostUserByIdChangePassword", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + }, + { + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + }, + { + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + }, + { + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/user/{id}/reset-password": { + "post": { + "tags": [ + "User" + ], + "operationId": "PostUserByIdResetPassword", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ResetPasswordUserResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ResetPasswordUserResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ResetPasswordUserResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/user/avatar/{id}": { "delete": { "tags": [ @@ -28312,6 +28808,70 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -28432,6 +28992,38 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -28458,81 +29050,6 @@ ] } }, - "/umbraco/management/api/v1/user/change-password/{id}": { - "post": { - "tags": [ - "User" - ], - "operationId": "PostUserChangePasswordById", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" - } - ] - } - }, - "text/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" - } - ] - } - }, - "application/*+json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "headers": { - "Umb-Notifications": { - "description": "The list of notifications produced during the request.", - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NotificationHeaderModel" - }, - "nullable": true - } - } - } - }, - "401": { - "description": "The resource is protected and requires an authentication token" - } - }, - "security": [ - { - "Backoffice User": [ ] - } - ] - } - }, "/umbraco/management/api/v1/user/configuration": { "get": { "tags": [ @@ -29125,6 +29642,38 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -29163,7 +29712,7 @@ "schema": { "oneOf": [ { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" } ] } @@ -29172,7 +29721,7 @@ "schema": { "oneOf": [ { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" } ] } @@ -29181,7 +29730,7 @@ "schema": { "oneOf": [ { - "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + "$ref": "#/components/schemas/ChangePasswordCurrentUserRequestModel" } ] } @@ -29204,6 +29753,38 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -29341,38 +29922,29 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] } }, "text/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] } }, "text/plain": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] } } } @@ -29450,6 +30022,26 @@ } } }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -29487,38 +30079,49 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] } }, "text/json": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] } }, "text/plain": { "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserPermissionsResponseModel" - } - ] - } + "oneOf": [ + { + "$ref": "#/components/schemas/UserPermissionsResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -29619,6 +30222,38 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -29730,6 +30365,38 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" }, @@ -29824,6 +30491,70 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -29920,6 +30651,38 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -30016,6 +30779,38 @@ } } }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -30121,6 +30916,38 @@ } } }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "401": { "description": "The resource is protected and requires an authentication token" } @@ -31085,6 +31912,21 @@ ], "additionalProperties": false }, + "ChangePasswordCurrentUserRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChangePasswordUserRequestModel" + } + ], + "properties": { + "oldPassword": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "ChangePasswordUserRequestModel": { "required": [ "newPassword" @@ -31093,10 +31935,6 @@ "properties": { "newPassword": { "type": "string" - }, - "oldPassword": { - "type": "string", - "nullable": true } }, "additionalProperties": false @@ -32670,23 +33508,10 @@ "$ref": "#/components/schemas/UserPresentationBaseModel" } ], - "additionalProperties": false - }, - "CreateUserResponseModel": { - "required": [ - "user" - ], - "type": "object", "properties": { - "user": { - "oneOf": [ - { - "$ref": "#/components/schemas/ReferenceByIdModel" - } - ] - }, - "initialPassword": { + "id": { "type": "string", + "format": "uuid", "nullable": true } }, @@ -37923,6 +38748,16 @@ }, "additionalProperties": false }, + "ResetPasswordUserResponseModel": { + "type": "object", + "properties": { + "resetPassword": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "RuntimeLevelModel": { "enum": [ "Unknown", @@ -40274,4 +41109,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserRequestModel.cs index 1e7e638b36..9c2ab978c9 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserRequestModel.cs @@ -6,9 +6,4 @@ public class ChangePasswordUserRequestModel /// The new password. /// public required string NewPassword { get; set; } - - /// - /// The old password. - /// - public string? OldPassword { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs index 6867a39655..fa9b1b856a 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs @@ -2,5 +2,5 @@ public class CreateUserRequestModel : UserPresentationBase { - + public Guid? Id { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/ChangePasswordCurrentUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/ChangePasswordCurrentUserRequestModel.cs new file mode 100644 index 0000000000..f413d1b2c5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/ChangePasswordCurrentUserRequestModel.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.User.Current; + +public class ChangePasswordCurrentUserRequestModel : ChangePasswordUserRequestModel +{ + /// + /// The old password. + /// + public string? OldPassword { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/ResetPasswordUserResponseModel.cs similarity index 70% rename from src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/User/ResetPasswordUserResponseModel.cs index c47c1834a6..974986cb79 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/ChangePasswordUserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/ResetPasswordUserResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.User; -public class ChangePasswordUserResponseModel +public class ResetPasswordUserResponseModel { public string? ResetPassword { get; set; } } diff --git a/src/Umbraco.Core/Models/UserCreateModel.cs b/src/Umbraco.Core/Models/UserCreateModel.cs index fafd47946c..2df55df358 100644 --- a/src/Umbraco.Core/Models/UserCreateModel.cs +++ b/src/Umbraco.Core/Models/UserCreateModel.cs @@ -4,6 +4,8 @@ namespace Umbraco.Cms.Core.Models; public class UserCreateModel { + public Guid? Id { get; set; } + public string Email { get; set; } = string.Empty; public string UserName { get; set; } = string.Empty; diff --git a/src/Umbraco.Core/Security/ICoreBackOfficeUserManager.cs b/src/Umbraco.Core/Security/ICoreBackOfficeUserManager.cs index d9225e372a..4456d7b6c0 100644 --- a/src/Umbraco.Core/Security/ICoreBackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/ICoreBackOfficeUserManager.cs @@ -17,6 +17,7 @@ public interface ICoreBackOfficeUserManager Task CreateForInvite(UserCreateModel createModel); Task> GenerateEmailConfirmationTokenAsync(IUser user); + Task> GeneratePasswordResetTokenAsync(IUser user); Task> UnlockUser(IUser user); @@ -24,6 +25,10 @@ public interface ICoreBackOfficeUserManager Task, UserOperationStatus>> GetLoginsAsync(IUser user); Task IsEmailConfirmationTokenValidAsync(IUser user, string token); + Task IsResetPasswordTokenValidAsync(IUser user, string token); + void NotifyForgotPasswordRequested(IPrincipal user, string toString); + + public string GeneratePassword(); } diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index b95a53d99b..8d6259b171 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -51,33 +51,33 @@ public interface IUserService : IMembershipUserService /// /// This creates both the Umbraco user and the identity user. /// - /// The key of the user performing the operation. + /// The key of the user performing the operation. /// Model to create the user from. /// Specifies if the user should be enabled be default. Defaults to false. /// An attempt indicating if the operation was a success as well as a more detailed . - Task> CreateAsync(Guid userKey, UserCreateModel model, bool approveUser = false); + Task> CreateAsync(Guid performingUserKey, UserCreateModel model, bool approveUser = false); - Task> InviteAsync(Guid userKey, UserInviteModel model); + Task> InviteAsync(Guid performingUserKey, UserInviteModel model); Task> VerifyInviteAsync(Guid userKey, string token); Task> CreateInitialPasswordAsync(Guid userKey, string token, string password); - Task> UpdateAsync(Guid userKey, UserUpdateModel model); + Task> UpdateAsync(Guid performingUserKey, UserUpdateModel model); Task SetAvatarAsync(Guid userKey, Guid temporaryFileKey); - Task DeleteAsync(Guid userKey, ISet keys); + Task DeleteAsync(Guid performingUserKey, ISet keys); - Task DeleteAsync(Guid userKey, Guid key) => DeleteAsync(userKey, new HashSet { key }); + Task DeleteAsync(Guid performingUserKey, Guid key) => DeleteAsync(performingUserKey, new HashSet { key }); - Task DisableAsync(Guid userKey, ISet keys); + Task DisableAsync(Guid performingUserKey, ISet keys); - Task EnableAsync(Guid userKey, ISet keys); + Task EnableAsync(Guid performingUserKey, ISet keys); - Task> UnlockAsync(Guid userKey, params Guid[] keys); + Task> UnlockAsync(Guid performingUserKey, params Guid[] keys); - Task> ChangePasswordAsync(Guid userKey, ChangeUserPasswordModel model); + Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model); Task ClearAvatarAsync(Guid userKey); @@ -86,11 +86,11 @@ public interface IUserService : IMembershipUserService /// /// Gets all users that the requesting user is allowed to see. /// - /// The Key of the user requesting the users. + /// The Key of the user requesting the users. /// Amount to skip. /// Amount to take. /// All users that the user is allowed to see. - Task?, UserOperationStatus>> GetAllAsync(Guid userKey, int skip, int take) => throw new NotImplementedException(); + Task?, UserOperationStatus>> GetAllAsync(Guid performingUserKey, int skip, int take) => throw new NotImplementedException(); public Task, UserOperationStatus>> FilterAsync( Guid userKey, @@ -406,4 +406,6 @@ public interface IUserService : IMembershipUserService Task> SendResetPasswordEmailAsync(string userEmail); Task> ResendInvitationAsync(Guid performingUserKey, UserResendInviteModel model); + + Task> ResetPasswordAsync(Guid performingUserKey, Guid userKey); } diff --git a/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs index 8cacd6d2a3..122f7fa792 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs @@ -24,7 +24,7 @@ public enum UserOperationStatus CannotDisableSelf, CannotDeleteSelf, CannotDisableInvitedUser, - OldPasswordRequired, + SelfOldPasswordRequired, InvalidAvatar, InvalidIsoCode, InvalidInviteToken, @@ -35,5 +35,7 @@ public enum UserOperationStatus MediaNodeNotFound, UnknownFailure, CannotPasswordReset, - NotInInviteState + NotInInviteState, + SelfPasswordResetNotAllowed, + DuplicateId, } diff --git a/src/Umbraco.Core/Services/TwoFactorLoginServiceBase.cs b/src/Umbraco.Core/Services/TwoFactorLoginServiceBase.cs index d8d69bfb26..28becea848 100644 --- a/src/Umbraco.Core/Services/TwoFactorLoginServiceBase.cs +++ b/src/Umbraco.Core/Services/TwoFactorLoginServiceBase.cs @@ -37,7 +37,7 @@ internal abstract class TwoFactorLoginServiceBase public virtual async Task, TwoFactorOperationStatus>> GetProviderNamesAsync(Guid userKey) { IEnumerable allProviders = _twoFactorLoginService.GetAllProviderNames(); - var userProviders =(await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(userKey)).ToHashSet(); + var userProviders = (await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(userKey)).ToHashSet(); IEnumerable result = allProviders.Select(x => new UserTwoFactorProviderModel(x, userProviders.Contains(x))); return Attempt.SucceedWithStatus(TwoFactorOperationStatus.Success, result); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 027f34d0fc..b0e0828b58 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -603,12 +603,12 @@ internal class UserService : RepositoryService, IUserService } /// - public async Task> CreateAsync(Guid userKey, UserCreateModel model, bool approveUser = false) + public async Task> CreateAsync(Guid performingUserKey, UserCreateModel model, bool approveUser = false) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); using IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -622,7 +622,7 @@ internal class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new UserCreationResult()); } - UserOperationStatus result = ValidateUserCreateModel(model); + UserOperationStatus result = await ValidateUserCreateModel(model); if (result != UserOperationStatus.Success) { return Attempt.FailWithStatus(result, new UserCreationResult()); @@ -724,12 +724,12 @@ internal class UserService : RepositoryService, IUserService return Attempt.Succeed(UserOperationStatus.Success); } - public async Task> InviteAsync(Guid userKey, UserInviteModel model) + public async Task> InviteAsync(Guid performingUserKey, UserInviteModel model) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); using IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -743,7 +743,7 @@ internal class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new UserInvitationResult()); } - UserOperationStatus validationResult = ValidateUserCreateModel(model); + UserOperationStatus validationResult = await ValidateUserCreateModel(model); if (validationResult is not UserOperationStatus.Success) { @@ -858,7 +858,7 @@ internal class UserService : RepositoryService, IUserService return Attempt.SucceedWithStatus(UserOperationStatus.Success, new UserInvitationResult { InvitedUser = invitedUser }); } - private UserOperationStatus ValidateUserCreateModel(UserCreateModel model) + private async Task ValidateUserCreateModel(UserCreateModel model) { if (_securitySettings.UsernameIsEmail && model.UserName != model.Email) { @@ -869,6 +869,11 @@ internal class UserService : RepositoryService, IUserService return UserOperationStatus.InvalidEmail; } + if (model.Id is not null && await GetAsync(model.Id.Value) is not null) + { + return UserOperationStatus.DuplicateId; + } + if (GetByEmail(model.Email) is not null) { return UserOperationStatus.DuplicateEmail; @@ -887,7 +892,7 @@ internal class UserService : RepositoryService, IUserService return UserOperationStatus.Success; } - public async Task> UpdateAsync(Guid userKey, UserUpdateModel model) + public async Task> UpdateAsync(Guid performingUserKey, UserUpdateModel model) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); using IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); @@ -897,10 +902,10 @@ internal class UserService : RepositoryService, IUserService if (existingUser is null) { - return Attempt.FailWithStatus(UserOperationStatus.MissingUser, existingUser); + return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, existingUser); } - IUser? performingUser = await userStore.GetAsync(userKey); + IUser? performingUser = await userStore.GetAsync(performingUserKey); if (performingUser is null) { @@ -1091,7 +1096,7 @@ internal class UserService : RepositoryService, IUserService return keys; } - public async Task> ChangePasswordAsync(Guid userKey, ChangeUserPasswordModel model) + public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) { IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -1103,15 +1108,16 @@ internal class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel()); } - IUser? performingUser = await userStore.GetAsync(userKey); + 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)) { - return Attempt.FailWithStatus(UserOperationStatus.OldPasswordRequired, new PasswordChangedModel()); + return Attempt.FailWithStatus(UserOperationStatus.SelfOldPasswordRequired, new PasswordChangedModel()); } if (performingUser.IsAdmin() is false && user.IsAdmin()) @@ -1121,7 +1127,7 @@ internal class UserService : RepositoryService, IUserService if (string.IsNullOrEmpty(model.ResetPasswordToken) is false) { - Attempt verifyPasswordResetAsync = await VerifyPasswordResetAsync(userKey, model.ResetPasswordToken); + Attempt verifyPasswordResetAsync = await VerifyPasswordResetAsync(model.UserKey, model.ResetPasswordToken); if (verifyPasswordResetAsync.Result != UserOperationStatus.Success) { return Attempt.FailWithStatus(verifyPasswordResetAsync.Result, new PasswordChangedModel()); @@ -1147,11 +1153,11 @@ internal class UserService : RepositoryService, IUserService return Attempt.SucceedWithStatus(UserOperationStatus.Success, result.Result ?? new PasswordChangedModel()); } - public async Task?, UserOperationStatus>> GetAllAsync(Guid userKey, int skip, int take) + public async Task?, UserOperationStatus>> GetAllAsync(Guid performingUserKey, int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IUser? requestingUser = await GetAsync(userKey); + IUser? requestingUser = await GetAsync(performingUserKey); if (requestingUser is null) { @@ -1364,7 +1370,7 @@ internal class UserService : RepositoryService, IUserService }; } - public async Task DeleteAsync(Guid userKey, ISet keys) + public async Task DeleteAsync(Guid performingUserKey, ISet keys) { if(keys.Any() is false) { @@ -1372,7 +1378,7 @@ internal class UserService : RepositoryService, IUserService } using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -1412,7 +1418,7 @@ internal class UserService : RepositoryService, IUserService return UserOperationStatus.Success; } - public async Task DisableAsync(Guid userKey, ISet keys) + public async Task DisableAsync(Guid performingUserKey, ISet keys) { if(keys.Any() is false) { @@ -1420,7 +1426,7 @@ internal class UserService : RepositoryService, IUserService } using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -1458,7 +1464,7 @@ internal class UserService : RepositoryService, IUserService return UserOperationStatus.Success; } - public async Task EnableAsync(Guid userKey, ISet keys) + public async Task EnableAsync(Guid performingUserKey, ISet keys) { if(keys.Any() is false) { @@ -1466,7 +1472,7 @@ internal class UserService : RepositoryService, IUserService } using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -1528,7 +1534,7 @@ internal class UserService : RepositoryService, IUserService return UserOperationStatus.Success; } - public async Task> UnlockAsync(Guid userKey, params Guid[] keys) + public async Task> UnlockAsync(Guid performingUserKey, params Guid[] keys) { if (keys.Length == 0) { @@ -1536,7 +1542,7 @@ internal class UserService : RepositoryService, IUserService } using ICoreScope scope = ScopeProvider.CreateCoreScope(); - IUser? performingUser = await GetAsync(userKey); + IUser? performingUser = await GetAsync(performingUserKey); if (performingUser is null) { @@ -2102,6 +2108,40 @@ internal class UserService : RepositoryService, IUserService return changePasswordAttempt; } + public async Task> ResetPasswordAsync(Guid performingUserKey, Guid userKey) + { + if (performingUserKey.Equals(userKey)) + { + return Attempt.FailWithStatus(UserOperationStatus.SelfPasswordResetNotAllowed, new PasswordChangedModel()); + } + + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + using IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + + ICoreBackOfficeUserManager backOfficeUserManager = serviceScope.ServiceProvider.GetRequiredService(); + + var generatedPassword = backOfficeUserManager.GeneratePassword(); + + Attempt changePasswordAttempt = + await ChangePasswordAsync(performingUserKey, new ChangeUserPasswordModel + { + NewPassword = generatedPassword, + UserKey = userKey, + }); + + scope.Complete(); + + // todo tidy this up + // this should be part of the result of the ChangePasswordAsync() method + // but the model requires NewPassword + // and the passwordChanger does not have a codePath that deals with generating + if (changePasswordAttempt.Success) + { + changePasswordAttempt.Result.ResetPassword = generatedPassword; + } + + return changePasswordAttempt; + } /// @@ -2234,7 +2274,7 @@ internal class UserService : RepositoryService, IUserService results.Add(new NodePermissions { NodeKey = idKeyMap[nodeId], Permissions = permissions }); } - return Attempt.SucceedWithStatus, UserOperationStatus>(UserOperationStatus.UserNotFound, results); + return Attempt.SucceedWithStatus, UserOperationStatus>(UserOperationStatus.Success, results); } /// diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 39d6831316..4c7943c69c 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -123,7 +123,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser /// This is allowed to be null (but would need to be filled in if trying to persist this instance) /// /// - public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null) + public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null) { if (string.IsNullOrWhiteSpace(username)) { @@ -139,8 +139,13 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser user.DisableChangeTracking(); user.UserName = username; user.Email = email; - user.Id = string.Empty; + + if (id is not null) + { + user.Key = id.Value; + } + user.HasIdentity = false; user._culture = culture; user.Name = name; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index bc284b6b8f..9a639926d5 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -140,8 +140,10 @@ public class BackOfficeUserStore : StartContentIds = user.StartContentIds ?? new int[] { }, StartMediaIds = user.StartMediaIds ?? new int[] { }, IsLockedOut = user.IsLockedOut, + Key = user.Key, }; + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); var isTokensPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.LoginTokens)); diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 25002432af..ecffa6b130 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -296,9 +296,9 @@ public class BackOfficeUserManager : UmbracoUserManager