Added is dirty properties and updated to reflect linter update

This commit is contained in:
emmagarland
2020-12-05 23:44:50 +00:00
parent 0560bef48c
commit 40f2a881ab
9 changed files with 644 additions and 316 deletions

View File

@@ -1,77 +1,179 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Members
{
/// <summary>
/// An Umbraco member user type
/// TODO: use of identity classes in future
/// </summary>
public class UmbracoMembersIdentityUser
//: IRememberBeingDirty
//TODO: use of identity classes
//: IdentityUser<int, IIdentityUserLogin, IdentityUserRole<string>, IdentityUserClaim<int>>,
public class UmbracoMembersIdentityUser : IRememberBeingDirty
{
private bool _hasIdentity;
private int _id;
public string Name { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string MemberTypeAlias { get; set; }
public bool IsLockedOut { get; set; }
public string RawPasswordValue { get; set; }
private string _passwordHash;
private string _passwordConfig;
/// <summary>
/// Returns true if an Id has been set on this object
/// Gets or sets the member name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the member email
/// </summary>
public string Email { get; set; }
/// <summary>
/// Gets or sets the member username
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Gets or sets the alias of the member type
/// </summary>
public string MemberTypeAlias { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the member is locked out
/// </summary>
public bool IsLockedOut { get; set; }
/// <summary>
/// Gets a value indicating whether an Id has been set on this object
/// This will be false if the object is new and not persisted to the database
/// </summary>
public bool HasIdentity => _hasIdentity;
public bool HasIdentity { get; private set; }
/// <summary>
/// Gets or sets the member Id
/// </summary>
public int Id
{
get => _id;
set
{
_id = value;
_hasIdentity = true;
HasIdentity = true;
}
}
//TODO: track
public string PasswordHash { get; set; }
/// <summary>
/// Gets or sets the salted/hashed form of the user password
/// </summary>
public string PasswordHash
{
get => _passwordHash;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash));
}
//TODO: config
public string PasswordConfig { get; set; }
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
internal bool IsApproved;
/// <summary>
/// Gets or sets a value indicating whether member Is Approved
/// </summary>
public bool IsApproved { get; set; }
//TODO: needed?
/// <summary>
/// Gets the <see cref="BeingDirty"/> for change tracking
/// </summary>
protected BeingDirty BeingDirty { get; } = new BeingDirty();
// TODO: implement as per base identity user
//public bool LoginsChanged;
//public bool RolesChanged;
/// <summary>
/// Create a new identity member
/// </summary>
/// <param name="username">The member username</param>
/// <param name="email">The member email</param>
/// <param name="memberTypeAlias">The member type alias</param>
/// <param name="name">The member name</param>
/// TODO: confirm <param name="password">The password (may be null in some instances)</param>
/// <exception cref="ArgumentException">Throws is username is null or whitespace</exception>
/// <returns>The identity member user</returns>
public static UmbracoMembersIdentityUser CreateNew(
string username,
string email,
string memberTypeAlias,
string name)
string name,
string password = null)
{
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
}
//no groups/roles yet
// no groups/roles yet
var member = new UmbracoMembersIdentityUser
{
UserName = username,
Email = email,
Name = name,
MemberTypeAlias = memberTypeAlias,
Id = 0, //TODO: is this meant to be 0 in this circumstance?
//false by default unless specifically set
_hasIdentity = false
Id = 0, // TODO: is this meant to be 0 in this circumstance?
// false by default unless specifically set
HasIdentity = false
};
//TODO: do we use this?
//member.EnableChangeTracking();
member.EnableChangeTracking();
return member;
}
/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged
{
add => BeingDirty.PropertyChanged += value;
remove => BeingDirty.PropertyChanged -= value;
}
/// <inheritdoc />
public bool IsDirty() => BeingDirty.IsDirty();
/// <inheritdoc />
public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName);
/// <inheritdoc />
public IEnumerable<string> GetDirtyProperties() => BeingDirty.GetDirtyProperties();
/// <inheritdoc />
public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties();
/// <inheritdoc />
public void DisableChangeTracking() => BeingDirty.DisableChangeTracking();
/// <inheritdoc />
public void EnableChangeTracking() => BeingDirty.EnableChangeTracking();
/// <inheritdoc />
public bool WasDirty() => BeingDirty.WasDirty();
/// <inheritdoc />
public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName);
/// <inheritdoc />
public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties();
/// <inheritdoc />
public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty);
/// <inheritdoc />
public IEnumerable<string> GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties();
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Members;
@@ -12,11 +13,10 @@ namespace Umbraco.Infrastructure.Members
public interface IUmbracoMembersUserManager<TUser> : IDisposable where TUser : UmbracoMembersIdentityUser
{
/// <summary>
/// Creates the specified <paramref name="memberUser"/> in the backing store with a password,
/// as an asynchronous operation.
/// Creates the specified <paramref name="memberUser"/> user in the backing store with given password, as an asynchronous operation.
/// </summary>
/// <param name="memberUser">The member to create.</param>
/// <param name="password">The password to add</param>
/// <param name="password">The new password</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
/// of the operation.
@@ -26,7 +26,7 @@ namespace Umbraco.Infrastructure.Members
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns></returns>
/// <returns>Returns the generated password</returns>
string GeneratePassword();
/// <summary>
@@ -51,5 +51,12 @@ namespace Umbraco.Infrastructure.Members
/// the specified <paramref name="password" /> matches the one store for the <paramref name="memberUser"/>,
/// otherwise false.</returns>
Task<bool> CheckPasswordAsync(TUser memberUser, string password);
/// <summary>
/// Method to validate the password without an identity user
/// </summary>
/// <param name="password">The password to validate</param>
/// <returns>The result of the validation</returns>
Task<List<IdentityResult>> ValidatePassword(string password);
}
}

