PR comments updated. Reorganised logic. Removed unused functionality. Started to reorganise update and save roles functionality.
This commit is contained in:
@@ -9,7 +9,8 @@ namespace Umbraco.Core.Models.Mapping
|
||||
/// <inheritdoc />
|
||||
public void DefineMaps(UmbracoMapper mapper) => mapper.Define<MemberSave, IMember>(Map);
|
||||
|
||||
// mappers
|
||||
//TODO: put this here instead of a new mapper definition (like user). Can move
|
||||
|
||||
private static void Map(MemberSave source, IMember target, MapperContext context)
|
||||
{
|
||||
// TODO: ensure all properties are mapped as required
|
||||
@@ -18,8 +19,8 @@ namespace Umbraco.Core.Models.Mapping
|
||||
target.Email = source.Email;
|
||||
target.Key = source.Key;
|
||||
target.Username = source.Username;
|
||||
target.Id = (int)(long)source.Id;
|
||||
target.Comments = source.Comments;
|
||||
//TODO: add groups as required
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// The result returned from the IMembersUserPasswordChecker
|
||||
/// </summary>
|
||||
public enum MembersUserPasswordCheckerResult
|
||||
{
|
||||
ValidCredentials,
|
||||
InvalidCredentials,
|
||||
FallbackToDefaultChecker
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,23 @@ namespace Umbraco.Infrastructure.Security
|
||||
/// <returns>A generated password</returns>
|
||||
string GeneratePassword();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a hashed 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 GeneratePassword(string password);
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate the password without an identity user
|
||||
/// Validation code is based on the default ValidatePasswordAsync code
|
||||
/// Should return <see cref="IdentityResult.Success"/> if validation is successful
|
||||
/// </summary>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
|
||||
|
||||
Task<IdentityResult> ValidatePasswordAsync(string password);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an email confirmation token for the specified user.
|
||||
/// </summary>
|
||||
|
||||
@@ -40,8 +40,6 @@ namespace Umbraco.Infrastructure.Security
|
||||
user.UserName = username;
|
||||
user.Email = email;
|
||||
user.MemberTypeAlias = memberTypeAlias;
|
||||
// TODO: confirm if should be approved
|
||||
user.IsApproved = true;
|
||||
user.Id = null;
|
||||
user.HasIdentity = false;
|
||||
user._name = name;
|
||||
|
||||
@@ -65,21 +65,18 @@ namespace Umbraco.Infrastructure.Security
|
||||
}
|
||||
|
||||
// create member
|
||||
// TODO: are we keeping this method, e.g. the Member Service?
|
||||
// The user service creates it directly, but this way we get the member type by alias first
|
||||
// TODO: are we keeping this method? The user service creates the member directly
|
||||
// but this way we get the member type by alias first
|
||||
IMember memberEntity = _memberService.CreateMember(
|
||||
user.UserName,
|
||||
user.Email,
|
||||
user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name,
|
||||
user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
|
||||
|
||||
// [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
|
||||
|
||||
UpdateMemberProperties(memberEntity, user);
|
||||
|
||||
// TODO: do we want to accept empty passwords here - if third-party for example?
|
||||
// In other method if so?
|
||||
// create the member
|
||||
_memberService.Save(memberEntity);
|
||||
|
||||
if (!memberEntity.HasIdentity)
|
||||
@@ -90,7 +87,9 @@ namespace Umbraco.Infrastructure.Security
|
||||
// re-assign id
|
||||
user.Id = UserIdToString(memberEntity.Id);
|
||||
|
||||
//TODO: confirm re externallogins implementation
|
||||
// [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
// var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
|
||||
// TODO: confirm re externallogins implementation
|
||||
//if (isLoginsPropertyDirty)
|
||||
//{
|
||||
// _externalLoginService.Save(
|
||||
@@ -101,9 +100,9 @@ namespace Umbraco.Infrastructure.Security
|
||||
// x.UserData)));
|
||||
//}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
|
||||
// TODO: confirm re roles implementations
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -230,7 +229,7 @@ namespace Umbraco.Infrastructure.Security
|
||||
return string.IsNullOrEmpty(user.PasswordHash) == false;
|
||||
}
|
||||
|
||||
return result;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -111,6 +112,54 @@ namespace Umbraco.Core.Security
|
||||
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 GeneratePassword(string password)
|
||||
{
|
||||
IPasswordHasher<TUser> passwordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
|
||||
|
||||
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
|
||||
/// Should return <see cref="IdentityResult.Success"/> if validation is successful
|
||||
/// </summary>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
|
||||
public async Task<IdentityResult> ValidatePasswordAsync(string password)
|
||||
{
|
||||
var errors = new List<IdentityError>();
|
||||
var isValid = true;
|
||||
foreach (IPasswordValidator<TUser> v in PasswordValidators)
|
||||
{
|
||||
IdentityResult result = await v.ValidateAsync(this, null, password);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
if (result.Errors.Any())
|
||||
{
|
||||
errors.AddRange(result.Errors);
|
||||
}
|
||||
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
Logger.LogWarning(14, "Password validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code)));
|
||||
return IdentityResult.Failed(errors.ToArray());
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> CheckPasswordAsync(TUser user, string password)
|
||||
{
|
||||
|
||||
@@ -196,6 +196,8 @@ namespace Umbraco.Tests.Integration.Testing
|
||||
builder.AddRuntimeMinifier();
|
||||
builder.AddBackOffice();
|
||||
builder.AddBackOfficeIdentity();
|
||||
builder.AddMembersIdentity();
|
||||
|
||||
|
||||
services.AddMvc();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using NUnit.Framework;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Infrastructure.Security;
|
||||
using Umbraco.Tests.Integration.Testing;
|
||||
using Umbraco.Web.BackOffice.Extensions;
|
||||
|
||||
namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice
|
||||
{
|
||||
|
||||
@@ -237,16 +237,6 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
[MemberSaveValidation]
|
||||
public async Task<ActionResult<MemberDisplay>> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
|
||||
{
|
||||
if (contentItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contentItem));
|
||||
}
|
||||
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
|
||||
}
|
||||
|
||||
// If we've reached here it means:
|
||||
// * Our model has been bound
|
||||
// * and validated
|
||||
@@ -257,7 +247,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
// map the properties to the persisted entity
|
||||
MapPropertyValues(contentItem);
|
||||
|
||||
ValidateMemberData(contentItem);
|
||||
await ValidateMemberDataAsync(contentItem);
|
||||
|
||||
// Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
|
||||
if (ModelState.IsValid == false)
|
||||
@@ -266,23 +256,14 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
forDisplay.Errors = ModelState.ToErrorDictionary();
|
||||
throw HttpResponseException.CreateValidationErrorResponse(forDisplay);
|
||||
}
|
||||
|
||||
// We're gonna look up the current roles now because the below code can cause
|
||||
// events to be raised and developers could be manually adding roles to members in
|
||||
// their handlers. If we don't look this up now there's a chance we'll just end up
|
||||
// removing the roles they've assigned.
|
||||
IEnumerable<string> currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
|
||||
|
||||
// find the ones to remove and remove them
|
||||
IEnumerable<string> roles = currentRoles.ToList();
|
||||
string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray();
|
||||
|
||||
|
||||
// Depending on the action we need to first do a create or update using the membership manager
|
||||
// this ensures that passwords are formatted correctly and also performs the validation on the provider itself.
|
||||
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
await UpdateMemberDataAsync(contentItem);
|
||||
await UpdateMemberAsync(contentItem);
|
||||
break;
|
||||
case ContentSaveAction.SaveNew:
|
||||
await CreateMemberAsync(contentItem);
|
||||
@@ -295,22 +276,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
// TODO: There's 3 things saved here and we should do this all in one transaction,
|
||||
// which we can do here by wrapping in a scope
|
||||
// but it would be nicer to have this taken care of within the Save method itself
|
||||
|
||||
// Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// if we are changing the username, it must be persisted before looking up the member roles).
|
||||
if (rolesToRemove.Any())
|
||||
{
|
||||
_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove);
|
||||
}
|
||||
|
||||
// find the ones to add and add them
|
||||
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
|
||||
if (toAdd.Any())
|
||||
{
|
||||
// add the ones submitted
|
||||
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
|
||||
}
|
||||
|
||||
|
||||
// return the updated model
|
||||
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
|
||||
@@ -365,7 +331,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="contentItem">Member content data</param>
|
||||
/// <returns>The identity result of the created member</returns>
|
||||
private async Task<IdentityResult> CreateMemberAsync(MemberSave contentItem)
|
||||
private async Task CreateMemberAsync(MemberSave contentItem)
|
||||
{
|
||||
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
if (memberType == null)
|
||||
@@ -373,25 +339,13 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
}
|
||||
|
||||
// Create the member with the MemberManager
|
||||
var identityMember = MembersIdentityUser.CreateNew(
|
||||
contentItem.Username,
|
||||
contentItem.Email,
|
||||
memberType.Alias,
|
||||
contentItem.Name);
|
||||
|
||||
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
// TODO: should we show the password rules?
|
||||
Task<bool> isPasswordValid = _memberManager.CheckPasswordAsync(identityMember, contentItem.Password.NewPassword);
|
||||
if (isPasswordValid.Result == false)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult($"Invalid password", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: may not need to add password like this
|
||||
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
|
||||
|
||||
if (created.Succeeded == false)
|
||||
@@ -402,19 +356,17 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
// now re-look the member back up which will now exist
|
||||
IMember member = _memberService.GetByEmail(contentItem.Email);
|
||||
|
||||
member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
// TODO: should this be removed since we've moved passwords out?
|
||||
|
||||
member.RawPasswordValue = identityMember.PasswordHash;
|
||||
member.Comments = contentItem.Comments;
|
||||
|
||||
// since the back office user is creating this member, they will be set to approved
|
||||
member.IsApproved = true;
|
||||
var creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
member.CreatorId = creatorId;
|
||||
|
||||
// map the save info over onto the user
|
||||
member = _umbracoMapper.Map(contentItem, member);
|
||||
member = _umbracoMapper.Map<MemberSave, IMember>(contentItem, member);
|
||||
|
||||
// now the member has been saved via identity, resave the member with mapped content properties
|
||||
_memberService.Save(member);
|
||||
contentItem.PersistedContent = member;
|
||||
return created;
|
||||
|
||||
AddOrUpdateRoles(contentItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -422,20 +374,8 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
|
||||
/// </summary>
|
||||
/// <param name="contentItem">The member to save</param>
|
||||
private async Task UpdateMemberDataAsync(MemberSave contentItem)
|
||||
private async Task UpdateMemberAsync(MemberSave contentItem)
|
||||
{
|
||||
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
|
||||
if (identityMember == null)
|
||||
{
|
||||
}
|
||||
|
||||
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);
|
||||
|
||||
if (updatedResult.Succeeded == false)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse(updatedResult.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
|
||||
// If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
|
||||
@@ -476,21 +416,59 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
ModelState.AddModelError("custom", "An admin cannot lock a user");
|
||||
}
|
||||
|
||||
// no password changes then exit ?
|
||||
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
|
||||
if (identityMember == null)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse("Member was not found");
|
||||
}
|
||||
|
||||
if (contentItem.Password != null)
|
||||
{
|
||||
// TODO: set the password using Identity core
|
||||
contentItem.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword();
|
||||
IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
|
||||
if (validatePassword.Succeeded == false)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse(validatePassword.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
string newPassword = _memberManager.GeneratePassword(contentItem.Password.NewPassword);
|
||||
identityMember.PasswordHash = newPassword;
|
||||
}
|
||||
|
||||
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);
|
||||
|
||||
if (updatedResult.Succeeded == false)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse(updatedResult.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash;
|
||||
|
||||
_memberService.Save(contentItem.PersistedContent);
|
||||
|
||||
AddOrUpdateRoles(contentItem);
|
||||
}
|
||||
|
||||
private void ValidateMemberData(MemberSave contentItem)
|
||||
private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
|
||||
{
|
||||
if (contentItem.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Invalid user name", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
// TODO: this currently stops the user interacting with the client-side when invalid
|
||||
IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
|
||||
if (!validPassword.Succeeded)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Invalid password: " + MapErrors(validPassword.Errors), new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
IMember byUsername = _memberService.GetByUsername(contentItem.Username);
|
||||
@@ -499,6 +477,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Username is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
IMember byEmail = _memberService.GetByEmail(contentItem.Email);
|
||||
@@ -507,22 +486,57 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Email address is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string MapErrors(List<IdentityResult> result)
|
||||
private string MapErrors(IEnumerable<IdentityError> result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
IEnumerable<IdentityResult> errors = result.Where(x => x.Succeeded == false);
|
||||
|
||||
foreach (IdentityResult error in errors)
|
||||
IEnumerable<IdentityError> identityErrors = result.ToList();
|
||||
foreach (IdentityError error in identityErrors)
|
||||
{
|
||||
sb.AppendLine(error.Errors.ToErrorMessage());
|
||||
string errorString = $"{error.Description}";
|
||||
sb.AppendLine(errorString);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: refactor using identity roles
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
private void AddOrUpdateRoles(MemberSave contentItem)
|
||||
{
|
||||
// We're gonna look up the current roles now because the below code can cause
|
||||
// events to be raised and developers could be manually adding roles to members in
|
||||
// their handlers. If we don't look this up now there's a chance we'll just end up
|
||||
// removing the roles they've assigned.
|
||||
IEnumerable<string> currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
|
||||
|
||||
// find the ones to remove and remove them
|
||||
IEnumerable<string> roles = currentRoles.ToList();
|
||||
string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray();
|
||||
|
||||
// Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// if we are changing the username, it must be persisted before looking up the member roles).
|
||||
if (rolesToRemove.Any())
|
||||
{
|
||||
_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove);
|
||||
}
|
||||
|
||||
// find the ones to add and add them
|
||||
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
|
||||
if (toAdd.Any())
|
||||
{
|
||||
// add the ones submitted
|
||||
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes a member
|
||||
/// </summary>
|
||||
|
||||
@@ -2,20 +2,19 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
namespace Umbraco.Web.BackOffice.Extensions
|
||||
{
|
||||
public static class UmbracoMemberIdentityBuilderExtensions
|
||||
public static class MemberIdentityBuilderExtensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="UserManager{TUser}"/> for the <seealso cref="UserType"/>.
|
||||
/// Adds a <see cref="UserManager{TUser}"/> for the <seealso cref="MembersIdentityUser"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TUserManager">The type of the user manager to add.</typeparam>
|
||||
/// <typeparam name="TInterface"></typeparam>
|
||||
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
|
||||
public static IdentityBuilder AddUserManager<TInterface, TUserManager>(this IdentityBuilder identityBuilder) where TUserManager : UserManager<MembersIdentityUser>, TInterface
|
||||
public static IdentityBuilder AddUserManager<TInterface, TUserManager>(this IdentityBuilder identityBuilder)
|
||||
where TUserManager : UserManager<MembersIdentityUser>, TInterface
|
||||
{
|
||||
identityBuilder.AddUserManager<TUserManager>();
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
|
||||
return identityBuilder;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Umbraco.Infrastructure.Security;
|
||||
using Umbraco.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
namespace Umbraco.Web.BackOffice.Extensions
|
||||
{
|
||||
public static class MembersUserServiceCollectionExtensions
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Core.DependencyInjection;
|
||||
using Umbraco.Web.BackOffice.Extensions;
|
||||
using Umbraco.Web.BackOffice.Filters;
|
||||
using Umbraco.Web.BackOffice.Security;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models;
|
||||
@@ -93,14 +93,11 @@ namespace Umbraco.Web.BackOffice.Mapping
|
||||
target.Path = $"-1,{source.Id}";
|
||||
target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key);
|
||||
}
|
||||
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context)
|
||||
{
|
||||
target.Properties = context.MapEnumerable<IProperty, ContentPropertyDto>(source.Properties);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -40,102 +41,6 @@ namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
|
||||
/// </summary>
|
||||
/// <param name="user">The user</param>
|
||||
/// <returns>True if the user is locked out, else false</returns>
|
||||
/// <remarks>
|
||||
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
|
||||
/// </remarks>
|
||||
public override async Task<bool> IsLockedOutAsync(MembersIdentityUser user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (user.IsApproved == false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await base.IsLockedOutAsync(user);
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> AccessFailedAsync(MembersIdentityUser user)
|
||||
{
|
||||
IdentityResult result = await base.AccessFailedAsync(user);
|
||||
|
||||
// Slightly confusing: this will return a Success if we successfully update the AccessFailed count
|
||||
if (result.Succeeded)
|
||||
{
|
||||
RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ChangePasswordWithResetAsync(string userId, string token, string newPassword)
|
||||
{
|
||||
IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ChangePasswordAsync(MembersIdentityUser user, string currentPassword, string newPassword)
|
||||
{
|
||||
IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IdentityResult> SetLockoutEndDateAsync(MembersIdentityUser user, DateTimeOffset? lockoutEnd)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd);
|
||||
|
||||
// The way we unlock is by setting the lockoutEnd date to the current datetime
|
||||
if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
|
||||
{
|
||||
RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
|
||||
|
||||
// Resets the login attempt fails back to 0 when unlock is clicked
|
||||
await ResetAccessFailedCountAsync(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<IdentityResult> ResetAccessFailedCountAsync(MembersIdentityUser user)
|
||||
{
|
||||
IdentityResult result = await base.ResetAccessFailedCountAsync(user);
|
||||
|
||||
// raise the event now that it's reset
|
||||
RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetCurrentUserId(IPrincipal currentUser)
|
||||
{
|
||||
UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity();
|
||||
@@ -150,14 +55,8 @@ namespace Umbraco.Web.Common.Security
|
||||
return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername);
|
||||
}
|
||||
|
||||
private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, string affectedUserId, string affectedUsername)
|
||||
{
|
||||
var currentUserId = currentUser.Id;
|
||||
var ip = IpResolver.GetCurrentRequestIpAddress();
|
||||
return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername);
|
||||
}
|
||||
|
||||
// TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager,
|
||||
// TODO: As per backoffice, review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager,
|
||||
// lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring
|
||||
public void RaiseAccountLockedEvent(IPrincipal currentUser, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty));
|
||||
|
||||
@@ -181,10 +80,6 @@ namespace Umbraco.Web.Common.Security
|
||||
return args;
|
||||
}
|
||||
|
||||
public void RaisePasswordChangedEvent(IPrincipal currentUser, string userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty));
|
||||
|
||||
public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, string userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty));
|
||||
|
||||
public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId(currentUser);
|
||||
@@ -196,9 +91,7 @@ namespace Umbraco.Web.Common.Security
|
||||
|
||||
public bool HasSendingUserInviteEventHandler => SendingUserInvite != null;
|
||||
|
||||
// TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot
|
||||
// have non-static events here because the user manager is a Scoped instance not a singleton
|
||||
// so we'll have to deal with this a diff way i.e. refactoring how events are done entirely
|
||||
// TODO: Comments re static events as per backofficeusermanager
|
||||
public static event EventHandler<IdentityAuditEventArgs> AccountLocked;
|
||||
public static event EventHandler<IdentityAuditEventArgs> AccountUnlocked;
|
||||
public static event EventHandler<IdentityAuditEventArgs> ForgotPasswordRequested;
|
||||
|
||||
Reference in New Issue
Block a user