Initial changes to password model

This commit is contained in:
Emma Garland
2021-02-20 19:16:31 +00:00
parent 0fe7ad826d
commit 0a10d3176d
13 changed files with 360 additions and 147 deletions

View File

@@ -1,6 +1,6 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models
namespace Umbraco.Core.Models
{
/// <summary>
/// A model representing the data required to set a member/user password depending on the provider installed.
@@ -20,9 +20,30 @@ namespace Umbraco.Web.Models
public string OldPassword { get; set; }
/// <summary>
/// The id of the user - required to allow changing password without the entire UserSave model
/// The ID of the current user/member requesting the password change
/// For users, required to allow changing password without the entire UserSave model
/// </summary>
[DataMember(Name = "id")]
public int Id { get; set; }
/// <summary>
/// The username of the user/member who is changing the password
/// </summary>
public string CurrentUsername { get; set; }
/// <summary>
/// The ID of the user/member whose password is being changed
/// </summary>
public int SavingUserId { get; set; }
/// <summary>
/// The username of the user/memeber whose password is being changed
/// </summary>
public string SavingUsername { get; set; }
/// <summary>
/// True if the current user has access to change the password for the member/user
/// </summary>
public bool CurrentUserHasSectionAccess { get; set; }
}
}

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models
{
public interface IMemberUserAdapter : IUser
{
IEnumerable<string> AllowedSections { get; }
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models
{
public class MemberUserAdapter : IMemberUserAdapter
{
private readonly IMember _member;
/// <summary>
/// Initializes a new instance of the <see cref="MemberUserAdapter"/> class.
/// Member adaptor to use existing user change password functionality and other shared functions
/// </summary>
/// <param name="member">The member to adapt</param>
public MemberUserAdapter(IMember member)
{
_member = member;
}
// This is the only reason we currently need this adaptor
public IEnumerable<string> AllowedSections => new List<string>()
{
Constants.Applications.Users
};
public UserState UserState { get; }
public string Name { get; set; }
public int SessionTimeout { get; set; }
public int[] StartContentIds { get; set; }
public int[] StartMediaIds { get; set; }
public string Language { get; set; }
public DateTime? InvitedDate { get; set; }
public IEnumerable<IReadOnlyUserGroup> Groups { get; }
public void RemoveGroup(string @group) => throw new NotImplementedException();
public void ClearGroups() => throw new NotImplementedException();
public void AddGroup(IReadOnlyUserGroup @group) => throw new NotImplementedException();
public IProfile ProfileData { get; }
public string Avatar { get; set; }
public string TourData { get; set; }
public T FromUserCache<T>(string cacheKey) where T : class => throw new NotImplementedException();
public void ToUserCache<T>(string cacheKey, T vals) where T : class => throw new NotImplementedException();
public object DeepClone() => throw new NotImplementedException();
public int Id { get; set; }
public Guid Key { get; set; }
public DateTime CreateDate { get; set; }
public DateTime UpdateDate { get; set; }
public DateTime? DeleteDate { get; set; }
public bool HasIdentity { get; }
public void ResetIdentity() => throw new NotImplementedException();
public string Username { get; set; }
public string Email { get; set; }
public DateTime? EmailConfirmedDate { get; set; }
public string RawPasswordValue { get; set; }
public string PasswordConfiguration { get; set; }
public string Comments { get; set; }
public bool IsApproved { get; set; }
public bool IsLockedOut { get; set; }
public DateTime LastLoginDate { get; set; }
public DateTime LastPasswordChangeDate { get; set; }
public DateTime LastLockoutDate { get; set; }
public int FailedPasswordAttempts { get; set; }
public string SecurityStamp { get; set; }
public bool IsDirty() => throw new NotImplementedException();
public bool IsPropertyDirty(string propName) => throw new NotImplementedException();
public IEnumerable<string> GetDirtyProperties() => throw new NotImplementedException();
public void ResetDirtyProperties() => throw new NotImplementedException();
public void DisableChangeTracking() => throw new NotImplementedException();
public void EnableChangeTracking() => throw new NotImplementedException();
public event PropertyChangedEventHandler PropertyChanged;
public bool WasDirty() => throw new NotImplementedException();
public bool WasPropertyDirty(string propertyName) => throw new NotImplementedException();
public void ResetWereDirtyProperties() => throw new NotImplementedException();
public void ResetDirtyProperties(bool rememberDirty) => throw new NotImplementedException();
public IEnumerable<string> GetWereDirtyProperties() => throw new NotImplementedException();
}
}

View File

@@ -263,14 +263,7 @@ namespace Umbraco.Infrastructure.Security
/// </summary>
/// <returns>A generated password</returns>
string GeneratePassword();
/// <summary>
/// Hashes a password for a null user based on the default password hasher
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
string HashPassword(string password);
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code

View File

@@ -100,19 +100,7 @@ namespace Umbraco.Infrastructure.Security
string password = _passwordGenerator.GeneratePassword();
return password;
}
/// <summary>
/// Generates a hashed password based on the default password hasher
/// No existing identity user is required and this does not validate the password
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
public string HashPassword(string password)
{
string hashedPassword = PasswordHasher.HashPassword(null, password);
return hashedPassword;
}
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code

View File

@@ -20,8 +20,8 @@ using Umbraco.Core.Events;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Core.Services;
@@ -33,6 +33,7 @@ using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
using Umbraco.Web;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Mapping;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Models;
@@ -69,11 +70,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
sut.ModelState.AddModelError("key", "Invalid model state");
Mock.Get(umbracoMembersUserManager)
@@ -105,7 +107,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
IBackOfficeSecurity backOfficeSecurity,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
@@ -123,7 +126,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
@@ -143,7 +146,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
IBackOfficeSecurity backOfficeSecurity,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
@@ -161,7 +165,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
@@ -182,7 +186,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
IBackOfficeSecurity backOfficeSecurity,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save);
@@ -194,9 +199,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
.ReturnsAsync(() => IdentityResult.Success);
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(passwordChanger)
.Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny<ChangingPasswordModel>(), umbracoMembersUserManager))
.ReturnsAsync(() => new Attempt<PasswordChangedModel>());
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
@@ -208,7 +213,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
.Returns(() => null)
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
@@ -228,7 +233,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
IBackOfficeSecurity backOfficeSecurity,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
@@ -245,7 +251,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
string reason = "Validation failed";
// act
@@ -267,7 +273,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
IBackOfficeSecurity backOfficeSecurity,
IPasswordChanger<MembersIdentityUser> passwordChanger)
{
// arrange
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
@@ -277,16 +284,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
{
roleName
};
var membersIdentityUser = new MembersIdentityUser();
var membersIdentityUser = new MembersIdentityUser(123);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(() => membersIdentityUser);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(passwordChanger)
.Setup(x => x.ChangePasswordWithIdentityAsync(It.IsAny<ChangingPasswordModel>(), umbracoMembersUserManager))
.ReturnsAsync(() => Attempt.Succeed(new PasswordChangedModel()));
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
@@ -299,7 +306,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor, passwordChanger);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
@@ -309,8 +316,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
Assert.IsNotNull(result.Value);
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.GetRolesAsync(membersIdentityUser));
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName }));
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName }));
Mock.Get(memberService)
.Verify(m => m.Save(It.IsAny<Member>(), true));
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
@@ -325,17 +332,18 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
/// <param name="membersUserManager">Members user manager</param>
/// <param name="dataTypeService">Data type service</param>
/// <param name="backOfficeSecurityAccessor">Back office security accessor</param>
/// <param name="mockPasswordChanger">Password changer class</param>
/// <returns>A member controller for the tests</returns>
private MemberController CreateSut(
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IMemberManager membersUserManager,
IUmbracoUserManager<MembersIdentityUser> membersUserManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IPasswordChanger<MembersIdentityUser> mockPasswordChanger)
{
var mockShortStringHelper = new MockShortStringHelper();
var textService = new Mock<ILocalizedTextService>();
var contentTypeBaseServiceProvider = new Mock<IContentTypeBaseServiceProvider>();
contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny<IContentBase>())).Returns(new ContentType(mockShortStringHelper, 123));
@@ -410,13 +418,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
_mapper,
memberService,
memberTypeService,
membersUserManager,
(IMemberManager) membersUserManager,
dataTypeService,
backOfficeSecurityAccessor,
new ConfigurationEditorJsonSerializer());
new ConfigurationEditorJsonSerializer(),
mockPasswordChanger);
}
/// <summary>
/// Setup all standard member data for test
/// </summary>
@@ -428,10 +436,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
// arrange
MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "test@example.com", "123", "test");
int memberId = 123;
var memberId = 123;
member.Id = memberId;
//TODO: replace with builder for MemberSave and MemberDisplay
// TODO: replace with builder for MemberSave and MemberDisplay
fakeMemberData = new MemberSave()
{
Id = memberId,

View File

@@ -17,6 +17,7 @@ using Umbraco.Core.IO;
using Umbraco.Core.Mapping;
using Umbraco.Core.Media;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
@@ -42,7 +43,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly ContentSettings _contentSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly UmbracoMapper _umbracoMapper;
private readonly IBackOfficeUserManager _backOfficeUserManager;
@@ -50,6 +51,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly ILocalizedTextService _localizedTextService;
private readonly AppCaches _appCaches;
private readonly IShortStringHelper _shortStringHelper;
private readonly IPasswordChanger<BackOfficeIdentityUser> _passwordChanger;
public CurrentUserController(
IMediaFileSystem mediaFileSystem,
@@ -63,13 +65,14 @@ namespace Umbraco.Web.BackOffice.Controllers
ILoggerFactory loggerFactory,
ILocalizedTextService localizedTextService,
AppCaches appCaches,
IShortStringHelper shortStringHelper)
IShortStringHelper shortStringHelper,
IPasswordChanger<BackOfficeIdentityUser> passwordChanger)
{
_mediaFileSystem = mediaFileSystem;
_contentSettings = contentSettings.Value;
_hostingEnvironment = hostingEnvironment;
_imageUrlGenerator = imageUrlGenerator;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backofficeSecurityAccessor;
_userService = userService;
_umbracoMapper = umbracoMapper;
_backOfficeUserManager = backOfficeUserManager;
@@ -77,6 +80,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_localizedTextService = localizedTextService;
_appCaches = appCaches;
_shortStringHelper = shortStringHelper;
_passwordChanger = passwordChanger;
}
@@ -89,7 +93,7 @@ namespace Umbraco.Web.BackOffice.Controllers
public Dictionary<int, string[]> GetPermissions(int[] nodeIds)
{
var permissions = _userService
.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds);
.GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeIds);
var permissionsDictionary = new Dictionary<int, string[]>();
foreach (var nodeId in nodeIds)
@@ -110,7 +114,7 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public bool HasPermission(string permissionToCheck, int nodeId)
{
var p = _userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions();
var p = _userService.GetPermissions(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, nodeId).GetAllPermissions();
if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture)))
{
return true;
@@ -129,15 +133,15 @@ namespace Umbraco.Web.BackOffice.Controllers
if (status == null) throw new ArgumentNullException(nameof(status));
List<UserTourStatus> userTours;
if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace())
if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace())
{
userTours = new List<UserTourStatus> { status };
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours);
_userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours);
_userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
return userTours;
}
userTours = JsonConvert.DeserializeObject<IEnumerable<UserTourStatus>>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList();
userTours = JsonConvert.DeserializeObject<IEnumerable<UserTourStatus>>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData).ToList();
var found = userTours.FirstOrDefault(x => x.Alias == status.Alias);
if (found != null)
{
@@ -145,8 +149,8 @@ namespace Umbraco.Web.BackOffice.Controllers
userTours.Remove(found);
}
userTours.Add(status);
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours);
_userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours);
_userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
return userTours;
}
@@ -156,10 +160,10 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <returns></returns>
public IEnumerable<UserTourStatus> GetUserTours()
{
if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace())
if (_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData.IsNullOrWhiteSpace())
return Enumerable.Empty<UserTourStatus>();
var userTours = JsonConvert.DeserializeObject<IEnumerable<UserTourStatus>>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData);
var userTours = JsonConvert.DeserializeObject<IEnumerable<UserTourStatus>>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData);
return userTours;
}
@@ -175,7 +179,7 @@ namespace Umbraco.Web.BackOffice.Controllers
[AllowAnonymous]
public async Task<ActionResult<UserDetail>> PostSetInvitedUserPassword([FromBody]string newPassword)
{
var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
var user = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
if (user == null) throw new InvalidOperationException("Could not find user");
var result = await _backOfficeUserManager.AddPasswordAsync(user, newPassword);
@@ -190,13 +194,13 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//They've successfully set their password, we can now update their user account to be approved
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true;
_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true;
//They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow;
_userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow;
_userService.Save(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
//now we can return their full object since they are now really logged into the back office
var userDisplay = _umbracoMapper.Map<UserDetail>(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
var userDisplay = _umbracoMapper.Map<UserDetail>(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser);
userDisplay.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds();
return userDisplay;
@@ -206,31 +210,38 @@ namespace Umbraco.Web.BackOffice.Controllers
public IActionResult PostSetAvatar(IList<IFormFile> file)
{
//borrow the logic from the user controller
return UsersController.PostSetAvatarInternal(file, _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>
/// Changes the users password
/// </summary>
/// <param name="data"></param>
/// <param name="changingPasswordModel">The changing password model</param>
/// <returns>
/// If the password is being reset it will return the newly reset password, otherwise will return an empty value
/// </returns>
public async Task<ActionResult<ModelWithNotifications<string>>> PostChangePassword(ChangingPasswordModel data)
public async Task<ActionResult<ModelWithNotifications<string>>> PostChangePassword(ChangingPasswordModel changingPasswordModel)
{
// TODO: Why don't we inject this? Then we can just inject a logger
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, data, _backOfficeUserManager);
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users);
// the current user has access to change their password
changingPasswordModel.CurrentUserHasSectionAccess = true;
changingPasswordModel.CurrentUsername = currentUser.Username;
changingPasswordModel.SavingUsername = currentUser.Username;
changingPasswordModel.SavingUserId = currentUser.Id;
Attempt<PasswordChangedModel> passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager);
if (passwordChangeResult.Success)
{
//even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword
// even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword
var result = new ModelWithNotifications<string>(passwordChangeResult.Result.ResetPassword);
result.AddSuccessNotification(_localizedTextService.Localize("user/password"), _localizedTextService.Localize("user/passwordChanged"));
return result;
}
foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
foreach (string memberName in passwordChangeResult.Result.ChangeError.MemberNames)
{
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage);
}
@@ -243,7 +254,7 @@ namespace Umbraco.Web.BackOffice.Controllers
[ValidateAngularAntiForgeryToken]
public async Task<Dictionary<string, string>> GetCurrentUserLinkedLogins()
{
var identityUser = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
var identityUser = await _backOfficeUserManager.FindByIdAsync(_backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
// deduplicate in case there are duplicates (there shouldn't be now since we have a unique constraint on the external logins
// but there didn't used to be)

View File

@@ -28,11 +28,13 @@ using Umbraco.Infrastructure.Security;
using Umbraco.Infrastructure.Services.Implement;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.ModelBinders;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Authorization;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.BackOffice.Controllers
@@ -56,6 +58,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IJsonSerializer _jsonSerializer;
private readonly IShortStringHelper _shortStringHelper;
private readonly IPasswordChanger<MembersIdentityUser> _passwordChanger;
/// <summary>
/// Initializes a new instance of the <see cref="MemberController"/> class.
@@ -73,6 +76,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <param name="dataTypeService">The data-type service</param>
/// <param name="backOfficeSecurityAccessor">The back office security accessor</param>
/// <param name="jsonSerializer">The JSON serializer</param>
/// <param name="passwordChanger">The password changer</param>
public MemberController(
ICultureDictionary cultureDictionary,
ILoggerFactory loggerFactory,
@@ -86,7 +90,8 @@ namespace Umbraco.Web.BackOffice.Controllers
IMemberManager memberManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IJsonSerializer jsonSerializer)
IJsonSerializer jsonSerializer,
IPasswordChanger<MembersIdentityUser> passwordChanger)
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
{
_propertyEditors = propertyEditors;
@@ -99,6 +104,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_jsonSerializer = jsonSerializer;
_shortStringHelper = shortStringHelper;
_passwordChanger = passwordChanger;
}
/// <summary>
@@ -390,7 +396,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//TODO: do we need to resave the key?
//contentItem.PersistedContent.Key = contentItem.Key;
// contentItem.PersistedContent.Key = contentItem.Key;
// now the member has been saved via identity, resave the member with mapped content properties
_memberService.Save(member);
@@ -450,7 +456,7 @@ namespace Umbraco.Web.BackOffice.Controllers
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
if (identityMember == null)
{
return new ValidationErrorResult("Member was not found");
return new ValidationErrorResult("Identity member was not found");
}
if (contentItem.Password != null)
@@ -461,14 +467,52 @@ namespace Umbraco.Web.BackOffice.Controllers
return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage());
}
string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword);
identityMember.PasswordHash = newPassword;
contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash;
if (identityMember.LastPasswordChangeDateUtc != null)
Attempt<int> intId = identityMember.Id.TryConvertTo<int>();
if (intId.Success == false)
{
contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow;
return new ValidationErrorResult("Member ID was not valid");
}
IMember foundMember = _memberService.GetById(intId.Result);
if (foundMember == null)
{
return new ValidationErrorResult("Member was not found");
}
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
var changingPasswordModel = new ChangingPasswordModel
{
Id = intId.Result,
OldPassword = contentItem.Password.OldPassword,
NewPassword = contentItem.Password.NewPassword,
CurrentUsername = currentUser.Username,
SavingUserId = foundMember.Id,
SavingUsername = foundMember.Username,
CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Members)
};
Attempt<PasswordChangedModel> passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager);
if (passwordChangeResult.Success)
{
contentItem.PersistedContent.RawPasswordValue = passwordChangeResult.Result.ResetPassword;
if (identityMember.LastPasswordChangeDateUtc != null)
{
contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow;
}
identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate;
}
if (passwordChangeResult.Result.ChangeError?.MemberNames != null)
{
foreach (string memberName in passwordChangeResult.Result.ChangeError?.MemberNames)
{
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError?.ErrorMessage ?? string.Empty);
}
}
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);