View File

@@ -1,14 +1,14 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Members;
using Umbraco.Core.Security;
using System.Threading;
using Umbraco.Core.Configuration.Models;
namespace Umbraco.Infrastructure.Members
@@ -18,6 +18,7 @@ namespace Umbraco.Infrastructure.Members
/// </summary>
public class UmbracoMembersUserManager : UmbracoMembersUserManager<UmbracoMembersIdentityUser>, IUmbracoMembersUserManager
{
///<inheritdoc />
public UmbracoMembersUserManager(
IUserStore<UmbracoMembersIdentityUser> store,
IOptions<UmbracoMembersIdentityOptions> optionsAccessor,
@@ -34,13 +35,28 @@ namespace Umbraco.Infrastructure.Members
}
}
/// <summary>
/// Manager for the member identity user
/// </summary>
/// <typeparam name="T">The identity user</typeparam>
public class UmbracoMembersUserManager<T> : UserManager<T>
where T : UmbracoMembersIdentityUser
{
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
private PasswordGenerator _passwordGenerator;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoMembersUserManager"/> class.
/// </summary>
/// <param name="store">The members store</param>
/// <param name="optionsAccessor">The identity options accessor</param>
/// <param name="passwordHasher">The password hasher</param>
/// <param name="userValidators">The user validators</param>
/// <param name="passwordValidators">The password validators</param>
/// <param name="keyNormalizer">The keep lookup normalizer</param>
/// <param name="errors">The error display messages</param>
/// <param name="services">The service provider</param>
/// <param name="logger">The logger</param>
/// <param name="passwordConfiguration">The password configuration</param>
public UmbracoMembersUserManager(
IUserStore<T> store,
IOptions<IdentityOptions> optionsAccessor,
@@ -51,14 +67,17 @@ namespace Umbraco.Infrastructure.Members
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<T>> logger,
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration) :
base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) =>
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
}
/// <summary>
/// Replace the underlying options property with our own strongly typed version
/// Gets or sets the password configuration
/// </summary>
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
/// <summary>
/// gets or sets the underlying options property with our own strongly typed version
/// </summary>
public new UmbracoMembersIdentityOptions Options
{
@@ -67,24 +86,24 @@ namespace Umbraco.Infrastructure.Members
}
/// <summary>
/// Gets/sets the default Umbraco member user password checker
/// Gets or sets the default Umbraco member user password checker
/// </summary>
public IUmbracoMembersUserPasswordChecker UmbracoMembersUserPasswordChecker { get; set; }
/// <summary>
/// [TODO: from BackOfficeUserManager duplicated, could be shared]
/// TODO: from BackOfficeUserManager duplicated, could be shared
/// Override to determine how to hash the password
/// </summary>
/// <param name="memberUser"></param>
/// <param name="newPassword"></param>
/// <param name="validatePassword"></param>
/// <returns></returns>
/// <param name="memberUser">The member to validate</param>
/// <param name="newPassword">The new password</param>
/// <param name="validatePassword">Whether to validate the password</param>
/// <returns>The identity result of updating the password hash</returns>
/// <remarks>
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
/// </remarks>
protected override async Task<IdentityResult> UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword)
{
//memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow;
// memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow;
if (validatePassword)
{
@@ -95,8 +114,10 @@ namespace Umbraco.Infrastructure.Members
}
}
var passwordStore = Store as IUserPasswordStore<T>;
if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
if (!(Store is IUserPasswordStore<T> passwordStore))
{
throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
}
var hash = newPassword != null ? PasswordHasher.HashPassword(memberUser, newPassword) : null;
await passwordStore.SetPasswordHashAsync(memberUser, hash, CancellationToken);
@@ -104,13 +125,13 @@ namespace Umbraco.Infrastructure.Members
return IdentityResult.Success;
}
///TODO: duplicated code from backofficeusermanager, could be shared?
/// TODO: duplicated code from backofficeusermanager, could be shared
/// <summary>
/// Logic used to validate a username and password
/// </summary>
/// <param name="member"></param>
/// <param name="password"></param>
/// <returns></returns>
/// <param name="member">The member to validate</param>
/// <param name="password">The password to validate</param>
/// <returns>Whether the password is the correct password for this member</returns>
/// <remarks>
/// By default this uses the standard ASP.Net Identity approach which is:
/// * Get password store
@@ -136,68 +157,88 @@ namespace Umbraco.Infrastructure.Members
return false;
}
//if the result indicates to not fallback to the default, then return true if the credentials are valid
// if the result indicates to not fallback to the default, then return true if the credentials are valid
if (result != UmbracoMembersUserPasswordCheckerResult.FallbackToDefaultChecker)
{
return result == UmbracoMembersUserPasswordCheckerResult.ValidCredentials;
}
}
//we cannot proceed if the user passed in does not have an identity
// we cannot proceed if the user passed in does not have an identity
if (member.HasIdentity == false)
{
return false;
}
//use the default behavior
// use the default behavior
return await base.CheckPasswordAsync(member, password);
}
///[TODO: from BackOfficeUserManager duplicated, could be shared]
/// TODO: from BackOfficeUserManager duplicated, could be shared
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
/// <param name="user">The user to update the security stamp for</param>
/// <returns>Task returns</returns>
private async Task UpdateSecurityStampInternal(T user)
{
if (SupportsUserSecurityStamp == false) return;
if (SupportsUserSecurityStamp == false)
{
return;
}
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
}
///[TODO: from BackOfficeUserManager duplicated, could be shared]
/// TODO: from BackOfficeUserManager duplicated, could be shared
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
/// <returns></returns>
/// <returns>Return a user security stamp</returns>
private IUserSecurityStampStore<T> GetSecurityStore()
{
var store = Store as IUserSecurityStampStore<T>;
if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
if (!(Store is IUserSecurityStampStore<T> store))
{
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
}
return store;
}
///[TODO: from BackOfficeUserManager duplicated, could be shared]
/// TODO: from BackOfficeUserManager duplicated, could be shared
/// <summary>
/// This is copied from the underlying .NET base class since they decided to not expose it
/// </summary>
/// <returns></returns>
private static string NewSecurityStamp()
{
return Guid.NewGuid().ToString();
}
/// <returns>Returns a new security stamp</returns>
private static string NewSecurityStamp() => Guid.NewGuid().ToString();
///[TODO: from BackOfficeUserManager duplicated, could be shared]
/// <summary>
/// TODO: from BackOfficeUserManager duplicated, could be shared
/// Helper method to generate a password for a member based on the current password validator
/// </summary>
/// <returns></returns>
/// <returns>The generated password</returns>
public string GeneratePassword()
{
if (_passwordGenerator == null)
{
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
}
_passwordGenerator ??= new PasswordGenerator(PasswordConfiguration);
string password = _passwordGenerator.GeneratePassword();
return password;
}
/// <summary>
/// Helper method to validate a password based on the current password validator
/// </summary>
/// <param name="password">The password to update</param>
/// <returns>The validated password</returns>
public async Task<List<IdentityResult>> ValidatePassword(string password)
{
var passwordValidators = new List<IdentityResult>();
foreach(IPasswordValidator<T> validator in PasswordValidators)
{
IdentityResult result = await validator.ValidateAsync(this, null, password);
passwordValidators.Add(result);
}
return passwordValidators;
}
}
}

