Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/align-infrastructure-namespaces

# Conflicts:
#	src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs
#	src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs
#	src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs
#	src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
#	src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs
#	src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs
#	src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs
#	src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
#	src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs
#	src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
#	src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
#	src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs
#	src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs
This commit is contained in:
Mole
2021-02-23 08:51:09 +01:00
64 changed files with 2799 additions and 485 deletions

View File

@@ -1,7 +1,7 @@
namespace Umbraco.Cms.Core.Configuration
{
/// <summary>
/// The password configuration for back office users
/// The password configuration for members
/// </summary>
public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration
{

View File

@@ -41,6 +41,9 @@ namespace Umbraco.Cms.Core
public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
public const string DefaultMemberTypeAlias = "Member";
/// <summary>
/// The prefix used for external identity providers for their authentication type
/// </summary>

View File

@@ -0,0 +1,33 @@
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Core.Models.Mapping
{
/// <inheritdoc />
public class MemberMapDefinition : IMapDefinition
{
/// <inheritdoc />
public void DefineMaps(UmbracoMapper mapper) => mapper.Define<MemberSave, IMember>(Map);
private static void Map(MemberSave source, IMember target, MapperContext context)
{
target.IsApproved = source.IsApproved;
target.Name = source.Name;
target.Email = source.Email;
target.Key = source.Key;
target.Username = source.Username;
target.Comments = source.Comments;
target.CreateDate = source.CreateDate;
target.UpdateDate = source.UpdateDate;
target.Email = source.Email;
// TODO: ensure all properties are mapped as required
//target.Id = source.Id;
//target.ParentId = -1;
//target.Path = "-1," + source.Id;
//TODO: add groups as required
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
@@ -19,8 +19,11 @@ namespace Umbraco.Cms.Core.Models
private string _email;
private string _rawPasswordValue;
private string _passwordConfig;
private DateTime? _emailConfirmedDate;
private string _securityStamp;
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
/// Constructor for creating an empty Member object
/// </summary>
/// <param name="contentType">ContentType for the current Content object</param>
@@ -29,13 +32,14 @@ namespace Umbraco.Cms.Core.Models
{
IsApproved = true;
//this cannot be null but can be empty
// this cannot be null but can be empty
_rawPasswordValue = "";
_email = "";
_username = "";
}
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
/// Constructor for creating a Member object
/// </summary>
/// <param name="name">Name of the content</param>
@@ -43,18 +47,21 @@ namespace Umbraco.Cms.Core.Models
public Member(string name, IMemberType contentType)
: base(name, -1, contentType, new PropertyCollection())
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
if (name == null)
throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
IsApproved = true;
//this cannot be null but can be empty
// this cannot be null but can be empty
_rawPasswordValue = "";
_email = "";
_username = "";
}
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
/// Constructor for creating a Member object
/// </summary>
/// <param name="name"></param>
@@ -64,22 +71,29 @@ namespace Umbraco.Cms.Core.Models
public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true)
: base(name, -1, contentType, new PropertyCollection())
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
if (email == null) throw new ArgumentNullException(nameof(email));
if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email));
if (username == null) throw new ArgumentNullException(nameof(username));
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username));
if (name == null)
throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
if (email == null)
throw new ArgumentNullException(nameof(email));
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email));
if (username == null)
throw new ArgumentNullException(nameof(username));
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username));
_email = email;
_username = username;
IsApproved = isApproved;
//this cannot be null but can be empty
// this cannot be null but can be empty
_rawPasswordValue = "";
}
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
/// Constructor for creating a Member object
/// </summary>
/// <param name="name"></param>
@@ -99,6 +113,7 @@ namespace Umbraco.Cms.Core.Models
}
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
/// Constructor for creating a Member object
/// </summary>
/// <param name="name"></param>
@@ -138,6 +153,13 @@ namespace Umbraco.Cms.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email));
}
[DataMember]
public DateTime? EmailConfirmedDate
{
get => _emailConfirmedDate;
set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate));
}
/// <summary>
/// Gets or sets the raw password value
/// </summary>
@@ -190,7 +212,8 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string));
if (a.Success == false) return a.Result;
if (a.Success == false)
return a.Result;
return Properties[Constants.Conventions.Member.Comments].GetValue() == null
? string.Empty
@@ -200,7 +223,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.Comments,
nameof(Comments)) == false) return;
nameof(Comments)) == false)
return;
Properties[Constants.Conventions.Member.Comments].SetValue(value);
}
@@ -221,8 +245,10 @@ namespace Umbraco.Cms.Core.Models
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsApproved, nameof(IsApproved),
//This is the default value if the prop is not found
true);
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null) return true;
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null)
return true;
var tryConvert = Properties[Constants.Conventions.Member.IsApproved].GetValue().TryConvertTo<bool>();
if (tryConvert.Success)
{
@@ -235,7 +261,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.IsApproved,
nameof(IsApproved)) == false) return;
nameof(IsApproved)) == false)
return;
Properties[Constants.Conventions.Member.IsApproved].SetValue(value);
}
@@ -254,8 +281,10 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, nameof(IsLockedOut), false);
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null) return false;
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null)
return false;
var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].GetValue().TryConvertTo<bool>();
if (tryConvert.Success)
{
@@ -268,7 +297,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.IsLockedOut,
nameof(IsLockedOut)) == false) return;
nameof(IsLockedOut)) == false)
return;
Properties[Constants.Conventions.Member.IsLockedOut].SetValue(value);
}
@@ -287,8 +317,10 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, nameof(LastLoginDate), default(DateTime));
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null) return default(DateTime);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
@@ -301,7 +333,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastLoginDate,
nameof(LastLoginDate)) == false) return;
nameof(LastLoginDate)) == false)
return;
Properties[Constants.Conventions.Member.LastLoginDate].SetValue(value);
}
@@ -320,8 +353,10 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, nameof(LastPasswordChangeDate), default(DateTime));
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null) return default(DateTime);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
@@ -334,7 +369,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastPasswordChangeDate,
nameof(LastPasswordChangeDate)) == false) return;
nameof(LastPasswordChangeDate)) == false)
return;
Properties[Constants.Conventions.Member.LastPasswordChangeDate].SetValue(value);
}
@@ -353,8 +389,10 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, nameof(LastLockoutDate), default(DateTime));
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null) return default(DateTime);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
@@ -367,7 +405,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastLockoutDate,
nameof(LastLockoutDate)) == false) return;
nameof(LastLockoutDate)) == false)
return;
Properties[Constants.Conventions.Member.LastLockoutDate].SetValue(value);
}
@@ -387,8 +426,10 @@ namespace Umbraco.Cms.Core.Models
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, nameof(FailedPasswordAttempts), 0);
if (a.Success == false) return a.Result;
if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null) return default(int);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null)
return default(int);
var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue().TryConvertTo<int>();
if (tryConvert.Success)
{
@@ -401,7 +442,8 @@ namespace Umbraco.Cms.Core.Models
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.FailedPasswordAttempts,
nameof(FailedPasswordAttempts)) == false) return;
nameof(FailedPasswordAttempts)) == false)
return;
Properties[Constants.Conventions.Member.FailedPasswordAttempts].SetValue(value);
}
@@ -413,6 +455,17 @@ namespace Umbraco.Cms.Core.Models
[DataMember]
public virtual string ContentTypeAlias => ContentType.Alias;
/// <summary>
/// The security stamp used by ASP.Net identity
/// </summary>
[IgnoreDataMember]
public string SecurityStamp
{
get => _securityStamp;
set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp));
}
/// <summary>
/// Internal/Experimental - only used for mapping queries.
/// </summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models.Membership
@@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.Models.Membership
{
string Username { get; set; }
string Email { get; set; }
DateTime? EmailConfirmedDate { get; set; }
/// <summary>
/// Gets or sets the raw password value
@@ -38,6 +39,11 @@ namespace Umbraco.Cms.Core.Models.Membership
/// </remarks>
int FailedPasswordAttempts { get; set; }
/// <summary>
/// Gets or sets the security stamp used by ASP.NET Identity
/// </summary>
string SecurityStamp { get; set; }
//object ProfileId { get; set; }
//IEnumerable<object> Groups { get; set; }
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models.Entities;
@@ -19,7 +19,6 @@ namespace Umbraco.Cms.Core.Models.Membership
int[] StartMediaIds { get; set; }
string Language { get; set; }
DateTime? EmailConfirmedDate { get; set; }
DateTime? InvitedDate { get; set; }
/// <summary>
@@ -38,11 +37,6 @@ namespace Umbraco.Cms.Core.Models.Membership
/// </summary>
IProfile ProfileData { get; }
/// <summary>
/// The security stamp used by ASP.Net identity
/// </summary>
string SecurityStamp { get; set; }
/// <summary>
/// Will hold the media file system relative path of the users custom avatar if they uploaded one
/// </summary>

View File

@@ -6,7 +6,6 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// A custom user identity for the Umbraco backoffice
/// </summary>

View File

@@ -1,29 +1,47 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
namespace Umbraco.Cms.Core.Services
{
public interface IMembershipRoleService<out T>
public interface IMembershipRoleService<out T>
where T : class, IMembershipUser
{
void AddRole(string roleName);
IEnumerable<string> GetAllRoles();
IEnumerable<IMemberGroup> GetAllRoles();
IEnumerable<string> GetAllRoles(int memberId);
IEnumerable<string> GetAllRoles(string username);
IEnumerable<int> GetAllRolesIds();
IEnumerable<int> GetAllRolesIds(int memberId);
IEnumerable<int> GetAllRolesIds(string username);
IEnumerable<T> GetMembersInRole(string roleName);
IEnumerable<T> FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
bool DeleteRole(string roleName, bool throwIfBeingUsed);
void AssignRole(string username, string roleName);
void AssignRoles(string[] usernames, string[] roleNames);
void DissociateRole(string username, string roleName);
void DissociateRoles(string[] usernames, string[] roleNames);
void AssignRole(int memberId, string roleName);
void AssignRoles(int[] memberIds, string[] roleNames);
void DissociateRole(int memberId, string roleName);
void DissociateRoles(int[] memberIds, string[] roleNames);
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text;
using System.Threading;

View File

@@ -31,6 +31,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
.Add<TagMapDefinition>()
.Add<TemplateMapDefinition>()
.Add<UserMapDefinition>()
.Add<MemberMapDefinition>()
.Add<LanguageMapDefinition>()
.Add<IdentityMapDefinition>();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Net.Http;
using System.Text;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -349,7 +349,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
NodeDto parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
@@ -357,10 +357,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
// create the dto
var dto = ContentBaseFactory.BuildDto(entity);
MemberDto dto = ContentBaseFactory.BuildDto(entity);
// update the node dto
var nodeDto = dto.ContentDto.NodeDto;
NodeDto nodeDto = dto.ContentDto.NodeDto;
Database.Update(nodeDto);
// update the content dto
@@ -411,7 +411,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
//get the group id
var grpQry = Query<IMemberGroup>().Where(group => group.Name.Equals(roleName));
var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault();
if (memberGroup == null) return Enumerable.Empty<IMember>();
if (memberGroup == null)
return Enumerable.Empty<IMember>();
// get the members by username
var query = Query<IMember>();
@@ -466,7 +467,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
{
var grpQry = Query<IMemberGroup>().Where(group => group.Name.Equals(groupName));
var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault();
if (memberGroup == null) return Enumerable.Empty<IMember>();
if (memberGroup == null)
return Enumerable.Empty<IMember>();
var subQuery = Sql().Select("Member").From<Member2MemberGroupDto>().Where<Member2MemberGroupDto>(dto => dto.MemberGroup == memberGroup.Id);
@@ -616,7 +618,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
var cached = IsolatedCache.GetCacheItem<IMember>(RepositoryCacheKeys.GetKey<IMember>(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Member) cached;
content[i] = (Member)cached;
continue;
}
}
@@ -658,7 +660,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
// get properties - indexed by version id
var versionId = dto.ContentVersionDto.Id;
var temp = new TempContent<Member>(dto.ContentDto.NodeId,versionId, 0, memberType);
var temp = new TempContent<Member>(dto.ContentDto.NodeId, versionId, 0, memberType);
var properties = GetPropertyCollections(new List<TempContent<Member>> { temp });
member.Properties = properties[versionId];

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

View File

@@ -93,7 +93,9 @@ namespace Umbraco.Cms.Core.Security
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
@@ -186,13 +188,13 @@ namespace Umbraco.Cms.Core.Security
{
get
{
var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating the IUser IsApproved
/// Gets or sets a value indicating whether the IUser IsApproved
/// </summary>
public bool IsApproved { get; set; }

View File

@@ -439,7 +439,7 @@ namespace Umbraco.Cms.Core.Security
}
/// <summary>
/// Returns the roles (user groups) for this user
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{

View File

@@ -0,0 +1,11 @@
using Umbraco.Core.Security;
namespace Umbraco.Core.Security
{
/// <summary>
/// The user manager for members
/// </summary>
public interface IMemberManager : IUmbracoUserManager<MembersIdentityUser>
{
}
}

View File

@@ -5,6 +5,7 @@ using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Security
@@ -14,7 +15,7 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
/// <typeparam name="TUser">The type of user</typeparam>
public interface IUmbracoUserManager<TUser> : IDisposable
where TUser : BackOfficeIdentityUser
where TUser : UmbracoIdentityUser
{
/// <summary>
/// Gets the user id of a user
@@ -221,12 +222,64 @@ namespace Umbraco.Cms.Core.Security
/// </returns>
Task<IdentityResult> CreateAsync(TUser user);
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
/// <param name="user">The user whose role names to retrieve.</param>
/// <returns>The Task that represents the asynchronous operation, containing a list of role names.</returns>
Task<IList<string>> GetRolesAsync(TUser user);
/// <summary>
/// Removes the specified user from the named roles.
/// </summary>
/// <param name="user">The user to remove from the named roles.</param>
/// <param name="roles">The name of the roles to remove the user from.</param>
/// <returns>The Task that represents the asynchronous operation, containing the IdentityResult of the operation.</returns>
Task<IdentityResult> RemoveFromRolesAsync(TUser user, IEnumerable<string> roles);
/// <summary>
/// Add the specified user to the named roles
/// </summary>
/// <param name="user">The user to add to the named roles</param>
/// <param name="roles">The name of the roles to add the user to.</param>
/// <returns>The Task that represents the asynchronous operation, containing the IdentityResult of the operation</returns>
Task<IdentityResult> AddToRolesAsync(TUser user, IEnumerable<string> roles);
/// <summary>
/// Creates the specified <paramref name="user"/> in the backing store with a password,
/// as an asynchronous operation.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="password">The password to add to the user.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
/// of the operation.
/// </returns>
Task<IdentityResult> CreateAsync(TUser user, string password);
/// <summary>
/// Generate a password for a user based on the current password validator
/// </summary>
/// <returns>A generated password</returns>
string GeneratePassword();
/// <summary>
/// Hashes a password for a null user based on the default password hasher
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
string HashPassword(string password);
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code
/// 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>

View File

@@ -1,9 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Extensions
namespace Umbraco.Core.Security
{
public static class IdentityExtensions
{

View File

@@ -3,6 +3,7 @@
using System;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -40,6 +41,20 @@ namespace Umbraco.Cms.Core.Security
target.ResetDirtyProperties(true);
target.EnableChangeTracking();
});
mapper.Define<IMember, MembersIdentityUser>(
(source, context) =>
{
var target = new MembersIdentityUser(source.Id);
target.DisableChangeTracking();
return target;
},
(source, target, context) =>
{
Map(source, target);
target.ResetDirtyProperties(true);
target.EnableChangeTracking();
});
}
// Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled
@@ -79,9 +94,24 @@ namespace Umbraco.Cms.Core.Security
//target.Roles =;
}
private static string GetPasswordHash(string storedPass)
private void Map(IMember source, MembersIdentityUser target)
{
return storedPass.StartsWith(Cms.Core.Constants.Security.EmptyPasswordPrefix) ? null : storedPass;
target.Email = source.Email;
target.UserName = source.Username;
target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime();
target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime();
target.EmailConfirmed = source.EmailConfirmedDate.HasValue;
target.Name = source.Name;
target.AccessFailedCount = source.FailedPasswordAttempts;
target.PasswordHash = GetPasswordHash(source.RawPasswordValue);
target.PasswordConfig = source.PasswordConfiguration;
target.IsApproved = source.IsApproved;
target.SecurityStamp = source.SecurityStamp;
target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null;
// NB: same comments re AutoMapper as per BackOfficeUser
}
private static string GetPasswordHash(string storedPass) => storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass;
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Services;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Security
{
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MemberRolesUserStore : RoleStoreBase<IdentityRole<string>, string, IdentityUserRole<string>, IdentityRoleClaim<string>>
{
private readonly IMemberService _memberService;
private readonly IMemberGroupService _memberGroupService;
private readonly IScopeProvider _scopeProvider;
public MemberRolesUserStore(IMemberService memberService, IMemberGroupService memberGroupService, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
}
/// <inheritdoc />
public override IQueryable<IdentityRole<string>> Roles { get; }
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityResult> UpdateAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityResult> DeleteAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityRole<string>> FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityRole<string>> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IList<Claim>> GetClaimsAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task AddClaimAsync(IdentityRole<string> role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task RemoveClaimAsync(IdentityRole<string> role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Core.Security
{
public class MembersIdentityBuilder : IdentityBuilder
{
public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services)
{
}
public MembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MembersIdentityUser), role, services)
{
}
/// <summary>
/// Adds a token provider for the <seealso cref="MembersIdentityUser"/>.
/// </summary>
/// <param name="providerName">The name of the provider to add.</param>
/// <param name="provider">The type of the <see cref="IUserTwoFactorTokenProvider{UmbracoMembersIdentityUser}"/> to add.</param>
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
public override IdentityBuilder AddTokenProvider(string providerName, Type provider)
{
if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
{
throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}");
}
Services.Configure<MembersIdentityOptions>(options =>
{
options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider);
});
Services.AddTransient(provider);
return this;
}
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// Identity options specifically for the Umbraco members identity implementation
/// </summary>
public class MembersIdentityOptions : IdentityOptions
{
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Core;
using Umbraco.Core.Models.Identity;
using Umbraco.Extensions;
namespace Umbraco.Core.Security
{
/// <summary>
/// The identity user used for the member
/// </summary>
public class MembersIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _passwordConfig;
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
// Custom comparer for enumerables
private static readonly DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>> s_groupsComparer = new DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>>(
(groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
groups => groups.GetHashCode());
/// <summary>
/// Initializes a new instance of the <see cref="MembersIdentityUser"/> class.
/// </summary>
public MembersIdentityUser(int userId)
{
// use the property setters - they do more than just setting a field
Id = UserIdToString(userId);
}
public MembersIdentityUser()
{
}
/// <summary>
/// Used to construct a new instance without an identity
/// </summary>
public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, string name = null)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
}
var user = new MembersIdentityUser();
user.DisableChangeTracking();
user.UserName = username;
user.Email = email;
user.MemberTypeAlias = memberTypeAlias;
user.Id = null;
user.HasIdentity = false;
user._name = name;
user.EnableChangeTracking();
return user;
}
/// <summary>
/// Gets or sets the member's real name
/// </summary>
public string Name
{
get => _name;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <summary>
/// Gets or sets the user groups
/// </summary>
public IReadOnlyCollection<IReadOnlyUserGroup> Groups
{
get => _groups;
set
{
_groups = value.Where(x => x.Alias != null).ToArray();
var roles = new List<IdentityUserRole<string>>();
foreach (IdentityUserRole<string> identityUserRole in _groups.Select(x => new IdentityUserRole<string>
{
RoleId = x.Alias,
UserId = Id
}))
{
roles.Add(identityUserRole);
}
// now reset the collection
Roles = roles;
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
}
}
/// <summary>
/// Gets a value indicating whether the member is locked out
/// </summary>
public bool IsLockedOut
{
get
{
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating whether the member is approved
/// </summary>
public bool IsApproved { get; set; }
/// <summary>
/// Gets or sets the alias of the member type
/// </summary>
public string MemberTypeAlias { get; set; }
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
}
}

View File

@@ -0,0 +1,702 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Services;
using Umbraco.Core.Scoping;
using Umbraco.Core.Security;
using Umbraco.Extensions;
namespace Umbraco.Core.Security
{
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MembersUserStore : UserStoreBase<MembersIdentityUser, IdentityRole<string>, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
{
private readonly IMemberService _memberService;
private readonly UmbracoMapper _mapper;
private readonly IScopeProvider _scopeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="MembersUserStore"/> 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>
/// <param name="describer">The error describer</param>
public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override IQueryable<MembersIdentityUser> Users => throw new NotImplementedException();
/// <inheritdoc />
public override Task<string> GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// create member
IMember memberEntity = _memberService.CreateMember(
user.UserName,
user.Email,
user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name,
user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
UpdateMemberProperties(memberEntity, user);
// create the member
_memberService.Save(memberEntity);
if (!memberEntity.HasIdentity)
{
throw new DataException("Could not create the member, check logs for details");
}
// re-assign id
user.Id = UserIdToString(memberEntity.Id);
// [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(
// user.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<IdentityResult> UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Attempt<int> asInt = user.Id.TryConvertTo<int>();
if (asInt == false)
{
throw new InvalidOperationException("The user 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 = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
if (UpdateMemberProperties(found, user))
{
_memberService.Save(found);
}
// TODO: when to implement external login service?
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// found.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
}
scope.Complete();
}
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<IdentityResult> DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IMember found = _memberService.GetById(UserIdToInt(user.Id));
if (found != null)
{
_memberService.Delete(found);
}
// TODO: when to implement external login service?
//_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id));
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
/// <inheritdoc />
protected override Task<MembersIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember user = _memberService.GetById(UserIdToInt(userId));
if (user == null)
{
return Task.FromResult((MembersIdentityUser)null);
}
return Task.FromResult(AssignLoginsCallback(_mapper.Map<MembersIdentityUser>(user)));
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByNameAsync(string userName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember user = _memberService.GetByUsername(userName);
if (user == null)
{
return Task.FromResult((MembersIdentityUser)null);
}
MembersIdentityUser result = AssignLoginsCallback(_mapper.Map<MembersIdentityUser>(user));
return Task.FromResult(result);
}
/// <inheritdoc />
public override async Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
}
/// <inheritdoc />
public override async Task<bool> HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
// This checks if it's null
var result = await base.HasPasswordAsync(user, cancellationToken);
if (result)
{
// we also want to check empty
return string.IsNullOrEmpty(user.PasswordHash) == false;
}
return false;
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember member = _memberService.GetByEmail(email);
MembersIdentityUser result = member == null
? null
: _mapper.Map<MembersIdentityUser>(member);
return Task.FromResult(AssignLoginsCallback(result));
}
/// <inheritdoc />
public override Task<string> GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken)
=> GetEmailAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
/// <inheritdoc />
public override Task AddLoginAsync(MembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
ICollection<IIdentityUserLogin> logins = user.Logins;
var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString());
IdentityUserLogin userLogin = instance;
logins.Add(userLogin);
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task RemoveLoginAsync(MembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
if (userLogin != null)
{
user.Logins.Remove(userLogin);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task<IList<UserLoginInfo>> GetLoginsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult((IList<UserLoginInfo>)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
}
/// <inheritdoc />
protected override async Task<IdentityUserLogin<string>> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
if (user == null)
{
return null;
}
IList<UserLoginInfo> logins = await GetLoginsAsync(user, cancellationToken);
UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider);
if (found == null)
{
return null;
}
return new IdentityUserLogin<string>
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null
UserId = user.Id
};
}
/// <inheritdoc />
protected override Task<IdentityUserLogin<string>> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var logins = new List<IIdentityUserLogin>();
// TODO: external login needed?
//_externalLoginService.Find(loginProvider, providerKey).ToList();
if (logins.Count == 0)
{
return Task.FromResult((IdentityUserLogin<string>)null);
}
IIdentityUserLogin found = logins[0];
return Task.FromResult(new IdentityUserLogin<string>
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = null, // TODO: We don't store this value so it will be null
UserId = found.UserId
});
}
/// <inheritdoc />
public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole == null)
{
_memberService.AssignRole(user.UserName, role);
user.AddRole(role);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole != null)
{
_memberService.DissociateRole(user.UserName, userRole.RoleId);
user.Roles.Remove(userRole);
}
return Task.CompletedTask;
}
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IEnumerable<string> currentRoles = _memberService.GetAllRoles(user.UserName);
ICollection<IdentityUserRole<string>> roles = currentRoles.Select(role => new IdentityUserRole<string>
{
RoleId = role,
UserId = user.Id
}).ToList();
user.Roles = roles;
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
}
/// <summary>
/// Returns true if a user is in the role
/// </summary>
public override Task<bool> IsInRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
}
/// <summary>
/// Lists all users of a given role.
/// </summary>
public override Task<IList<MembersIdentityUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
IEnumerable<IMember> members = _memberService.GetMembersByMemberType(normalizedRoleName);
IList<MembersIdentityUser> membersIdentityUsers = members.Select(x => _mapper.Map<MembersIdentityUser>(x)).ToList();
return Task.FromResult(membersIdentityUsers);
}
/// <inheritdoc/>
protected override Task<IdentityRole<string>> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName);
if (group == null)
{
return Task.FromResult((IdentityRole<string>)null);
}
return Task.FromResult(new IdentityRole<string>(group.Name)
{
//TODO: what should the alias be?
Id = @group.Id.ToString()
});
}
/// <inheritdoc/>
protected override async Task<IdentityUserRole<string>> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken)
{
MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
if (user == null)
{
return null;
}
IdentityUserRole<string> found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId));
return found;
}
/// <inheritdoc />
public override Task<string> GetSecurityStampAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
? user.PasswordHash.GenerateHash()
: user.SecurityStamp);
}
private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user)
{
if (user != null)
{
//TODO: when to
//user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetAll(UserIdToInt(user.Id))));
}
return user;
}
private bool UpdateMemberProperties(IMember member, MembersIdentityUser identityUserMember)
{
var anythingChanged = false;
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastLoginDateUtc))
|| (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false)
|| (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value))
{
anythingChanged = true;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUserMember.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUserMember.LastLoginDateUtc.Value.ToLocalTime();
member.LastLoginDate = dt;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastPasswordChangeDateUtc))
|| (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value))
{
anythingChanged = true;
member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime();
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed))
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false)
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed))
{
anythingChanged = true;
member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Name))
&& member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Name = identityUserMember.Name;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Email))
&& member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Email = identityUserMember.Email;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.AccessFailedCount))
&& member.FailedPasswordAttempts != identityUserMember.AccessFailedCount)
{
anythingChanged = true;
member.FailedPasswordAttempts = identityUserMember.AccessFailedCount;
}
if (member.IsLockedOut != identityUserMember.IsLockedOut)
{
anythingChanged = true;
member.IsLockedOut = identityUserMember.IsLockedOut;
if (member.IsLockedOut)
{
// need to set the last lockout date
member.LastLockoutDate = DateTime.Now;
}
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.UserName))
&& member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Username = identityUserMember.UserName;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.PasswordHash))
&& member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.RawPasswordValue = identityUserMember.PasswordHash;
member.PasswordConfiguration = identityUserMember.PasswordConfig;
}
if (member.SecurityStamp != identityUserMember.SecurityStamp)
{
anythingChanged = true;
member.SecurityStamp = identityUserMember.SecurityStamp;
}
// TODO: Fix this for Groups too (as per backoffice comment)
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups)))
{
}
// reset all changes
identityUserMember.ResetDirtyProperties(false);
return anythingChanged;
}
private static int UserIdToInt(string userId)
{
Attempt<int> attempt = userId.TryConvertTo<int>();
if (attempt.Success)
{
return attempt.Result;
}
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
}
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<Claim>> GetClaimsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task AddClaimsAsync(MembersIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task ReplaceClaimAsync(MembersIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task RemoveClaimsAsync(MembersIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<MembersIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task<IdentityUserToken<string>> FindTokenAsync(MembersIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
}
}

View File

@@ -5,10 +5,8 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
/// </summary>
public class BackOfficeLookupNormalizer : ILookupNormalizer
public class NoopLookupNormalizer : ILookupNormalizer
{
// TODO: Do we need this?
public string NormalizeName(string name) => name;
public string NormalizeEmail(string email) => email;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
@@ -32,12 +33,11 @@ namespace Umbraco.Cms.Core.Security
IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<TUser>> logger,
IOptions<TPasswordConfig> passwordConfiguration)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoopLookupNormalizer(), errors, services, logger)
{
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
@@ -72,8 +72,8 @@ namespace Umbraco.Cms.Core.Security
/// Used to validate a user's session
/// </summary>
/// <param name="userId">The user id</param>
/// <param name="sessionId">The sesion id</param>
/// <returns>True if the sesion is valid, else false</returns>
/// <param name="sessionId">The session id</param>
/// <returns>True if the session is valid, else false</returns>
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore<TUser>;
@@ -88,26 +88,62 @@ namespace Umbraco.Cms.Core.Security
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
/// <param name="passwordConfiguration">The <see cref="IPasswordConfiguration"/></param>
/// <returns>An <see cref="IPasswordHasher{T}"/></returns>
protected virtual IPasswordHasher<TUser> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher<TUser>();
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns>The generated password</returns>
public string GeneratePassword()
{
if (_passwordGenerator == null)
_passwordGenerator ??= new PasswordGenerator(PasswordConfiguration);
string password = _passwordGenerator.GeneratePassword();
return password;
}
/// <summary>
/// Generates a hashed password based on the default password hasher
/// No existing identity user is required and this does not validate the password
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
public string HashPassword(string password)
{
string hashedPassword = PasswordHasher.HashPassword(null, password);
return hashedPassword;
}
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code
/// 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)
{
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
IdentityResult result = await v.ValidateAsync(this, null, password);
if (!result.Succeeded)
{
if (result.Errors.Any())
{
errors.AddRange(result.Errors);
}
isValid = false;
}
}
var password = _passwordGenerator.GeneratePassword();
return password;
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 />

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
using Umbraco.Extensions;
using Umbraco.Core.Services.Implement;
namespace Umbraco.Cms.Infrastructure.Services.Implement
{
@@ -111,19 +112,20 @@ namespace Umbraco.Cms.Infrastructure.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())
{
CreateMember(scope, member, 0, false);
scope.Complete();
}
using IScope scope = ScopeProvider.CreateScope();
CreateMember(scope, member, 0, false);
scope.Complete();
return member;
}
@@ -314,7 +316,9 @@ namespace Umbraco.Cms.Infrastructure.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);
@@ -323,7 +327,9 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
}
if (withIdentity == false)
{
return;
}
Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}");
}
@@ -803,11 +809,11 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
/// <inheritdoc />
public void Save(IMember member, bool raiseEvents = true)
{
//trimming username and email to make sure we have no trailing space
// trimming username and email to make sure we have no trailing space
member.Username = member.Username.Trim();
member.Email = member.Email.Trim();
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new SaveEventArgs<IMember>(member);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
@@ -830,6 +836,7 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs);
}
Audit(AuditType.Save, 0, member.Id);
scope.Complete();
@@ -926,18 +933,28 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
}
}
public IEnumerable<string> GetAllRoles()
/// <summary>
/// Returns a list of all member roles
/// </summary>
/// <returns>A list of member roles</returns>
public IEnumerable<IMemberGroup> GetAllRoles()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
return _memberGroupRepository.GetMany().Select(x => x.Name).Distinct();
return _memberGroupRepository.GetMany().Distinct();
}
}
/// <summary>
/// Returns a list of all member roles for a given member ID
/// </summary>
/// <param name="memberId"></param>
/// <returns>A list of member roles</returns>
public IEnumerable<string> GetAllRoles(int memberId)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
@@ -947,17 +964,17 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
public IEnumerable<string> GetAllRoles(string username)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(username);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Name).Distinct();
}
}
public IEnumerable<int> GetAllRolesIds()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
@@ -966,27 +983,27 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
public IEnumerable<int> GetAllRolesIds(int memberId)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable<int> GetAllRolesIds(string username)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(username);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable<IMember> GetMembersInRole(string roleName)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
return _memberRepository.GetByMemberGroup(roleName);
@@ -995,7 +1012,7 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
public IEnumerable<IMember> FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Cms.Core.Constants.Locks.MemberTree);
return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
@@ -1004,71 +1021,66 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
public bool DeleteRole(string roleName, bool throwIfBeingUsed)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
if (throwIfBeingUsed)
{
// get members in role
var membersInRole = _memberRepository.GetByMemberGroup(roleName);
IEnumerable<IMember> membersInRole = _memberRepository.GetByMemberGroup(roleName);
if (membersInRole.Any())
{
throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
}
}
var query = Query<IMemberGroup>().Where(g => g.Name == roleName);
var found = _memberGroupRepository.Get(query).ToArray();
IQuery<IMemberGroup> query = Query<IMemberGroup>().Where(g => g.Name == roleName);
IMemberGroup[] found = _memberGroupRepository.Get(query).ToArray();
foreach (var memberGroup in found)
foreach (IMemberGroup memberGroup in found)
{
_memberGroupService.Delete(memberGroup);
}
scope.Complete();
return found.Length > 0;
}
}
public void AssignRole(string username, string roleName)
{
AssignRoles(new[] { username }, new[] { roleName });
}
public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName });
public void AssignRoles(string[] usernames, string[] roleNames)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
var ids = _memberGroupRepository.GetMemberIds(usernames);
int[] ids = _memberGroupRepository.GetMemberIds(usernames);
_memberGroupRepository.AssignRoles(ids, roleNames);
scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles));
scope.Complete();
}
}
public void DissociateRole(string username, string roleName)
{
DissociateRoles(new[] { username }, new[] { roleName });
}
public void DissociateRole(string username, string roleName) => DissociateRoles(new[] { username }, new[] { roleName });
public void DissociateRoles(string[] usernames, string[] roleNames)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
var ids = _memberGroupRepository.GetMemberIds(usernames);
int[] ids = _memberGroupRepository.GetMemberIds(usernames);
_memberGroupRepository.DissociateRoles(ids, roleNames);
scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles));
scope.Complete();
}
}
public void AssignRole(int memberId, string roleName)
{
AssignRoles(new[] { memberId }, new[] { roleName });
}
public void AssignRole(int memberId, string roleName) => AssignRoles(new[] { memberId }, new[] { roleName });
public void AssignRoles(int[] memberIds, string[] roleNames)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
_memberGroupRepository.AssignRoles(memberIds, roleNames);
@@ -1077,14 +1089,11 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
}
}
public void DissociateRole(int memberId, string roleName)
{
DissociateRoles(new[] { memberId }, new[] { roleName });
}
public void DissociateRole(int memberId, string roleName) => DissociateRoles(new[] { memberId }, new[] { roleName });
public void DissociateRoles(int[] memberIds, string[] roleNames)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
_memberGroupRepository.DissociateRoles(memberIds, roleNames);
@@ -1097,10 +1106,7 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
#region Private Methods
private void Audit(AuditType type, int userId, int objectId, string message = null)
{
_auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message));
}
private void Audit(AuditType type, int userId, int objectId, string message = null) => _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message));
#endregion
@@ -1155,12 +1161,15 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
/// </remarks>
public MemberExportModel ExportMember(Guid key)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
var query = Query<IMember>().Where(x => x.Key == key);
var member = _memberRepository.Get(query).FirstOrDefault();
IQuery<IMember> query = Query<IMember>().Where(x => x.Key == key);
IMember member = _memberRepository.Get(query).FirstOrDefault();
if (member == null) return null;
if (member == null)
{
return null;
}
var model = new MemberExportModel
{
@@ -1184,11 +1193,14 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
private static IEnumerable<MemberExportProperty> GetPropertyExportItems(IMember member)
{
if (member == null) throw new ArgumentNullException(nameof(member));
if (member == null)
{
throw new ArgumentNullException(nameof(member));
}
var exportProperties = new List<MemberExportProperty>();
foreach (var property in member.Properties)
foreach (IProperty property in member.Properties)
{
var propertyExportModel = new MemberExportProperty
{
@@ -1216,15 +1228,14 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
public void DeleteMembersOfType(int memberTypeId)
{
// note: no tree to manage here
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.MemberTree);
// TODO: What about content that has the contenttype as part of its composition?
var query = Query<IMember>().Where(x => x.ContentTypeId == memberTypeId);
IQuery<IMember> query = Query<IMember>().Where(x => x.ContentTypeId == memberTypeId);
var members = _memberRepository.Get(query).ToArray();
IMember[] members = _memberRepository.Get(query).ToArray();
var deleteEventArgs = new DeleteEventArgs<IMember>(members);
if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs))
@@ -1233,43 +1244,58 @@ namespace Umbraco.Cms.Infrastructure.Services.Implement
return;
}
foreach (var member in members)
foreach (IMember member in members)
{
// delete media
// triggers the deleted event (and handles the files)
DeleteLocked(scope, member);
}
scope.Complete();
}
}
private IMemberType GetMemberType(IScope scope, string memberTypeAlias)
{
if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias));
if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
if (memberTypeAlias == null)
{
throw new ArgumentNullException(nameof(memberTypeAlias));
}
if (string.IsNullOrWhiteSpace(memberTypeAlias))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
}
scope.ReadLock(Cms.Core.Constants.Locks.MemberTypes);
var memberType = _memberTypeRepository.Get(memberTypeAlias);
IMemberType memberType = _memberTypeRepository.Get(memberTypeAlias);
if (memberType == null)
{
throw new Exception($"No MemberType matching the passed in Alias: '{memberTypeAlias}' was found"); // causes rollback
}
return memberType;
}
private IMemberType GetMemberType(string memberTypeAlias)
{
if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias));
if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
if (memberTypeAlias == null)
{
throw new ArgumentNullException(nameof(memberTypeAlias));
}
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
if (string.IsNullOrWhiteSpace(memberTypeAlias))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
}
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
return GetMemberType(scope, memberTypeAlias);
}
}
#endregion
}
}

