Netcore: Fixes issues with user invites (#9616)

* AB9629
Fixes issues with user invites
- Issue with the generated link in the invite email
- Allow anonymous access to CurrentUserController.PostSetInvitedUserPassword, as it is used by users not logged in
- Allow anonymous access to AuthenticationController.GetPasswordConfig, as this is used to set a password for newly invited users, before they login

* Fix issues with invite flow

* Fix minor typos

* Fixed issue with validation response and remove/change avatar

* Fix issue with disable users, after all enums are handled like strings

* Fix tests

* Fix other validation issue

* Fix yet another validation issue

Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
This commit is contained in:
Bjarke Berg
2021-01-12 16:15:19 +01:00
committed by GitHub
parent b15046ccf6
commit fe016dd103
12 changed files with 162 additions and 144 deletions

View File

@@ -33,7 +33,6 @@ using Umbraco.Web.Common.Filters;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.BackOffice.Controllers
{
@@ -117,11 +116,15 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog
/// </summary>
/// <returns></returns>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[AllowAnonymous] // Needed for users that are invited when they use the link from the mail they are not authorized
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
public IDictionary<string, object> GetPasswordConfig(int userId)
{
return _passwordConfiguration.GetConfiguration(userId != _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
Attempt<int> currentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId();
return _passwordConfiguration.GetConfiguration(
currentUserId.Success
? currentUserId.Result != userId
: true);
}
/// <summary>

View File

@@ -16,12 +16,14 @@ using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Mapping;
using Umbraco.Core.Media;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Authorization;
using Umbraco.Web.Common.Exceptions;
@@ -170,7 +172,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <remarks>
/// This only works when the user is logged in (partially)
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController
[AllowAnonymous]
public async Task<UserDetail> PostSetInvitedUserPassword([FromBody]string newPassword)
{
var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
@@ -201,10 +203,10 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[AppendUserModifiedHeader]
public IActionResult PostSetAvatar(IList<IFormFile> files)
public IActionResult PostSetAvatar(IList<IFormFile> file)
{
//borrow the logic from the user controller
return UsersController.PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0));
return UsersController.PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0));
}
/// <summary>
@@ -214,7 +216,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <returns>
/// If the password is being reset it will return the newly reset password, otherwise will return an empty value
/// </returns>
public async Task<ModelWithNotifications<string>> PostChangePassword(ChangingPasswordModel data)
public async Task<ActionResult<ModelWithNotifications<string>>> PostChangePassword(ChangingPasswordModel data)
{
// TODO: Why don't we inject this? Then we can just inject a logger
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
@@ -233,7 +235,7 @@ namespace Umbraco.Web.BackOffice.Controllers
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage);
}
throw HttpResponseException.CreateValidationErrorResponse(ModelState);
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
// TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController

View File

@@ -14,7 +14,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
@@ -39,9 +38,6 @@ using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Editors;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Constants = Umbraco.Core.Constants;
using IUser = Umbraco.Core.Models.Membership.IUser;
using Task = System.Threading.Tasks.Task;
namespace Umbraco.Web.BackOffice.Controllers
{
@@ -133,9 +129,9 @@ namespace Umbraco.Web.BackOffice.Controllers
[AppendUserModifiedHeader("id")]
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostSetAvatar(int id, IList<IFormFile> files)
public IActionResult PostSetAvatar(int id, IList<IFormFile> file)
{
return PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id);
return PostSetAvatarInternal(file, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id);
}
internal static IActionResult PostSetAvatarInternal(IList<IFormFile> files, IUserService userService, IAppCache cache, IMediaFileSystem mediaFileSystem, IShortStringHelper shortStringHelper, ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, int id)
@@ -337,13 +333,13 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
public async Task<UserDisplay> PostCreateUser(UserInvite userSave)
public async Task<ActionResult<UserDisplay>> PostCreateUser(UserInvite userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
if (ModelState.IsValid == false)
{
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
if (_securitySettings.UsernameIsEmail)
@@ -358,6 +354,11 @@ namespace Umbraco.Web.BackOffice.Controllers
}
CheckUniqueEmail(userSave.Email, null);
if (ModelState.IsValid == false)
{
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
//Perform authorization here to see if the current user can actually save this user with the info being requested
var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups);
if (canSaveUser == false)
@@ -418,11 +419,6 @@ namespace Umbraco.Web.BackOffice.Controllers
if (userSave.Message.IsNullOrWhiteSpace())
ModelState.AddModelError("Message", "Message cannot be empty");
if (ModelState.IsValid == false)
{
return new ValidationErrorResult(ModelState);
}
IUser user;
if (_securitySettings.UsernameIsEmail)
{
@@ -436,6 +432,11 @@ namespace Umbraco.Web.BackOffice.Controllers
}
user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue);
if (ModelState.IsValid == false)
{
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
if (!EmailSender.CanSendRequiredEmail(_globalSettings) && !_userManager.HasSendingUserInviteEventHandler)
{
return new ValidationErrorResult("No Email server is configured");
@@ -519,7 +520,6 @@ namespace Umbraco.Web.BackOffice.Controllers
if (user != null && (extraCheck == null || extraCheck(user)))
{
ModelState.AddModelError("Email", "A user with the email already exists");
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
}
return user;
}
@@ -548,9 +548,12 @@ namespace Umbraco.Web.BackOffice.Controllers
token.ToUrlBase64());
// Get an mvc helper to get the URL
var action = _linkGenerator.GetPathByAction("VerifyInvite", "BackOffice", new
var action = _linkGenerator.GetPathByAction(
nameof(BackOfficeController.VerifyInvite),
ControllerExtensions.GetControllerName<BackOfficeController>(),
new
{
area = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment),
area = Constants.Web.Mvc.BackOfficeArea,
invite = inviteToken
});