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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
33
src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs
Normal file
33
src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A custom user identity for the Umbraco backoffice
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
.Add<TagMapDefinition>()
|
||||
.Add<TemplateMapDefinition>()
|
||||
.Add<UserMapDefinition>()
|
||||
.Add<MemberMapDefinition>()
|
||||
.Add<LanguageMapDefinition>()
|
||||
.Add<IdentityMapDefinition>();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
11
src/Umbraco.Infrastructure/Security/IMemberManager.cs
Normal file
11
src/Umbraco.Infrastructure/Security/IMemberManager.cs
Normal 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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
57
src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs
Normal file
57
src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
131
src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs
Normal file
131
src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
702
src/Umbraco.Infrastructure/Security/MembersUserStore.cs
Normal file
702
src/Umbraco.Infrastructure/Security/MembersUserStore.cs
Normal 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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
|
||||
.AddBackOfficeCore()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddBackOfficeIdentity()
|
||||
.AddMembersIdentity()
|
||||
.AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme)
|
||||
.AddPreviewSupport()
|
||||
.AddMvcAndRazor(mvcBuilding: mvcBuilder =>
|
||||
|
||||
@@ -216,6 +216,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
.AddRuntimeMinifier()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddBackOfficeIdentity()
|
||||
.AddMembersIdentity()
|
||||
.AddTestServices(TestHelper, GetAppCaches());
|
||||
|
||||
if (TestOptions.Mapper)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
66
src/Umbraco.Web.Common/Security/MemberManager.cs
Normal file
66
src/Umbraco.Web.Common/Security/MemberManager.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user