View File

@@ -156,6 +156,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
.AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme)
.AddPreviewSupport()
.AddMvcAndRazor(mvcBuilding: mvcBuilder =>

View File

@@ -216,6 +216,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing
.AddRuntimeMinifier()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
.AddMembersIdentity()
.AddTestServices(TestHelper, GetAppCaches());
if (TestOptions.Mapper)

View File

@@ -185,10 +185,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
{
MemberService.AddRole("MyTestRole");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(1, found.Count());
Assert.AreEqual("MyTestRole", found.Single());
Assert.AreEqual("MyTestRole", found.Single().Name);
}
[Test]
@@ -197,10 +197,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.AddRole("MyTestRole");
MemberService.AddRole("MyTestRole");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(1, found.Count());
Assert.AreEqual("MyTestRole", found.Single());
Assert.AreEqual("MyTestRole", found.Single().Name);
}
[Test]
@@ -210,7 +210,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.AddRole("MyTestRole2");
MemberService.AddRole("MyTestRole3");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(3, found.Count());
}
@@ -294,7 +294,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.DeleteRole("MyTestRole1", false);
IEnumerable<string> memberRoles = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> memberRoles = MemberService.GetAllRoles();
Assert.AreEqual(0, memberRoles.Count());
}

View File

@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Core.Security;
using Umbraco.Extensions;
namespace Umbraco.Tests.Integration.Umbraco.Web.Common
{
[TestFixture]
public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest
{
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddMembersIdentity();
[Test]
public void AddMembersIdentity_ExpectMembersUserStoreResolvable()
{
IUserStore<MembersIdentityUser> userStore = Services.GetService<IUserStore<MembersIdentityUser>>();
Assert.IsNotNull(userStore);
Assert.AreEqual(typeof(MembersUserStore), userStore.GetType());
}
[Test]
public void AddMembersIdentity_ExpectMembersUserManagerResolvable()
{
IMemberManager userManager = Services.GetService<IMemberManager>();
Assert.NotNull(userManager);
}
}
}