View File

@@ -23,6 +23,7 @@ using Umbraco.Core.Mail;
using Umbraco.Core.Mapping;
using Umbraco.Core.Media;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence;
using Umbraco.Core.Security;
@@ -56,7 +57,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly SecuritySettings _securitySettings;
private readonly IEmailSender _emailSender;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly AppCaches _appCaches;
private readonly IShortStringHelper _shortStringHelper;
private readonly IUserService _userService;
@@ -68,6 +69,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly LinkGenerator _linkGenerator;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper;
private readonly IPasswordChanger<BackOfficeIdentityUser> _passwordChanger;
private readonly ILogger<UsersController> _logger;
public UsersController(
@@ -89,7 +91,8 @@ namespace Umbraco.Web.BackOffice.Controllers
ILoggerFactory loggerFactory,
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalLogins,
UserEditorAuthorizationHelper userEditorAuthorizationHelper)
UserEditorAuthorizationHelper userEditorAuthorizationHelper,
IPasswordChanger<BackOfficeIdentityUser> passwordChanger)
{
_mediaFileSystem = mediaFileSystem;
_contentSettings = contentSettings.Value;
@@ -98,7 +101,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_imageUrlGenerator = imageUrlGenerator;
_securitySettings = securitySettings.Value;
_emailSender = emailSender;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backofficeSecurityAccessor;
_appCaches = appCaches;
_shortStringHelper = shortStringHelper;
_userService = userService;
@@ -110,6 +113,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_linkGenerator = linkGenerator;
_externalLogins = externalLogins;
_userEditorAuthorizationHelper = userEditorAuthorizationHelper;
_passwordChanger = passwordChanger;
_logger = _loggerFactory.CreateLogger<UsersController>();
}
@@ -119,7 +123,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <returns></returns>
public ActionResult<string[]> GetCurrentUserAvatarUrls()
{
var urls = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator);
var urls = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator);
if (urls == null)
return new ValidationErrorResult("Could not access Gravatar endpoint");
@@ -285,7 +289,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var hideDisabledUsers = _securitySettings.HideDisabledUsersInBackOffice;
var excludeUserGroups = new string[0];
var isAdmin = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin();
var isAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsAdmin();
if (isAdmin == false)
{
//this user is not an admin so in that case we need to exclude all admin users
@@ -294,7 +298,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var filterQuery = _sqlContext.Query<IUser>();
if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper())
if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsSuper())
{
// only super can see super - but don't use IsSuper, cannot be mapped to SQL
//filterQuery.Where(x => !x.IsSuper());
@@ -359,7 +363,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//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);
var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
return Unauthorized(canSaveUser.Result);
@@ -448,7 +452,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//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, user, null, null, userSave.UserGroups);
var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized);
@@ -511,7 +515,7 @@ namespace Umbraco.Web.BackOffice.Controllers
{
//send the email
await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message);
await SendUserInviteEmailAsync(display, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message);
}
@@ -605,7 +609,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return NotFound();
//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, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups);
var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups);
if (canSaveUser == false)
{
return Unauthorized(canSaveUser.Result);
@@ -665,7 +669,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var display = _umbracoMapper.Map<UserDisplay>(user);
// determine if the user has changed their own language;
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
var userHasChangedOwnLanguage =
user.Id == currentUser.Id && currentUser.Language != user.Language;
@@ -691,21 +695,25 @@ namespace Umbraco.Web.BackOffice.Controllers
return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary()));
}
var intId = changingPasswordModel.Id.TryConvertTo<int>();
Attempt<int> intId = changingPasswordModel.Id.TryConvertTo<int>();
if (intId.Success == false)
{
return NotFound();
}
var found = _userService.GetUserById(intId.Result);
IUser found = _userService.GetUserById(intId.Result);
if (found == null)
{
return NotFound();
}
// TODO: Why don't we inject this? Then we can just inject a logger
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _userManager);
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
changingPasswordModel.CurrentUserHasSectionAccess = currentUser.HasSectionAccess(Constants.Applications.Users);
changingPasswordModel.CurrentUsername = currentUser.Username;
changingPasswordModel.SavingUserId = found.Id;
changingPasswordModel.SavingUsername = found.Username;
Attempt<PasswordChangedModel> passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager);
if (passwordChangeResult.Success)
{
@@ -714,7 +722,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return result;
}
foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
foreach (string memberName in passwordChangeResult.Result.ChangeError.MemberNames)
{
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage);
}
@@ -730,7 +738,7 @@ namespace Umbraco.Web.BackOffice.Controllers
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostDisableUsers([FromQuery]int[] userIds)
{
var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId();
var tryGetCurrentUserId = _backOfficeSecurityAccessor.BackOfficeSecurity.GetUserId();
if (tryGetCurrentUserId && userIds.Contains(tryGetCurrentUserId.Result))
{
return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot disable itself");

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Umbraco.Core.DependencyInjection;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Infrastructure.DependencyInjection;
@@ -81,6 +82,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
builder.Services.AddUnique<PreviewAuthenticationMiddleware>();
builder.Services.AddUnique<BackOfficeExternalLoginProviderErrorMiddleware>();
builder.Services.AddUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
builder.Services.AddUnique<IPasswordChanger<UmbracoIdentityUser>, PasswordChanger<UmbracoIdentityUser>>();
return builder;
}

View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Models;
namespace Umbraco.Web.BackOffice.Security
{
public interface IPasswordChanger<TUser> where TUser : UmbracoIdentityUser
{
public Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
ChangingPasswordModel passwordModel,
IUmbracoUserManager<TUser> userMgr);
}
}