View File

@@ -1,12 +1,15 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Mapping;
using Umbraco.Core.Members;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Infrastructure.Members
@@ -25,40 +28,57 @@ namespace Umbraco.Infrastructure.Members
//IUserTwoFactorStore<UmbracoMembersIdentityUser>
//IUserSessionStore<UmbracoMembersIdentityUser>
{
private bool _disposed = false;
private readonly bool _disposed = false;
private readonly IMemberService _memberService;
private readonly UmbracoMapper _mapper;
private readonly IScopeProvider _scopeProvider;
public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper)
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoMembersUserStore"/> class for the members identity store
/// </summary>
/// <param name="memberService">The member service</param>
/// <param name="mapper">The mapper for properties</param>
/// <param name="scopeProvider">The scope provider</param>
public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider)
{
_memberService = memberService;
_mapper = mapper;
_scopeProvider = scopeProvider;
}
/// <summary>
/// Create the member as an identity user
/// </summary>
/// <param name="user">The identity user` for a member</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>The identity result</returns>
public Task<IdentityResult> CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
//create member
//TODO: are we keeping this method, e.g. the Member Service? The user service creates it directly, but this gets the membertype
// 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
IMember member = _memberService.CreateMember(
user.UserName,
user.Email,
user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name,
user.MemberTypeAlias.IsNullOrWhiteSpace() ?
Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
UpdateMemberProperties(member, user);
//TODO: do we want to accept empty passwords here - if third-party for example? In other method if so?
// TODO: do we want to accept empty passwords here - if third-party for example?
// In other method if so?
_memberService.Save(member);
//re-assign id
// re-assign id
user.Id = member.Id;
// TODO: do we need this?
// TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
//bool isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins));
@@ -72,18 +92,21 @@ namespace Umbraco.Infrastructure.Members
// x.UserData)));
//}
if (!member.HasIdentity) throw new DataException("Could not create the user, check logs for details");
if (!member.HasIdentity)
{
throw new DataException("Could not create the member, check logs for details");
}
return Task.FromResult(IdentityResult.Success);
//TODO: confirm
// TODO: confirm and implement
//if (memberUser.LoginsChanged)
//{
// var logins = await GetLoginsAsync(memberUser);
// _externalLoginStore.SaveUserLogins(member.Id, logins);
//}
//TODO: confirm
// TODO: confirm and implement
//if (memberUser.RolesChanged)
//{
//IMembershipRoleService<IMember> memberRoleService = _memberService;
@@ -102,18 +125,17 @@ namespace Umbraco.Infrastructure.Members
private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser)
{
//[Comments as per BackOfficeUserStore & identity package]
var anythingChanged = false;
//don't assign anything if nothing has changed as this will trigger the track changes of the model
if (
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) &&
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Name)) &&
member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Name = memberIdentityUser.Name;
}
if (
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email)) &&
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Email)) &&
member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
@@ -127,22 +149,20 @@ namespace Umbraco.Infrastructure.Members
if (member.IsLockedOut)
{
//need to set the last lockout date
// need to set the last lockout date
member.LastLockoutDate = DateTime.Now;
}
}
if (
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) &&
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.UserName)) &&
member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Username = memberIdentityUser.UserName;
}
if (
//member.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))&&
member.RawPasswordValue != memberIdentityUser.PasswordHash
&& memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false)
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.PasswordHash))
&& member.RawPasswordValue != memberIdentityUser.PasswordHash && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.RawPasswordValue = memberIdentityUser.PasswordHash;
@@ -151,7 +171,7 @@ namespace Umbraco.Infrastructure.Members
// TODO: Roles
// [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too
//if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
//if (identityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
//{
// var userGroupAliases = member.Groups.Select(x => x.Alias).ToArray();
@@ -182,9 +202,7 @@ namespace Umbraco.Infrastructure.Members
// }
//}
//TODO: reset all changes
//memberIdentityUser.ResetDirtyProperties(false);
memberIdentityUser.ResetDirtyProperties(false);
return anythingChanged;
}
@@ -200,16 +218,17 @@ namespace Umbraco.Infrastructure.Members
public async Task<UmbracoMembersIdentityUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
//TODO: confirm logic
// TODO: confirm logic
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var member = _memberService.GetByUsername(normalizedUserName);
IMember member = _memberService.GetByUsername(normalizedUserName);
if (member == null)
{
return null;
}
var result = _mapper.Map<UmbracoMembersIdentityUser>(member);
UmbracoMembersIdentityUser result = _mapper.Map<UmbracoMembersIdentityUser>(member);
return await Task.FromResult(result);
}
@@ -223,64 +242,147 @@ namespace Umbraco.Infrastructure.Members
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.Id.ToString());
}
public Task<string> GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
{
//TODO: unit tests for and implement all bodies
// TODO: unit tests for and implement all bodies
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.UserName);
}
public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken)
{
return SetUserNameAsync(user, normalizedName, cancellationToken); throw new NotImplementedException();
}
/// <summary>
/// Sets the normalized user name
/// </summary>
/// <param name="user">The member identity user</param>
/// <param name="normalizedName">The normalized member name</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A task once complete</returns>
public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
/// <summary>
/// Sets the user name as an async operation
/// </summary>
/// <param name="user">The member identity user</param>
/// <param name="userName">The member user name</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A task once complete</returns>
public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.UserName = userName;
return Task.CompletedTask;
}
public Task<IdentityResult> UpdateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
/// <summary>
/// Update the user asynchronously
/// </summary>
/// <param name="member">The member identity user</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An identity result task</returns>
public Task<IdentityResult> UpdateAsync(UmbracoMembersIdentityUser member, CancellationToken cancellationToken)
{
throw new NotImplementedException();
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (member == null)
{
throw new ArgumentNullException(nameof(member));
}
Attempt<int> asInt = member.Id.TryConvertTo<int>();
if (asInt == false)
{
throw new InvalidOperationException("The member id must be an integer to work with the Umbraco");
}
using (IScope scope = _scopeProvider.CreateScope())
{
IMember found = _memberService.GetById(asInt.Result);
if (found != null)
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
if (UpdateMemberProperties(found, member))
{
_memberService.Save(found);
}
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// found.Id,
// member.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
}
scope.Complete();
}
return Task.FromResult(IdentityResult.Success);
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
///TODO: All from BackOfficeUserStore - same. Can we share?
/// TODO: All from BackOfficeUserStore - same. Can we share?
/// <summary>
/// Set the user password hash
/// </summary>
/// <param name="user"/><param name="passwordHash"/>
/// <param name="cancellationToken"></param>
/// <returns/>
/// <param name="user">The identity member user</param>
/// <param name="passwordHash">The password hash</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <exception cref="ArgumentException">Throws if the properties are null</exception>
/// <returns>Returns asynchronously</returns>
public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash));
if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (passwordHash == null)
{
throw new ArgumentNullException(nameof(passwordHash));
}
if (string.IsNullOrEmpty(passwordHash))
{
throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
}
user.PasswordHash = passwordHash;
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
// Clear this so that it's reset at the repository level
user.PasswordConfig = null;
return Task.CompletedTask;
}
@@ -290,12 +392,16 @@ namespace Umbraco.Infrastructure.Members
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <exception cref="ArgumentNullException"></exception>
/// <returns/>
public Task<string> GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.PasswordHash);
}
@@ -303,17 +409,19 @@ namespace Umbraco.Infrastructure.Members
/// <summary>
/// Returns true if a user has a password set
/// </summary>
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
/// <param name="user">The identity user</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>True if the user has a password</returns>
public Task<bool> HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null) throw new ArgumentNullException(nameof(user));
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -109,19 +109,20 @@ namespace Umbraco.Core.Services.Implement
/// <param name="email">Email of the Member to create</param>
/// <param name="name">Name of the Member to create</param>
/// <param name="memberTypeAlias">Alias of the MemberType the Member should be based on</param>
/// <exception cref="ArgumentException">Thrown when a member type for the given alias isn't found</exception>
/// <returns><see cref="IMember"/></returns>
public IMember CreateMember(string username, string email, string name, string memberTypeAlias)
{
var memberType = GetMemberType(memberTypeAlias);
IMemberType memberType = GetMemberType(memberTypeAlias);
if (memberType == null)
{
throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias));
}
var member = new Member(name, email.ToLower().Trim(), username, memberType);
using (var scope = ScopeProvider.CreateScope())
{
using IScope scope = ScopeProvider.CreateScope();
CreateMember(scope, member, 0, false);
scope.Complete();
}
return member;
}
@@ -312,7 +313,9 @@ namespace Umbraco.Core.Services.Implement
// if saving is cancelled, media remains without an identity
var saveEventArgs = new SaveEventArgs<IMember>(member);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
return;
}
_memberRepository.Save(member);
@@ -321,7 +324,9 @@ namespace Umbraco.Core.Services.Implement
}
if (withIdentity == false)
{
return;
}
Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}");
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -10,6 +10,7 @@ using NUnit.Framework;
using Umbraco.Core.Mapping;
using Umbraco.Core.Members;
using Umbraco.Core.Models;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Infrastructure.Members;
using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
@@ -26,7 +27,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members
_mockMemberService = new Mock<IMemberService>();
return new UmbracoMembersUserStore(
_mockMemberService.Object,
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())));
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())),
new Mock<IScopeProvider>().Object);
}
[Test]