View File

@@ -51,8 +51,10 @@ namespace Umbraco.Cms.Tests.UnitTests.AutoFixture
.Customize(new ConstructorCustomization(typeof(UsersController), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(InstallController), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(PreviewController), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(MemberController), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(BackOfficeController), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery()));
.Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery()))
.Customize(new ConstructorCustomization(typeof(MemberManager), new GreedyConstructorQuery()));
fixture.Customize(new AutoMoqCustomization());

View File

@@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using NUnit.Framework;
using Umbraco.Extensions;
using Umbraco.Core.Security;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.BackOffice
{

View File

@@ -1,60 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using NUnit.Framework;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackOffice
{
public class BackOfficeLookupNormalizerTests
{
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
Assert.AreEqual(name, normalizedName);
}
[Test]
public void NormalizeName_Expect_Input_Returned()
{
var name = Guid.NewGuid().ToString();
var sut = new BackOfficeLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
Assert.AreEqual(name, normalizedName);
}
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
[Test]
public void NormalizeEmail_Expect_Input_Returned()
{
var email = $"{Guid.NewGuid()}@umbraco";
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Net;
using Umbraco.Core.Security;
using Umbraco.Web.Common.Security;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberIdentityUserManagerTests
{
private Mock<IUserStore<MembersIdentityUser>> _mockMemberStore;
private Mock<IOptions<MembersIdentityOptions>> _mockIdentityOptions;
private Mock<IPasswordHasher<MembersIdentityUser>> _mockPasswordHasher;
private Mock<IUserValidator<MembersIdentityUser>> _mockUserValidators;
private Mock<IEnumerable<IPasswordValidator<MembersIdentityUser>>> _mockPasswordValidators;
private Mock<ILookupNormalizer> _mockNormalizer;
private IdentityErrorDescriber _mockErrorDescriber;
private Mock<IServiceProvider> _mockServiceProviders;
private Mock<ILogger<UserManager<MembersIdentityUser>>> _mockLogger;
private Mock<IOptions<MemberPasswordConfigurationSettings>> _mockPasswordConfiguration;
public MemberManager CreateSut()
{
_mockMemberStore = new Mock<IUserStore<MembersIdentityUser>>();
_mockIdentityOptions = new Mock<IOptions<MembersIdentityOptions>>();
var idOptions = new MembersIdentityOptions { Lockout = { AllowedForNewUsers = false } };
_mockIdentityOptions.Setup(o => o.Value).Returns(idOptions);
_mockPasswordHasher = new Mock<IPasswordHasher<MembersIdentityUser>>();
var userValidators = new List<IUserValidator<MembersIdentityUser>>();
_mockUserValidators = new Mock<IUserValidator<MembersIdentityUser>>();
var validator = new Mock<IUserValidator<MembersIdentityUser>>();
userValidators.Add(validator.Object);
_mockPasswordValidators = new Mock<IEnumerable<IPasswordValidator<MembersIdentityUser>>>();
_mockNormalizer = new Mock<ILookupNormalizer>();
_mockErrorDescriber = new IdentityErrorDescriber();
_mockServiceProviders = new Mock<IServiceProvider>();
_mockLogger = new Mock<ILogger<UserManager<MembersIdentityUser>>>();
_mockPasswordConfiguration = new Mock<IOptions<MemberPasswordConfigurationSettings>>();
_mockPasswordConfiguration.Setup(x => x.Value).Returns(() =>
new MemberPasswordConfigurationSettings()
{
});
var pwdValidators = new List<PasswordValidator<MembersIdentityUser>>
{
new PasswordValidator<MembersIdentityUser>()
};
var userManager = new MemberManager(
new Mock<IIpResolver>().Object,
_mockMemberStore.Object,
_mockIdentityOptions.Object,
_mockPasswordHasher.Object,
userValidators,
pwdValidators,
new BackOfficeIdentityErrorDescriber(),
_mockServiceProviders.Object,
new Mock<IHttpContextAccessor>().Object,
new Mock<ILogger<UserManager<MembersIdentityUser>>>().Object,
_mockPasswordConfiguration.Object);
validator.Setup(v => v.ValidateAsync(
userManager,
It.IsAny<MembersIdentityUser>()))
.Returns(Task.FromResult(IdentityResult.Success)).Verifiable();
return userManager;
}
[Test]
public async Task GivenICreateUser_AndTheIdentityResultFailed_ThenIShouldGetAFailedResultAsync()
{
//arrange
MemberManager sut = CreateSut();
MembersIdentityUser fakeUser = new MembersIdentityUser()
{
PasswordConfig = "testConfig"
};
CancellationToken fakeCancellationToken = new CancellationToken() { };
IdentityError[] identityErrors =
{
new IdentityError()
{
Code = "IdentityError1",
Description = "There was an identity error when creating a user"
}
};
_mockMemberStore.Setup(x =>
x.CreateAsync(fakeUser, fakeCancellationToken))
.ReturnsAsync(IdentityResult.Failed(identityErrors));
//act
IdentityResult identityResult = await sut.CreateAsync(fakeUser);
//assert
Assert.IsFalse(identityResult.Succeeded);
Assert.IsFalse(!identityResult.Errors.Any());
}
[Test]
public async Task GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync()
{
//arrange
MemberManager sut = CreateSut();
CancellationToken fakeCancellationToken = new CancellationToken() { };
IdentityError[] identityErrors =
{
new IdentityError()
{
Code = "IdentityError1",
Description = "There was an identity error when creating a user"
}
};
_mockMemberStore.Setup(x =>
x.CreateAsync(null, fakeCancellationToken))
.ReturnsAsync(IdentityResult.Failed(identityErrors));
//act
var identityResult = new Func<Task<IdentityResult>>(() => sut.CreateAsync(null));
//assert
Assert.That(identityResult, Throws.ArgumentNullException);
}
[Test]
public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync()
{
//arrange
MemberManager sut = CreateSut();
MembersIdentityUser fakeUser = new MembersIdentityUser()
{
PasswordConfig = "testConfig"
};
CancellationToken fakeCancellationToken = new CancellationToken() { };
_mockMemberStore.Setup(x =>
x.CreateAsync(fakeUser, fakeCancellationToken))
.ReturnsAsync(IdentityResult.Success);
//act
IdentityResult identityResult = await sut.CreateAsync(fakeUser);
//assert
Assert.IsTrue(identityResult.Succeeded);
Assert.IsTrue(!identityResult.Errors.Any());
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
using Umbraco.Core.Models;
using Umbraco.Core.Scoping;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberIdentityUserStoreTests
{
private Mock<IMemberService> _mockMemberService;
public MembersUserStore CreateSut()
{
_mockMemberService = new Mock<IMemberService>();
return new MembersUserStore(
_mockMemberService.Object,
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())),
new Mock<IScopeProvider>().Object,
new IdentityErrorDescriber());
}
[Test]
public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync()
{
// arrange
MembersUserStore sut = CreateSut();
CancellationToken fakeCancellationToken = new CancellationToken(){};
// act
Action actual = () => sut.CreateAsync(null, fakeCancellationToken);
// assert
Assert.That(actual, Throws.ArgumentNullException);
}
[Test]
public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync()
{
// arrange
MembersUserStore sut = CreateSut();
var fakeUser = new MembersIdentityUser() { };
var fakeCancellationToken = new CancellationToken() { };
IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77);
IMember mockMember = Mock.Of<IMember>(m =>
m.Name == "fakeName" &&
m.Email == "fakeemail@umbraco.com" &&
m.Username == "fakeUsername" &&
m.RawPasswordValue == "fakePassword" &&
m.ContentTypeAlias == fakeMemberType.Alias &&
m.HasIdentity == true);
bool raiseEvents = false;
_mockMemberService.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(mockMember);
_mockMemberService.Setup(x => x.Save(mockMember, raiseEvents));
// act
IdentityResult identityResult = await sut.CreateAsync(fakeUser, fakeCancellationToken);
// assert
Assert.IsTrue(identityResult.Succeeded);
Assert.IsTrue(!identityResult.Errors.Any());
}
//GetPasswordHashAsync
//GetUserIdAsync
}
}

View File

@@ -5,17 +5,15 @@ using System;
using NUnit.Framework;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.BackOffice
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
{
public class NopLookupNormalizerTests
public class NoopLookupNormalizerTests
{
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
public void NormalizeName_Expect_Input_Returned()
{
var sut = new BackOfficeLookupNormalizer();
var name = Guid.NewGuid().ToString();
var sut = new NoopLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
@@ -23,10 +21,23 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.BackOffice
}
[Test]
public void NormalizeName_Expect_Input_Returned()
public void NormalizeEmail_Expect_Input_Returned()
{
var name = Guid.NewGuid().ToString();
var sut = new BackOfficeLookupNormalizer();
var email = $"{Guid.NewGuid()}@umbraco";
var sut = new NoopLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
{
var sut = new NoopLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
@@ -39,18 +50,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.BackOffice
[TestCase(" ")]
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
[Test]
public void NormalizeEmail_Expect_Input_Returned()
{
var email = $"{Guid.NewGuid()}@umbraco";
var sut = new BackOfficeLookupNormalizer();
var sut = new NoopLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);

View File

@@ -0,0 +1,564 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Common;
using AutoFixture.NUnit3;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.ContentApps;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Mapping;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Cms.Web.BackOffice.Mapping;
using Umbraco.Cms.Web.Common.ActionsResults;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
{
[TestFixture]
public class MemberControllerUnitTests
{
private UmbracoMapper _mapper;
[Test]
[AutoMoqData]
public void PostSaveMember_WhenMemberIsNull_ExpectFailureResponse(
MemberController sut)
{
// arrange
// act
ArgumentNullException exception = Assert.ThrowsAsync<ArgumentNullException>(() => sut.PostSave(null));
// assert
Assert.That(exception.Message, Is.EqualTo("Value cannot be null. (Parameter 'The member content item was null')"));
}
[Test]
[AutoMoqData]
public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
sut.ModelState.AddModelError("key", "Invalid model state");
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
var value = new MemberDisplay();
string reason = "Validation failed";
// act
ActionResult<MemberDisplay> result = sut.PostSave(fakeMemberData).Result;
var validation = result.Result as ValidationErrorResult;
// assert
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Value);
Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(() => new MembersIdentityUser());
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
string reason = "Validation failed";
// act
ActionResult<MemberDisplay> result = sut.PostSave(fakeMemberData).Result;
var validation = result.Result as ValidationErrorResult;
// assert
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Value);
Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectSuccessResponse(
[Frozen] IMemberManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
var roleName = "anyrole";
IMember member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save);
fakeMemberData.Groups = new List<string>()
{
roleName
};
var membersIdentityUser = new MembersIdentityUser();
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(() => membersIdentityUser);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.GetRolesAsync(membersIdentityUser));
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName }));
Mock.Get(memberService)
.Verify(m => m.Save(It.IsAny<Member>(), true));
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
/// <summary>
/// Create member controller to test
/// </summary>
/// <param name="memberService">Member service</param>
/// <param name="memberTypeService">Member type service</param>
/// <param name="memberGroupService">Member group service</param>
/// <param name="membersUserManager">Members user manager</param>
/// <param name="dataTypeService">Data type service</param>
/// <param name="backOfficeSecurityAccessor">Back office security accessor</param>
/// <returns>A member controller for the tests</returns>
private MemberController CreateSut(
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IMemberManager membersUserManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
var mockShortStringHelper = new MockShortStringHelper();
var textService = new Mock<ILocalizedTextService>();
var contentTypeBaseServiceProvider = new Mock<IContentTypeBaseServiceProvider>();
contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny<IContentBase>())).Returns(new ContentType(mockShortStringHelper, 123));
var contentAppFactories = new Mock<List<IContentAppFactory>>();
var mockContentAppFactoryCollection = new Mock<ILogger<ContentAppFactoryCollection>>();
var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache());
var contentAppFactoryCollection = new ContentAppFactoryCollection(
contentAppFactories.Object,
mockContentAppFactoryCollection.Object,
hybridBackOfficeSecurityAccessor);
var mockUserService = new Mock<IUserService>();
var commonMapper = new CommonMapper(
mockUserService.Object,
contentTypeBaseServiceProvider.Object,
contentAppFactoryCollection,
textService.Object);
var mockCultureDictionary = new Mock<ICultureDictionary>();
var mockPasswordConfig = new Mock<IOptions<MemberPasswordConfigurationSettings>>();
mockPasswordConfig.Setup(x => x.Value).Returns(() => new MemberPasswordConfigurationSettings());
IDataEditor dataEditor = Mock.Of<IDataEditor>(
x => x.Type == EditorType.PropertyValue
&& x.Alias == Constants.PropertyEditors.Aliases.Label);
Mock.Get(dataEditor).Setup(x => x.GetValueEditor()).Returns(new TextOnlyValueEditor(Mock.Of<IDataTypeService>(), Mock.Of<ILocalizationService>(), new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox, "Test Textbox", "textbox"), textService.Object, Mock.Of<IShortStringHelper>(), Mock.Of<IJsonSerializer>()));
var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
IMapDefinition memberMapDefinition = new MemberMapDefinition(
commonMapper,
new CommonTreeNodeMapper(Mock.Of<LinkGenerator>()),
new MemberTabsAndPropertiesMapper(
mockCultureDictionary.Object,
backOfficeSecurityAccessor,
textService.Object,
memberTypeService,
memberService,
memberGroupService,
mockPasswordConfig.Object,
contentTypeBaseServiceProvider.Object,
propertyEditorCollection),
new HttpContextAccessor());
var map = new MapDefinitionCollection(new List<IMapDefinition>()
{
new global::Umbraco.Core.Models.Mapping.MemberMapDefinition(),
memberMapDefinition,
new ContentTypeMapDefinition(
commonMapper,
propertyEditorCollection,
dataTypeService,
new Mock<IFileService>().Object,
new Mock<IContentTypeService>().Object,
new Mock<IMediaTypeService>().Object,
memberTypeService,
new Mock<ILoggerFactory>().Object,
mockShortStringHelper,
new Mock<IOptions<GlobalSettings>>().Object,
new Mock<IHostingEnvironment>().Object)
});
_mapper = new UmbracoMapper(map);
return new MemberController(
new DefaultCultureDictionary(
new Mock<ILocalizationService>().Object,
new HttpRequestAppCache(() => null)),
new LoggerFactory(),
mockShortStringHelper,
new DefaultEventMessagesFactory(
new Mock<IEventMessagesAccessor>().Object),
textService.Object,
propertyEditorCollection,
_mapper,
memberService,
memberTypeService,
membersUserManager,
dataTypeService,
backOfficeSecurityAccessor,
new ConfigurationEditorJsonSerializer());
}
/// <summary>
/// Setup all standard member data for test
/// </summary>
private Member SetupMemberTestData(
out MemberSave fakeMemberData,
out MemberDisplay memberDisplay,
ContentSaveAction contentAction)
{
// arrange
MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "test@example.com", "123", "test");
int memberId = 123;
member.Id = memberId;
//TODO: replace with builder for MemberSave and MemberDisplay
fakeMemberData = new MemberSave()
{
Id = memberId,
SortOrder = member.SortOrder,
ContentTypeId = memberType.Id,
Key = member.Key,
Password = new ChangingPasswordModel()
{
Id = 456,
NewPassword = member.RawPasswordValue,
OldPassword = null
},
Name = member.Name,
Email = member.Email,
Username = member.Username,
PersistedContent = member,
PropertyCollectionDto = new ContentPropertyCollectionDto()
{
},
Groups = new List<string>(),
//Alias = "fakeAlias",
ContentTypeAlias = member.ContentTypeAlias,
Action = contentAction,
Icon = "icon-document",
Path = member.Path
};
memberDisplay = new MemberDisplay()
{
Id = memberId,
SortOrder = member.SortOrder,
ContentTypeId = memberType.Id,
Key = member.Key,
Name = member.Name,
Email = member.Email,
Username = member.Username,
//Alias = "fakeAlias",
ContentTypeAlias = member.ContentTypeAlias,
ContentType = new ContentTypeBasic(),
ContentTypeName = member.ContentType.Name,
Icon = fakeMemberData.Icon,
Path = member.Path,
Tabs = new List<Tab<ContentPropertyDisplay>>()
{
new Tab<ContentPropertyDisplay>()
{
Alias = "test",
Id = 77,
Properties = new List<ContentPropertyDisplay>()
{
new ContentPropertyDisplay()
{
Alias = "_umb_id",
View = "idwithguid",
Value = new []
{
"123",
"guid"
}
},
new ContentPropertyDisplay()
{
Alias = "_umb_doctype"
},
new ContentPropertyDisplay()
{
Alias = "_umb_login"
},
new ContentPropertyDisplay()
{
Alias= "_umb_email"
},
new ContentPropertyDisplay()
{
Alias = "_umb_password"
},
new ContentPropertyDisplay()
{
Alias = "_umb_membergroup"
}
}
}
}
};
return member;
}
/// <summary>
/// Check all member properties are equal
/// </summary>
/// <param name="memberDisplay"></param>
/// <param name="resultValue"></param>
private void AssertMemberDisplayPropertiesAreEqual(MemberDisplay memberDisplay, MemberDisplay resultValue)
{
Assert.AreNotSame(memberDisplay, resultValue);
Assert.AreEqual(memberDisplay.Id, resultValue.Id);
Assert.AreEqual(memberDisplay.Alias, resultValue.Alias);
Assert.AreEqual(memberDisplay.Username, resultValue.Username);
Assert.AreEqual(memberDisplay.Email, resultValue.Email);
Assert.AreEqual(memberDisplay.AdditionalData, resultValue.AdditionalData);
Assert.AreEqual(memberDisplay.ContentApps, resultValue.ContentApps);
Assert.AreEqual(memberDisplay.ContentType.Alias, resultValue.ContentType.Alias);
Assert.AreEqual(memberDisplay.ContentTypeAlias, resultValue.ContentTypeAlias);
Assert.AreEqual(memberDisplay.ContentTypeName, resultValue.ContentTypeName);
Assert.AreEqual(memberDisplay.ContentTypeId, resultValue.ContentTypeId);
Assert.AreEqual(memberDisplay.Icon, resultValue.Icon);
Assert.AreEqual(memberDisplay.Errors, resultValue.Errors);
Assert.AreEqual(memberDisplay.Key, resultValue.Key);
Assert.AreEqual(memberDisplay.Name, resultValue.Name);
Assert.AreEqual(memberDisplay.Path, resultValue.Path);
Assert.AreEqual(memberDisplay.SortOrder, resultValue.SortOrder);
Assert.AreEqual(memberDisplay.Trashed, resultValue.Trashed);
Assert.AreEqual(memberDisplay.TreeNodeUrl, resultValue.TreeNodeUrl);
//TODO: can we check create/update dates when saving?
//Assert.AreEqual(memberDisplay.CreateDate, resultValue.CreateDate);
//Assert.AreEqual(memberDisplay.UpdateDate, resultValue.UpdateDate);
//TODO: check all properties
Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count());
Assert.AreNotSame(memberDisplay.Properties, resultValue.Properties);
for (var index = 0; index < resultValue.Properties.Count(); index++)
{
Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index));
//Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index));
}
}
}
}