View File

@@ -1,22 +1,31 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Models;
using IUser = Umbraco.Core.Models.Membership.IUser;
namespace Umbraco.Web.BackOffice.Security
{
internal class PasswordChanger
/// <summary>
/// Changes the password for an identity user
/// </summary>
internal class PasswordChanger<TUser> : IPasswordChanger<TUser>
where TUser : UmbracoIdentityUser
{
private readonly ILogger<PasswordChanger> _logger;
private readonly ILogger<PasswordChanger<TUser>> _logger;
public PasswordChanger(ILogger<PasswordChanger> logger)
/// <summary>
/// Initializes a new instance of the <see cref="PasswordChanger"/> class.
/// Password changing functionality
/// </summary>
/// <param name="logger">Logger for this class</param>
public PasswordChanger(ILogger<PasswordChanger<TUser>> logger)
{
_logger = logger;
}
@@ -24,55 +33,60 @@ namespace Umbraco.Web.BackOffice.Security
/// <summary>
/// Changes the password for a user based on the many different rules and config options
/// </summary>
/// <param name="currentUser">The user performing the password save action</param>
/// <param name="savingUser">The user who's password is being changed</param>
/// <param name="passwordModel"></param>
/// <param name="userMgr"></param>
/// <returns></returns>
/// <param name="changingPasswordModel">The changing password model</param>
/// <param name="userMgr">The identity manager to use to update the password</param>
/// Create an adapter to pass through everything - adapting the member into a user for this functionality
/// <returns>The outcome of the password changed model</returns>
public async Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
IUser currentUser,
IUser savingUser,
ChangingPasswordModel passwordModel,
IBackOfficeUserManager userMgr)
ChangingPasswordModel changingPasswordModel,
IUmbracoUserManager<TUser> userMgr)
{
if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
if (userMgr == null) throw new ArgumentNullException(nameof(userMgr));
if (changingPasswordModel == null)
{
throw new ArgumentNullException(nameof(changingPasswordModel));
}
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
if (userMgr == null)
{
throw new ArgumentNullException(nameof(userMgr));
}
if (changingPasswordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id.ToString());
if (backOfficeIdentityUser == null)
TUser identityUser = await userMgr.FindByIdAsync(changingPasswordModel.SavingUserId.ToString());
if (identityUser == null)
{
//this really shouldn't ever happen... but just in case
// this really shouldn't ever happen... but just in case
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) });
}
//Are we just changing another user's password?
if (passwordModel.OldPassword.IsNullOrWhiteSpace())
// Are we just changing another user's password?
if (changingPasswordModel.OldPassword.IsNullOrWhiteSpace())
{
//if it's the current user, the current user cannot reset their own password
if (currentUser.Username == savingUser.Username)
// if it's the current user, the current user cannot reset their own password
// For members, this should not happen
if (changingPasswordModel.CurrentUsername == changingPasswordModel.SavingUsername)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not allowed", new[] { "value" }) });
}
//if the current user has access to reset/manually change the password
if (currentUser.HasSectionAccess(Umbraco.Core.Constants.Applications.Users) == false)
// if the current user has access to reset/manually change the password
if (changingPasswordModel.CurrentUserHasSectionAccess)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "value" }) });
}
//ok, we should be able to reset it
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser);
// ok, we should be able to reset it
string resetToken = await userMgr.GeneratePasswordResetTokenAsync(identityUser);
var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id.ToString(), resetToken, passwordModel.NewPassword);
IdentityResult resetResult = await userMgr.ChangePasswordWithResetAsync(changingPasswordModel.SavingUserId.ToString(), resetToken, changingPasswordModel.NewPassword);
if (resetResult.Succeeded == false)
{
var errors = resetResult.Errors.ToErrorMessage();
string errors = resetResult.Errors.ToErrorMessage();
_logger.LogWarning("Could not reset user password {PasswordErrors}", errors);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "value" }) });
}
@@ -80,24 +94,25 @@ namespace Umbraco.Web.BackOffice.Security
return Attempt.Succeed(new PasswordChangedModel());
}
//is the old password correct?
var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword);
// is the old password correct?
bool validateResult = await userMgr.CheckPasswordAsync(identityUser, changingPasswordModel.OldPassword);
if (validateResult == false)
{
//no, fail with an error message for "oldPassword"
// no, fail with an error message for "oldPassword"
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) });
}
//can we change to the new password?
var changeResult = await userMgr.ChangePasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword, passwordModel.NewPassword);
// can we change to the new password?
IdentityResult changeResult = await userMgr.ChangePasswordAsync(identityUser, changingPasswordModel.OldPassword, changingPasswordModel.NewPassword);
if (changeResult.Succeeded == false)
{
//no, fail with error messages for "password"
var errors = changeResult.Errors.ToErrorMessage();
// no, fail with error messages for "password"
string errors = changeResult.Errors.ToErrorMessage();
_logger.LogWarning("Could not change user password {PasswordErrors}", errors);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) });
}
return Attempt.Succeed(new PasswordChangedModel());
}
}
}