View File

@@ -1,15 +1,10 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Events;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
@@ -54,14 +49,22 @@ namespace Umbraco.Web.BackOffice.Controllers
_serializer = serializer;
}
/// <summary>
/// Handles if the content for the specified ID isn't found
/// </summary>
/// <param name="id">The content ID to find</param>
/// <param name="throwException">Whether to throw an exception</param>
/// <returns>The error response</returns>
protected NotFoundObjectResult HandleContentNotFound(object id, bool throwException = true)
{
ModelState.AddModelError("id", $"content with id: {id} was not found");
var errorResponse = NotFound(ModelState);
NotFoundObjectResult errorResponse = NotFound(ModelState);
if (throwException)
{
throw new HttpResponseException(errorResponse);
}
return errorResponse;
}
@@ -78,7 +81,7 @@ namespace Umbraco.Web.BackOffice.Controllers
where TSaved : IContentSave<TPersisted>
{
// map the property values
foreach (var propertyDto in dto.Properties)
foreach (ContentPropertyDto propertyDto in dto.Properties)
{
// get the property editor
if (propertyDto.PropertyEditor == null)
@@ -89,19 +92,24 @@ namespace Umbraco.Web.BackOffice.Controllers
// get the value editor
// nothing to save/map if it is readonly
var valueEditor = propertyDto.PropertyEditor.GetValueEditor();
if (valueEditor.IsReadOnly) continue;
IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor();
if (valueEditor.IsReadOnly)
{
continue;
}
// get the property
var property = contentItem.PersistedContent.Properties[propertyDto.Alias];
IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias];
// prepare files, if any matching property and culture
var files = contentItem.UploadedFiles
ContentPropertyFile[] files = contentItem.UploadedFiles
.Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment)
.ToArray();
foreach (var file in files)
foreach (ContentPropertyFile file in files)
{
file.FileName = file.FileName.ToSafeFileName(ShortStringHelper);
}
// create the property data for the property editor
var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration)
@@ -112,25 +120,35 @@ namespace Umbraco.Web.BackOffice.Controllers
};
// let the editor convert the value that was received, deal with files, etc
var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
// set the value - tags are special
var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
if (tagAttribute != null)
{
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
if (tagConfiguration.Delimiter == default)
{
tagConfiguration.Delimiter = tagAttribute.Delimiter;
}
var tagCulture = property.PropertyType.VariesByCulture() ? culture : null;
property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture);
}
else
{
savePropertyValue(contentItem, property, value);
}
}
}
/// <summary>
/// Handles if the state is invalid
/// </summary>
/// <param name="display">The model state to display</param>
protected virtual void HandleInvalidModelState(IErrorModel display)
{
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
if (!ModelState.IsValid)
{
display.Errors = ModelState.ToErrorDictionary();
@@ -151,38 +169,45 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </remarks>
protected TPersisted GetObjectFromRequest<TPersisted>(Func<TPersisted> getFromService)
{
//checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
// checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
// it from the callback
return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null
? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()]
? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()]
: getFromService();
}
/// <summary>
/// Returns true if the action passed in means we need to create something new
/// </summary>
/// <param name="action"></param>
/// <returns></returns>
internal static bool IsCreatingAction(ContentSaveAction action)
{
return (action.ToString().EndsWith("New"));
}
/// <param name="action">The content action</param>
/// <returns>Returns true if this is a creating action</returns>
internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New");
protected void AddCancelMessage(INotificationModel display,
string header = "speechBubbles/operationCancelledHeader",
string message = "speechBubbles/operationCancelledText",
bool localizeHeader = true,
/// <summary>
/// Adds a cancelled message to the display
/// </summary>
/// <param name="display"></param>
/// <param name="header"></param>
/// <param name="message"></param>
/// <param name="localizeHeader"></param>
/// <param name="localizeMessage"></param>
/// <param name="headerParams"></param>
/// <param name="messageParams"></param>
protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true,
bool localizeMessage = true,
string[] headerParams = null,
string[] messageParams = null)
{
//if there's already a default event message, don't add our default one
var msgs = EventMessages;
if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return;
// if there's already a default event message, don't add our default one
IEventMessagesFactory messages = EventMessages;
if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage))
{
return;
}
display.AddWarningNotification(
localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header,
localizeMessage ? LocalizedTextService.Localize(message, messageParams): message);
localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -7,8 +7,8 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
@@ -17,6 +17,7 @@ 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.Security;
using Umbraco.Core.Serialization;
@@ -54,9 +55,25 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IUmbracoMembersUserManager _memberManager;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MemberController"/> class.
/// </summary>
/// <param name="cultureDictionary">The culture dictionary</param>
/// <param name="loggerFactory">The logger factory</param>
/// <param name="shortStringHelper">The string helper</param>
/// <param name="eventMessages">The event messages factory</param>
/// <param name="localizedTextService">The entry point for localizing key services</param>
/// <param name="propertyEditors">The property editors</param>
/// <param name="umbracoMapper">The mapper</param>
/// <param name="memberService">The member service</param>
/// <param name="memberTypeService">The member type service</param>
/// <param name="memberManager">The member manager</param>
/// <param name="dataTypeService">The data-type service</param>
/// <param name="backOfficeSecurityAccessor">The back office security accessor</param>
/// <param name="jsonSerializer">The JSON serializer</param>
public MemberController(
ICultureDictionary cultureDictionary,
ILoggerFactory loggerFactory,
@@ -69,7 +86,7 @@ namespace Umbraco.Web.BackOffice.Controllers
IMemberTypeService memberTypeService,
IUmbracoMembersUserManager memberManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IJsonSerializer jsonSerializer)
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
{
@@ -80,10 +97,21 @@ namespace Umbraco.Web.BackOffice.Controllers
_memberManager = memberManager;
_dataTypeService = dataTypeService;
_localizedTextService = localizedTextService;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_jsonSerializer = jsonSerializer;
}
/// <summary>
/// The paginated list of members
/// </summary>
/// <param name="pageNumber">The page number to display</param>
/// <param name="pageSize">The size of the page</param>
/// <param name="orderBy">The ordering of the member list</param>
/// <param name="orderDirection">The direction of the member list</param>
/// <param name="orderBySystemField">The system field to order by</param>
/// <param name="filter">The current filter for the list</param>
/// <param name="memberTypeAlias">The member type</param>
/// <returns>The paged result of members</returns>
public PagedResult<MemberBasic> GetPagedResults(
int pageNumber = 1,
int pageSize = 100,
@@ -123,11 +151,11 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Returns a display node with a list view to render members
/// </summary>
/// <param name="listName"></param>
/// <returns></returns>
/// <param name="listName">The member type to list</param>
/// <returns>The member list for display</returns>
public MemberListDisplay GetListNodeDisplay(string listName)
{
var foundType = _memberTypeService.Get(listName);
IMemberType foundType = _memberTypeService.Get(listName);
var name = foundType != null ? foundType.Name : listName;
var apps = new List<ContentApp>
@@ -159,25 +187,26 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Gets the content json for the member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <param name="key">The Guid key of the member</param>
/// <returns>The member for display</returns>
[OutgoingEditorModelEvent]
public MemberDisplay GetByKey(Guid key)
{
//TODO: this is not finding the key currently
// TODO: this is not finding the key currently
IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
HandleContentNotFound(key);
}
return _umbracoMapper.Map<MemberDisplay>(foundMember);
}
/// <summary>
/// Gets an empty content item for the
/// </summary>
/// <param name="contentTypeAlias"></param>
/// <returns></returns>
/// <param name="contentTypeAlias">The content type</param>
/// <returns>The empty member for display</returns>
[OutgoingEditorModelEvent]
public MemberDisplay GetEmpty(string contentTypeAlias = null)
{
@@ -202,91 +231,109 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Saves member
/// </summary>
/// <returns></returns>
/// <param name="contentItem">The content item to save as a member</param>
/// <returns>The resulting member display object</returns>
[FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
[MemberSaveValidation]
public async Task<ActionResult<MemberDisplay>> PostSave(
[ModelBinder(typeof(MemberBinder))]
MemberSave contentItem)
public async Task<ActionResult<MemberDisplay>> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
{
if (contentItem == null) throw new ArgumentNullException(nameof(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:
// If we've reached here it means:
// * Our model has been bound
// * and validated
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
//map the properties to the persisted entity
// map the properties to the persisted entity
MapPropertyValues(contentItem);
UmbracoMembersIdentityUser identityMember = ValidateMemberData(contentItem);
ValidateMemberData(contentItem);
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
// 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)
{
var forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
MemberDisplay forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
throw HttpResponseException.CreateValidationErrorResponse(forDisplay);
}
//We're gonna look up the current roles now because the below code can cause
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
if (memberType == null)
{
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
}
// Create the member with the MemberManager
var identityMember = UmbracoMembersIdentityUser.CreateNew(
contentItem.Username,
contentItem.Email,
memberType.Alias,
contentItem.Name);
// 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.
var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
IEnumerable<string> currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
//find the ones to remove and remove them
IEnumerable<string> roles = currRoles.ToList();
var rolesToRemove = roles.Except(contentItem.Groups).ToArray();
// 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.
// 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:
UpdateMemberData(contentItem);
break;
case ContentSaveAction.SaveNew:
await CreateMemberAsync(contentItem, identityMember);
IdentityResult identityResult = await CreateMemberAsync(contentItem, identityMember);
break;
default:
//we don't support anything else for members
// we don't support anything else for members
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//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
// 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
// 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
var toAdd = contentItem.Groups.Except(roles).ToArray();
// find the ones to add and add them
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
if (toAdd.Any())
{
//add the ones submitted
// add the ones submitted
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
}
//return the updated model
var display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
// return the updated model
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
HandleInvalidModelState(display);
var localizedTextService = _localizedTextService;
//put the correct messages in
ILocalizedTextService localizedTextService = _localizedTextService;
// put the correct messages in
switch (contentItem.Action)
{
case ContentSaveAction.Save:
@@ -303,77 +350,64 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Maps the property values to the persisted entity
/// </summary>
/// <param name="contentItem"></param>
/// <param name="contentItem">The member content item to map properties from</param>
private void MapPropertyValues(MemberSave contentItem)
{
//Don't update the name if it is empty
// Don't update the name if it is empty
if (contentItem.Name.IsNullOrWhiteSpace() == false)
{
contentItem.PersistedContent.Name = contentItem.Name;
}
//map the custom properties - this will already be set for new entities in our member binder
// map the custom properties - this will already be set for new entities in our member binder
contentItem.PersistedContent.Email = contentItem.Email;
contentItem.PersistedContent.Username = contentItem.Username;
//use the base method to map the rest of the properties
base.MapPropertyValuesForPersistence<IMember, MemberSave>(
// use the base method to map the rest of the properties
MapPropertyValuesForPersistence<IMember, MemberSave>(
contentItem,
contentItem.PropertyCollectionDto,
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v), //set prop val
(save, property) => property.GetValue(), // get prop val
(save, property, v) => property.SetValue(v), // set prop val
null); // member are all invariant
}
/// <summary>
/// Create a member from the supplied member content data
/// All member password processing and creation is done via the aspnet identity MemberUserManager
///
/// All member password processing and creation is done via the identity manager
/// </summary>
/// <param name="contentItem"></param>
/// <param name="identityMember"></param>
/// <returns></returns>
private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember)
/// <param name="contentItem">Member content data</param>
/// <param name="identityMember">The identity member to update</param>
/// <returns>The identity result of the created member</returns>
private async Task<IdentityResult> CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember)
{
//var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
//if (memberType == null)
// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
//var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
//{
// CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
// Comments = contentItem.Comments,
// IsApproved = contentItem.IsApproved
//};
//return member;
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
if (created.Succeeded == false)
{
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
}
//now re-look the member back up which will now exist
// now re-look the member back up which will now exist
IMember member = _memberService.GetByEmail(contentItem.Email);
member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
// should this be removed since we've moved passwords out?
//since the back office user is creating this member, they will be set to approved
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;
//map the save info over onto the user
// map the save info over onto the user
member = _umbracoMapper.Map(contentItem, member);
contentItem.PersistedContent = member;
return created;
}
private UmbracoMembersIdentityUser ValidateMemberData(MemberSave contentItem)
private void ValidateMemberData(MemberSave contentItem)
{
var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
if (memberType == null)
{
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
}
if (contentItem.Name.IsNullOrWhiteSpace())
{
ModelState.AddPropertyError(
@@ -399,14 +433,13 @@ namespace Umbraco.Web.BackOffice.Controllers
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
{
//TODO: check password
//var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword);
//if (!validPassword)
//{
// ModelState.AddPropertyError(
// new ValidationResult("Invalid password", new[] { "value" }),
// $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
//}
Task<List<IdentityResult>> result = _memberManager.ValidatePassword(contentItem.Password.NewPassword);
if (result.Result.Exists(x => x.Succeeded == false))
{
ModelState.AddPropertyError(
new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
}
}
else
{
@@ -414,48 +447,48 @@ namespace Umbraco.Web.BackOffice.Controllers
new ValidationResult("Password cannot be empty", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
}
}
// Create the member with the MemberManager
var identityMember = UmbracoMembersIdentityUser.CreateNew(
contentItem.Username,
contentItem.Email,
memberType.Alias,
contentItem.Name);
//TODO: confirm where to do this
identityMember.RawPasswordValue = contentItem.Password.NewPassword;
return identityMember;
private string MapErrors(List<IdentityResult> result)
{
var sb = new StringBuilder();
IEnumerable<IdentityResult> errors = result.Where(x => x.Succeeded == false);
foreach (IdentityResult error in errors)
{
sb.AppendLine(error.Errors.ToErrorMessage());
}
return sb.ToString();
}
/// <summary>
/// Update the member security data
/// </summary>
/// <param name="memberSave"></param>
/// <returns>
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
/// </returns>
private async void UpdateMemberData(MemberSave memberSave)
/// </summary>
/// <param name="memberSave">The member to save</param>
private void UpdateMemberData(MemberSave memberSave)
{
//TODO: optimise based on new member manager
memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
memberSave.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
// have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
// There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut
// but we will take care of this in a generic way below so that it works for all props.
if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
{
var memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId);
IMemberType memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId);
var sensitiveProperties = memberType
.PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias))
.ToList();
foreach (var sensitiveProperty in sensitiveProperties)
foreach (IPropertyType sensitiveProperty in sensitiveProperties)
{
var destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
ContentPropertyBasic destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
if (destProp != null)
{
//if found, change the value of the contentItem model to the persisted value so it remains unchanged
var origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias);
// if found, change the value of the contentItem model to the persisted value so it remains unchanged
object origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias);
destProp.Value = origValue;
}
}
@@ -463,7 +496,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var isLockedOut = memberSave.IsLockedOut;
//if they were locked but now they are trying to be unlocked
// if they were locked but now they are trying to be unlocked
if (memberSave.PersistedContent.IsLockedOut && isLockedOut == false)
{
memberSave.PersistedContent.IsLockedOut = false;
@@ -471,34 +504,34 @@ namespace Umbraco.Web.BackOffice.Controllers
}
else if (!memberSave.PersistedContent.IsLockedOut && isLockedOut)
{
//NOTE: This should not ever happen unless someone is mucking around with the request data.
//An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
// NOTE: This should not ever happen unless someone is mucking around with the request data.
// An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
ModelState.AddModelError("custom", "An admin cannot lock a user");
}
//no password changes then exit ?
if (memberSave.Password == null)
return;
//TODO: update member password functionality in manager// set the password
// no password changes then exit ?
if (memberSave.Password != null)
{
// set the password
memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword();
}
}
/// <summary>
/// Permanently deletes a member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <param name="key">Guid of the member to delete</param>
/// <returns>The result of the deletion</returns>
///
[HttpPost]
public IActionResult DeleteByKey(Guid key)
{
var foundMember = _memberService.GetByKey(key);
IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
return HandleContentNotFound(key, false);
}
_memberService.Delete(foundMember);
return Ok();
@@ -512,19 +545,23 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public IActionResult ExportMemberData(Guid key)
{
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
if (currentUser.HasAccessToSensitiveData() == false)
{
return Forbid();
}
var member = ((MemberService)_memberService).ExportMember(key);
if (member is null) throw new NullReferenceException("No member found with key " + key);
MemberExportModel member = ((MemberService)_memberService).ExportMember(key);
if (member is null)
{
throw new NullReferenceException("No member found with key " + key);
}
var json = _jsonSerializer.Serialize(member);
var fileName = $"{member.Name}_{member.Email}.txt";
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
HttpContext.Response.Headers.Add("x-filename", fileName);

View File

@@ -5,5 +5,6 @@
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=2DA32DA040A7D74599ABE288C7224CF0/Severity/@EntryValue">HINT</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=37A0B37A0ABAA34AA5CB32A93653C4FE/@KeyIndexDefined">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">Default</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Umbraco/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublishing/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>