View File

@@ -70,10 +70,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
/// </summary>
protected ILocalizedTextService LocalizedTextService { get; }
/// <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)
{
ModelState.AddModelError("id", $"content with id: {id} was not found");
var errorResponse = NotFound(ModelState);
NotFoundObjectResult errorResponse = NotFound(ModelState);
return errorResponse;
}
@@ -90,7 +98,7 @@ namespace Umbraco.Cms.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)
@@ -101,42 +109,53 @@ namespace Umbraco.Cms.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)
{
ContentKey = contentItem.PersistedContent.Key,
PropertyTypeKey = property.PropertyType.Key,
Files = files
Files = files
};
// 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);
}
}
}
@@ -153,38 +172,45 @@ namespace Umbraco.Cms.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

@@ -8,9 +8,9 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.ContentApps;
@@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
@@ -46,46 +47,72 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[OutgoingNoHyphenGuidFormat]
public class MemberController : ContentControllerBase
{
private readonly MemberPasswordConfigurationSettings _passwordConfig;
private readonly PropertyEditorCollection _propertyEditors;
private readonly LegacyPasswordSecurity _passwordSecurity;
private readonly UmbracoMapper _umbracoMapper;
private readonly IMemberService _memberService;
private readonly IMemberTypeService _memberTypeService;
private readonly IMemberManager _memberManager;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IJsonSerializer _jsonSerializer;
private readonly IShortStringHelper _shortStringHelper;
/// <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,
IShortStringHelper shortStringHelper,
IEventMessagesFactory eventMessages,
ILocalizedTextService localizedTextService,
IOptions<MemberPasswordConfigurationSettings> passwordConfig,
PropertyEditorCollection propertyEditors,
LegacyPasswordSecurity passwordSecurity,
UmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberManager memberManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IJsonSerializer jsonSerializer)
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
{
_passwordConfig = passwordConfig.Value;
_propertyEditors = propertyEditors;
_passwordSecurity = passwordSecurity;
_umbracoMapper = umbracoMapper;
_memberService = memberService;
_memberTypeService = memberTypeService;
_memberManager = memberManager;
_dataTypeService = dataTypeService;
_localizedTextService = localizedTextService;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_jsonSerializer = jsonSerializer;
_shortStringHelper = shortStringHelper;
}
/// <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,
@@ -101,8 +128,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
}
var members = _memberService
.GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray();
IMember[] members = _memberService.GetAll(
pageNumber - 1,
pageSize,
out var totalRecords,
orderBy,
orderDirection,
orderBySystemField,
memberTypeAlias,
filter).ToArray();
if (totalRecords == 0)
{
return new PagedResult<MemberBasic>(0, 0, 0);
@@ -110,8 +144,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var pagedResult = new PagedResult<MemberBasic>(totalRecords, pageNumber, pageSize)
{
Items = members
.Select(x => _umbracoMapper.Map<MemberBasic>(x))
Items = members.Select(x => _umbracoMapper.Map<MemberBasic>(x))
};
return pagedResult;
}
@@ -119,15 +152,15 @@ namespace Umbraco.Cms.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);
var name = foundType != null ? foundType.Name : listName;
IMemberType foundType = _memberTypeService.Get(listName);
string name = foundType != null ? foundType.Name : listName;
var apps = new List<ContentApp>();
apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Constants.DataTypes.DefaultMembersListView));
apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView));
apps[0].Active = true;
var display = new MemberListDisplay
@@ -148,138 +181,130 @@ namespace Umbraco.Cms.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)
{
var foundMember = _memberService.GetByKey(key);
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 ActionResult<MemberDisplay> GetEmpty(string contentTypeAlias = null)
{
IMember emptyContent;
if (contentTypeAlias == null)
{
return NotFound();
}
var contentType = _memberTypeService.Get(contentTypeAlias);
IMemberType contentType = _memberTypeService.Get(contentTypeAlias);
if (contentType == null)
{
return NotFound();
}
var passwordGenerator = new PasswordGenerator(_passwordConfig);
string newPassword = _memberManager.GeneratePassword();
emptyContent = new Member(contentType);
emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword();
IMember emptyContent = new Member(contentType);
emptyContent.AdditionalData["NewPassword"] = newPassword;
return _umbracoMapper.Map<MemberDisplay>(emptyContent);
}
/// <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("The member content item was null");
}
//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);
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
// 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();
return new ValidationErrorResult(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.
var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
//find the ones to remove and remove them
var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray();
//Depending on the action we need to first do a create or update using the membership provider
// 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);
ActionResult<bool> updateSuccessful = await UpdateMemberAsync(contentItem);
if (!(updateSuccessful.Result is null))
{
return updateSuccessful.Result;
}
break;
case ContentSaveAction.SaveNew:
contentItem.PersistedContent = CreateMemberData(contentItem);
ActionResult<bool> createSuccessful = await CreateMemberAsync(contentItem);
if (!(createSuccessful.Result is null))
{
return createSuccessful.Result;
}
break;
default:
//we don't support anything else for members
// we don't support anything else for members
return 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
//create/save the IMember
_memberService.Save(contentItem.PersistedContent);
// return the updated model
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
//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(currRoles).ToArray();
if (toAdd.Any())
{
//add the ones submitted
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
}
//return the updated model
var 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
if (!ModelState.IsValid)
{
display.Errors = ModelState.ToErrorDictionary();
return new ValidationErrorResult(display, StatusCodes.Status403Forbidden);
}
var localizedTextService = _localizedTextService;
//put the correct messages in
ILocalizedTextService localizedTextService = _localizedTextService;
// put the correct messages in
switch (contentItem.Action)
{
case ContentSaveAction.Save:
case ContentSaveAction.SaveNew:
display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved"));
display.AddSuccessNotification(
localizedTextService.Localize("speechBubbles/editMemberSaved"),
localizedTextService.Localize("speechBubbles/editMemberSaved"));
break;
}
@@ -289,81 +314,121 @@ namespace Umbraco.Cms.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)
{
UpdateName(contentItem);
// 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
}
private IMember CreateMemberData(MemberSave contentItem)
/// <summary>
/// Create a member from the supplied member content data
///
/// All member password processing and creation is done via the identity manager
/// </summary>
/// <param name="contentItem">Member content data</param>
/// <returns>The identity result of the created member</returns>
private async Task<ActionResult<bool>> CreateMemberAsync(MemberSave contentItem)
{
throw new NotImplementedException("Members have not been migrated to netcore");
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
if (memberType == null)
{
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
}
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
var identityMember = MembersIdentityUser.CreateNew(
contentItem.Username,
contentItem.Email,
memberType.Alias,
contentItem.Name);
//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
//};
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
//return member;
if (created.Succeeded == false)
{
return new ValidationErrorResult(created.Errors.ToErrorMessage());
}
// now re-look up the member, which will now exist
IMember member = _memberService.GetByEmail(contentItem.Email);
// map the save info over onto the user
member = _umbracoMapper.Map<MemberSave, IMember>(contentItem, member);
int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
member.CreatorId = creatorId;
// assign the mapped property values that are not part of the identity properties
string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
foreach (ContentPropertyBasic property in contentItem.Properties)
{
if (builtInAliases.Contains(property.Alias) == false)
{
member.Properties[property.Alias].SetValue(property.Value);
}
}
//TODO: do we need to resave the key?
//contentItem.PersistedContent.Key = contentItem.Key;
// now the member has been saved via identity, resave the member with mapped content properties
_memberService.Save(member);
contentItem.PersistedContent = member;
await AddOrUpdateRoles(contentItem, identityMember);
return true;
}
/// <summary>
/// Update the member security data
/// </summary>
/// <param name="contentItem"></param>
/// <returns>
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
/// </returns>
private void UpdateMemberData(MemberSave contentItem)
/// </summary>
/// <param name="contentItem">The member to save</param>
private async Task<ActionResult<bool>> UpdateMemberAsync(MemberSave contentItem)
{
contentItem.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
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
// 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(contentItem.PersistedContent.ContentTypeId);
IMemberType memberType = _memberTypeService.Get(contentItem.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 = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
ContentPropertyBasic destProp = contentItem.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 = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
// if found, change the value of the contentItem model to the persisted value so it remains unchanged
object origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
destProp.Value = origValue;
}
}
}
var isLockedOut = contentItem.IsLockedOut;
bool isLockedOut = contentItem.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 (contentItem.PersistedContent.IsLockedOut && isLockedOut == false)
{
contentItem.PersistedContent.IsLockedOut = false;
@@ -371,90 +436,153 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
else if (!contentItem.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 (contentItem.Password == null)
return;
throw new NotImplementedException("Members have not been migrated to netcore");
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
// set the password
//contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword);
}
private static void UpdateName(MemberSave memberSave)
{
//Don't update the name if it is empty
if (memberSave.Name.IsNullOrWhiteSpace() == false)
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
if (identityMember == null)
{
memberSave.PersistedContent.Name = memberSave.Name;
return new ValidationErrorResult("Member was not found");
}
if (contentItem.Password != null)
{
IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
if (validatePassword.Succeeded == false)
{
return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage());
}
string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword);
identityMember.PasswordHash = newPassword;
contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash;
if (identityMember.LastPasswordChangeDateUtc != null)
{
contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow;
identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate;
}
}
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);
if (updatedResult.Succeeded == false)
{
return new ValidationErrorResult(updatedResult.Errors.ToErrorMessage());
}
_memberService.Save(contentItem.PersistedContent);
await AddOrUpdateRoles(contentItem, identityMember);
return true;
}
// TODO: This logic should be pulled into the service layer
private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
{
if (contentItem.Name.IsNullOrWhiteSpace())
{
ModelState.AddPropertyError(
new ValidationResult("Invalid user name", new[] { "value" }),
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Invalid user name", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
return false;
}
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
{
//TODO implement when NETCORE members are implemented
throw new NotImplementedException("TODO implement when members are implemented");
// var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword);
// if (!validPassword)
// {
// ModelState.AddPropertyError(
// new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }),
// string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
// return false;
// }
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;
}
}
var byUsername = _memberService.GetByUsername(contentItem.Username);
IMember byUsername = _memberService.GetByUsername(contentItem.Username);
if (byUsername != null && byUsername.Key != contentItem.Key)
{
ModelState.AddPropertyError(
new ValidationResult("Username is already in use", new[] { "value" }),
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Username is already in use", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
return false;
}
var byEmail = _memberService.GetByEmail(contentItem.Email);
IMember byEmail = _memberService.GetByEmail(contentItem.Email);
if (byEmail != null && byEmail.Key != contentItem.Key)
{
ModelState.AddPropertyError(
new ValidationResult("Email address is already in use", new[] { "value" }),
string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Email address is already in use", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
return false;
}
return true;
}
private string MapErrors(IEnumerable<IdentityError> result)
{
var sb = new StringBuilder();
IEnumerable<IdentityError> identityErrors = result.ToList();
foreach (IdentityError error in identityErrors)
{
string errorString = $"{error.Description}";
sb.AppendLine(errorString);
}
return sb.ToString();
}
/// <summary>
/// Add or update the identity roles
/// </summary>
/// <param name="contentItem">The member content item</param>
/// <param name="identityMember">The member as an identity user</param>
private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember)
{
// 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 = await _memberManager.GetRolesAsync(identityMember);
// 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())
{
IdentityResult rolesIdentityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove);
}
// find the ones to add and add them
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
if (toAdd.Any())
{
// add the ones submitted
IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd);
}
}
/// <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);
//TODO: move to MembersUserStore
IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
return HandleContentNotFound(key);
}
_memberService.Delete(foundMember);
return Ok();
@@ -468,25 +596,27 @@ namespace Umbraco.Cms.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);
return File( Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
return File(Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
}
}
}

View File

@@ -56,7 +56,7 @@ namespace Umbraco.Extensions
services.TryAddScoped<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>, UserClaimsPrincipalFactory<BackOfficeIdentityUser>>();
// CUSTOM:
services.TryAddScoped<BackOfficeLookupNormalizer>();
services.TryAddScoped<NoopLookupNormalizer>();
services.TryAddScoped<BackOfficeIdentityErrorDescriber>();
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
@@ -68,7 +68,7 @@ namespace Umbraco.Extensions
* To validate the container the following registrations are required (dependencies of UserManager<T>)
* Perhaps we shouldn't be registering UserManager<T> at all and only registering/depending the UmbracoBackOffice prefixed types.
*/
services.TryAddScoped<ILookupNormalizer, BackOfficeLookupNormalizer>();
services.TryAddScoped<ILookupNormalizer, NoopLookupNormalizer>();
services.TryAddScoped<IdentityErrorDescriber, BackOfficeIdentityErrorDescriber>();
return new BackOfficeIdentityBuilder(services);

View File

@@ -39,6 +39,7 @@ namespace Umbraco.Extensions
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
.AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies()
.AddUmbracoProfiler()
.AddMvcAndRazor()
@@ -95,6 +96,16 @@ namespace Umbraco.Extensions
return builder;
}
/// <summary>
/// Adds Identity support for Umbraco members
/// </summary>
public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder)
{
builder.Services.AddMembersIdentity();
return builder;
}
/// <summary>
/// Adds Umbraco back office authorization policies
/// </summary>

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -94,14 +94,11 @@ namespace Umbraco.Cms.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);
}
}
}

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Configuration.Models;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;

View File

@@ -54,12 +54,14 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
/// <summary>
/// Gets an individual tree node
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
public ActionResult<TreeNode> GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
public ActionResult<TreeNode> GetTreeNode([FromRoute]string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
{
var node = GetSingleTreeNode(id, queryStrings);
ActionResult<TreeNode> node = GetSingleTreeNode(id, queryStrings);
if (!(node.Result is null))
{
return node.Result;
}
//add the tree alias to the node since it is standalone (has no root for which this normally belongs)
node.Value.AdditionalData["treeAlias"] = TreeAlias;

View File

@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Web.Caching;
using SixLabors.ImageSharp.Web.Commands;
@@ -9,6 +11,8 @@ using SixLabors.ImageSharp.Web.DependencyInjection;
using SixLabors.ImageSharp.Web.Processors;
using SixLabors.ImageSharp.Web.Providers;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Web.Common.Security;
namespace Umbraco.Extensions
{
@@ -55,6 +59,25 @@ namespace Umbraco.Extensions
return services;
}
/// <summary>
/// Adds the services required for using Members Identity
/// </summary>
public static void AddMembersIdentity(this IServiceCollection services) =>
services.BuildMembersIdentity()
.AddDefaultTokenProviders()
.AddUserStore<MembersUserStore>()
.AddMembersManager<IMemberManager, MemberManager>();
private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services)
{
// Services used by Umbraco members identity
services.TryAddScoped<IUserValidator<MembersIdentityUser>, UserValidator<MembersIdentityUser>>();
services.TryAddScoped<IPasswordValidator<MembersIdentityUser>, PasswordValidator<MembersIdentityUser>>();
services.TryAddScoped<IPasswordHasher<MembersIdentityUser>, PasswordHasher<MembersIdentityUser>>();
return new MembersIdentityBuilder(services);
}
private static void RemoveIntParamenterIfValueGreatherThen(IDictionary<string, string> commands, string parameter, int maxValue)
{
if (commands.TryGetValue(parameter, out var command))

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.Security;
namespace Umbraco.Extensions
{
/// <summary>
/// Extension methods for <see cref="IdentityBuilder"/>
/// </summary>
public static class IdentityBuilderExtensions
{
/// <summary>
/// Adds a <see cref="UserManager{TUser}"/> for the <seealso cref="MembersIdentityUser"/>.
/// </summary>
/// <typeparam name="TInterface">The usermanager interface</typeparam>
/// <typeparam name="TUserManager">The usermanager type</typeparam>
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
public static IdentityBuilder AddMembersManager<TInterface, TUserManager>(this IdentityBuilder identityBuilder)
where TUserManager : UserManager<MembersIdentityUser>, TInterface
{
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
return identityBuilder;
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Exceptions;

View File

@@ -26,13 +26,12 @@ namespace Umbraco.Cms.Web.Common.Security
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<BackOfficeIdentityUser>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
}
@@ -136,7 +135,7 @@ namespace Umbraco.Cms.Web.Common.Security
return result;
}
/// <inheritdoc/>
public override async Task<IdentityResult> SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd)
{

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Security;
using Umbraco.Core.Security;
using Umbraco.Extensions;
namespace Umbraco.Web.Common.Security
{
public class MemberManager : UmbracoUserManager<MembersIdentityUser, MemberPasswordConfigurationSettings>, IMemberManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MemberManager(
IIpResolver ipResolver,
IUserStore<MembersIdentityUser> store,
IOptions<MembersIdentityOptions> optionsAccessor,
IPasswordHasher<MembersIdentityUser> passwordHasher,
IEnumerable<IUserValidator<MembersIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<MembersIdentityUser>> passwordValidators,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<MembersIdentityUser>> logger,
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
}
private string GetCurrentUserId(IPrincipal currentUser)
{
UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity();
var currentUserId = umbIdentity?.GetUserId<string>() ?? Cms.Core.Constants.Security.SuperUserIdAsString;
return currentUserId;
}
private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, string affectedUserId, string affectedUsername)
{
var currentUserId = GetCurrentUserId(currentUser);
var ip = IpResolver.GetCurrentRequestIpAddress();
return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername);
}
//TODO: have removed all other member audit events - can revisit if we need member auditing on a user level in future
public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException();
public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException();
public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) => throw new NotImplementedException();
public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) => throw new NotImplementedException();
public bool HasSendingUserInviteEventHandler { get; }
}
}

View File

@@ -71,4 +71,4 @@
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -19,6 +19,15 @@ using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Web.Security
{
// MIGRATED TO NETCORE
// TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation
/// <summary>
/// Helper class containing logic relating to the built-in Umbraco members macros and controllers for:
/// - Registration
/// - Updating
/// - Logging in
/// - Current status
/// </summary>
public class MembershipHelper
{
private readonly MembersMembershipProvider _membershipProvider;
@@ -118,7 +127,7 @@ namespace Umbraco.Web.Security
var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider);
var result = new Dictionary<string, bool>();
foreach(var path in paths)
foreach (var path in paths)
{
pathsWithAccess.TryGetValue(path, out var hasAccess);
// if it's not found it's false anyways
@@ -144,7 +153,8 @@ namespace Umbraco.Web.Security
string[] userRoles = null;
string[] getUserRoles(string username)
{
if (userRoles != null) return userRoles;
if (userRoles != null)
return userRoles;
userRoles = roleProvider.GetRolesForUser(username).ToArray();
return userRoles;
}
@@ -185,7 +195,8 @@ namespace Umbraco.Web.Security
var provider = _membershipProvider;
var membershipUser = provider.GetCurrentUser();
//NOTE: This should never happen since they are logged in
if (membershipUser == null) throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name);
if (membershipUser == null)
throw new InvalidOperationException("Could not find member with username " + _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name);
try
{
@@ -257,7 +268,8 @@ namespace Umbraco.Web.Security
null, null,
true, null, out status);
if (status != MembershipCreateStatus.Success) return null;
if (status != MembershipCreateStatus.Success)
return null;
var member = _memberService.GetByUsername(membershipUser.UserName);
member.Name = model.Name;
@@ -367,7 +379,8 @@ namespace Umbraco.Web.Security
public virtual IPublishedContent Get(Udi udi)
{
var guidUdi = udi as GuidUdi;
if (guidUdi == null) return null;
if (guidUdi == null)
return null;
var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(udi.EntityType);
@@ -702,27 +715,32 @@ namespace Umbraco.Web.Security
if (email != null)
{
if (member.Email != email) update = true;
if (member.Email != email)
update = true;
member.Email = email;
}
if (isApproved.HasValue)
{
if (member.IsApproved != isApproved.Value) update = true;
if (member.IsApproved != isApproved.Value)
update = true;
member.IsApproved = isApproved.Value;
}
if (lastLoginDate.HasValue)
{
if (member.LastLoginDate != lastLoginDate.Value) update = true;
if (member.LastLoginDate != lastLoginDate.Value)
update = true;
member.LastLoginDate = lastLoginDate.Value;
}
if (lastActivityDate.HasValue)
{
if (member.LastActivityDate != lastActivityDate.Value) update = true;
if (member.LastActivityDate != lastActivityDate.Value)
update = true;
member.LastActivityDate = lastActivityDate.Value;
}
if (comment != null)
{
if (member.Comment != comment) update = true;
if (member.Comment != comment)
update = true;
member.Comment = comment;
}
@@ -741,8 +759,8 @@ namespace Umbraco.Web.Security
{
var provider = _membershipProvider;
var username = provider.GetCurrentUserName();
// The result of this is cached by the MemberRepository
var username = provider.GetCurrentUserName();
// The result of this is cached by the MemberRepository
var member = _memberService.GetByUsername(username);
return member;
}
@@ -763,8 +781,10 @@ namespace Umbraco.Web.Security
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
if (passwordModel == null)
throw new ArgumentNullException(nameof(passwordModel));
if (membershipProvider == null)
throw new ArgumentNullException(nameof(membershipProvider));
var userId = -1;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Configuration.Provider;
@@ -14,9 +14,11 @@ using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Web.Security
{
//TODO: Delete - should not be used
/// <summary>
/// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing.
/// </summary>
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
public abstract class MembershipProviderBase : MembershipProvider
{
private readonly IHostingEnvironment _hostingEnvironment;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
@@ -15,6 +15,8 @@ using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete: should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
/// <summary>
/// Custom Membership Provider for Umbraco Members (User authentication for Frontend applications NOT umbraco CMS)
/// </summary>
@@ -121,6 +123,7 @@ namespace Umbraco.Web.Security.Providers
public override LegacyPasswordSecurity PasswordSecurity => _passwordSecurity.Value;
public IPasswordConfiguration PasswordConfiguration => _passwordConfig.Value;
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
private class MembershipProviderPasswordConfiguration : IPasswordConfiguration
{
public MembershipProviderPasswordConfiguration(int requiredLength, bool requireNonLetterOrDigit, bool requireDigit, bool requireLowercase, bool requireUppercase, bool useLegacyEncoding, string hashAlgorithmType, int maxFailedAccessAttemptsBeforeLockout)

View File

@@ -1,4 +1,5 @@
using System.Configuration.Provider;
using System;
using System.Configuration.Provider;
using System.Linq;
using System.Web.Security;
using Umbraco.Cms.Core.Models;
@@ -8,9 +9,12 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete: should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
public class MembersRoleProvider : RoleProvider
{
private readonly IMembershipRoleService<IMember> _roleService;
private string _applicationName;
public MembersRoleProvider(IMembershipRoleService<IMember> roleService)
{
@@ -22,8 +26,6 @@ namespace Umbraco.Web.Security.Providers
{
}
private string _applicationName;
public override bool IsUserInRole(string username, string roleName)
{
return GetRolesForUser(username).Any(x => x == roleName);
@@ -44,10 +46,12 @@ namespace Umbraco.Web.Security.Providers
return _roleService.DeleteRole(roleName, throwOnPopulatedRole);
}
public override bool RoleExists(string roleName)
{
return _roleService.GetAllRoles().Any(x => x == roleName);
}
/// <summary>
/// Returns true if the specified member role name exists
/// </summary>
/// <param name="roleName">Member role name</param>
/// <returns>True if member role exists, otherwise false</returns>
public override bool RoleExists(string roleName) => _roleService.GetAllRoles().Any(x => x.Name == roleName);
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
@@ -64,10 +68,11 @@ namespace Umbraco.Web.Security.Providers
return _roleService.GetMembersInRole(roleName).Select(x => x.Username).ToArray();
}
public override string[] GetAllRoles()
{
return _roleService.GetAllRoles().ToArray();
}
/// <summary>
/// Gets all the member roles
/// </summary>
/// <returns>A list of member roles</returns>
public override string[] GetAllRoles() => _roleService.GetAllRoles().Select(x => x.Name).ToArray();
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
@@ -85,6 +90,7 @@ namespace Umbraco.Web.Security.Providers
{
return _applicationName;
}
set
{
if (string.IsNullOrEmpty(value))

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Linq;
@@ -17,6 +17,8 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete - should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
/// <summary>
/// Abstract Membership Provider that users any implementation of IMembershipMemberService{TEntity} service
/// </summary>
@@ -30,7 +32,7 @@ namespace Umbraco.Web.Security.Providers
protected IMembershipMemberService<TEntity> MemberService { get; private set; }
protected UmbracoMembershipProvider(IMembershipMemberService<TEntity> memberService, IUmbracoVersion umbracoVersion, IHostingEnvironment hostingEnvironment, IIpResolver ipResolver)
:base(hostingEnvironment)
: base(hostingEnvironment)
{
_umbracoVersion = umbracoVersion;
_ipResolver = ipResolver;
@@ -53,9 +55,11 @@ namespace Umbraco.Web.Security.Providers
/// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
public override void Initialize(string name, NameValueCollection config)
{
if (config == null) { throw new ArgumentNullException("config"); }
if (config == null)
{ throw new ArgumentNullException("config"); }
if (string.IsNullOrEmpty(name)) name = ProviderName;
if (string.IsNullOrEmpty(name))
name = ProviderName;
// Initialize base provider class
base.Initialize(name, config);
@@ -78,7 +82,8 @@ namespace Umbraco.Web.Security.Providers
// in order to support updating passwords from the umbraco core, we can't validate the old password
var m = MemberService.GetByUsername(username);
if (m == null) return false;
if (m == null)
return false;
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, newPassword, out salt);
@@ -172,7 +177,8 @@ namespace Umbraco.Web.Security.Providers
public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
var member = MemberService.GetByUsername(username);
if (member == null) return false;
if (member == null)
return false;
MemberService.Delete(member);
return true;
@@ -421,7 +427,8 @@ namespace Umbraco.Web.Security.Providers
}
// Non need to update
if (member.IsLockedOut == false) return true;
if (member.IsLockedOut == false)
return true;
member.IsLockedOut = false;
member.FailedPasswordAttempts = 0;

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>