diff --git a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs
index 8bc98f4286..c7ce20454f 100644
--- a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs
+++ b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs
@@ -1,7 +1,7 @@
namespace Umbraco.Cms.Core.Configuration
{
///
- /// The password configuration for back office users
+ /// The password configuration for members
///
public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration
{
diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index 51b7a1824c..ba0f1e0a37 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -41,6 +41,9 @@ namespace Umbraco.Cms.Core
public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
+ public const string DefaultMemberTypeAlias = "Member";
+
+
///
/// The prefix used for external identity providers for their authentication type
///
diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs
new file mode 100644
index 0000000000..d4cbe6c95f
--- /dev/null
+++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs
@@ -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
+{
+ ///
+ public class MemberMapDefinition : IMapDefinition
+ {
+ ///
+ public void DefineMaps(UmbracoMapper mapper) => mapper.Define(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
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
index e54203619e..92aab36bd4 100644
--- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs
index 455aa44c13..e218113bf6 100644
--- a/src/Umbraco.Core/Models/Member.cs
+++ b/src/Umbraco.Core/Models/Member.cs
@@ -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;
///
+ /// Initializes a new instance of the class.
/// Constructor for creating an empty Member object
///
/// ContentType for the current Content object
@@ -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 = "";
}
///
+ /// Initializes a new instance of the class.
/// Constructor for creating a Member object
///
/// Name of the content
@@ -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 = "";
}
///
+ /// Initializes a new instance of the class.
/// Constructor for creating a Member object
///
///
@@ -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 = "";
}
///
+ /// Initializes a new instance of the class.
/// Constructor for creating a Member object
///
///
@@ -99,6 +113,7 @@ namespace Umbraco.Cms.Core.Models
}
///
+ /// Initializes a new instance of the class.
/// Constructor for creating a Member object
///
///
@@ -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));
+ }
+
///
/// Gets or sets the raw password value
///
@@ -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();
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();
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();
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();
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();
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();
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;
+ ///
+ /// The security stamp used by ASP.Net identity
+ ///
+ [IgnoreDataMember]
+ public string SecurityStamp
+ {
+ get => _securityStamp;
+ set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp));
+ }
+
+
///
/// Internal/Experimental - only used for mapping queries.
///
diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs
index 3374f1f11a..29a6bf4cdb 100644
--- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs
+++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs
@@ -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; }
///
/// Gets or sets the raw password value
@@ -38,6 +39,11 @@ namespace Umbraco.Cms.Core.Models.Membership
///
int FailedPasswordAttempts { get; set; }
+ ///
+ /// Gets or sets the security stamp used by ASP.NET Identity
+ ///
+ string SecurityStamp { get; set; }
+
//object ProfileId { get; set; }
//IEnumerable
/// The type of user
public interface IUmbracoUserManager : IDisposable
- where TUser : BackOfficeIdentityUser
+ where TUser : UmbracoIdentityUser
{
///
/// Gets the user id of a user
@@ -221,12 +222,64 @@ namespace Umbraco.Cms.Core.Security
///
Task CreateAsync(TUser user);
+ ///
+ /// Gets a list of role names the specified user belongs to.
+ ///
+ /// The user whose role names to retrieve.
+ /// The Task that represents the asynchronous operation, containing a list of role names.
+ Task> GetRolesAsync(TUser user);
+
+ ///
+ /// Removes the specified user from the named roles.
+ ///
+ /// The user to remove from the named roles.
+ /// The name of the roles to remove the user from.
+ /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation.
+ Task RemoveFromRolesAsync(TUser user, IEnumerable roles);
+
+ ///
+ /// Add the specified user to the named roles
+ ///
+ /// The user to add to the named roles
+ /// The name of the roles to add the user to.
+ /// The Task that represents the asynchronous operation, containing the IdentityResult of the operation
+ Task AddToRolesAsync(TUser user, IEnumerable roles);
+
+ ///
+ /// Creates the specified in the backing store with a password,
+ /// as an asynchronous operation.
+ ///
+ /// The user to create.
+ /// The password to add to the user.
+ ///
+ /// The that represents the asynchronous operation, containing the
+ /// of the operation.
+ ///
+ Task CreateAsync(TUser user, string password);
+
///
/// Generate a password for a user based on the current password validator
///
/// A generated password
string GeneratePassword();
+ ///
+ /// Hashes a password for a null user based on the default password hasher
+ ///
+ /// The password to hash
+ /// The hashed password
+ string HashPassword(string password);
+
+ ///
+ /// Used to validate the password without an identity user
+ /// Validation code is based on the default ValidatePasswordAsync code
+ /// Should return if validation is successful
+ ///
+ /// The password.
+ /// A representing whether validation was successful.
+
+ Task ValidatePasswordAsync(string password);
+
///
/// Generates an email confirmation token for the specified user.
///
diff --git a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs
index 95a63c6001..d7ec33d1f3 100644
--- a/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs
+++ b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs
@@ -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
{
diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
index 65bbe7d2bd..1a76dec2d5 100644
--- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
+++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs
@@ -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(
+ (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;
}
}
diff --git a/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs
new file mode 100644
index 0000000000..6026efa62f
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/MemberRolesUserStore.cs
@@ -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
+{
+ ///
+ /// A custom user store that uses Umbraco member data
+ ///
+ public class MemberRolesUserStore : RoleStoreBase, string, IdentityUserRole, IdentityRoleClaim>
+ {
+ 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));
+ }
+
+ ///
+ public override IQueryable> Roles { get; }
+
+ ///
+ public override Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task> FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task> GetClaimsAsync(IdentityRole role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task AddClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+
+ ///
+ public override Task RemoveClaimAsync(IdentityRole role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs
new file mode 100644
index 0000000000..30229e4521
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/MembersIdentityBuilder.cs
@@ -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)
+ {
+ }
+
+ ///
+ /// Adds a token provider for the .
+ ///
+ /// The name of the provider to add.
+ /// The type of the to add.
+ /// The current instance.
+ 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(options =>
+ {
+ options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider);
+ });
+ Services.AddTransient(provider);
+ return this;
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs
new file mode 100644
index 0000000000..d8db1d29a0
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/MembersIdentityOptions.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Core.Security
+{
+ ///
+ /// Identity options specifically for the Umbraco members identity implementation
+ ///
+ public class MembersIdentityOptions : IdentityOptions
+ {
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs
new file mode 100644
index 0000000000..57d5365b33
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/MembersIdentityUser.cs
@@ -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
+{
+ ///
+ /// The identity user used for the member
+ ///
+ public class MembersIdentityUser : UmbracoIdentityUser
+ {
+ private string _name;
+ private string _passwordConfig;
+ private IReadOnlyCollection _groups;
+
+ // Custom comparer for enumerables
+ private static readonly DelegateEqualityComparer> s_groupsComparer = new DelegateEqualityComparer>(
+ (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
+ groups => groups.GetHashCode());
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MembersIdentityUser(int userId)
+ {
+ // use the property setters - they do more than just setting a field
+ Id = UserIdToString(userId);
+ }
+
+ public MembersIdentityUser()
+ {
+ }
+
+ ///
+ /// Used to construct a new instance without an identity
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets or sets the member's real name
+ ///
+ public string Name
+ {
+ get => _name;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
+ }
+
+ ///
+ /// Gets or sets the password config
+ ///
+ public string PasswordConfig
+ {
+ get => _passwordConfig;
+ set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
+ }
+
+ ///
+ /// Gets or sets the user groups
+ ///
+ public IReadOnlyCollection Groups
+ {
+ get => _groups;
+ set
+ {
+ _groups = value.Where(x => x.Alias != null).ToArray();
+
+ var roles = new List>();
+ foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole
+ {
+ RoleId = x.Alias,
+ UserId = Id
+ }))
+ {
+ roles.Add(identityUserRole);
+ }
+
+ // now reset the collection
+ Roles = roles;
+
+ BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the member is locked out
+ ///
+ public bool IsLockedOut
+ {
+ get
+ {
+ bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
+ return isLocked;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the member is approved
+ ///
+ public bool IsApproved { get; set; }
+
+ ///
+ /// Gets or sets the alias of the member type
+ ///
+ public string MemberTypeAlias { get; set; }
+
+ private static string UserIdToString(int userId) => string.Intern(userId.ToString());
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/MembersUserStore.cs b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs
new file mode 100644
index 0000000000..332308bbc7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Security/MembersUserStore.cs
@@ -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
+{
+ ///
+ /// A custom user store that uses Umbraco member data
+ ///
+ public class MembersUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>
+ {
+ private readonly IMemberService _memberService;
+ private readonly UmbracoMapper _mapper;
+ private readonly IScopeProvider _scopeProvider;
+
+ ///
+ /// Initializes a new instance of the class for the members identity store
+ ///
+ /// The member service
+ /// The mapper for properties
+ /// The scope provider
+ /// The error describer
+ 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));
+ }
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override IQueryable Users => throw new NotImplementedException();
+
+ ///
+ public override Task GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
+
+ ///
+ public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
+
+ ///
+ public override Task 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);
+ }
+
+ ///
+ public override Task UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ Attempt asInt = user.Id.TryConvertTo();
+ 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);
+ }
+
+ ///
+ public override Task 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);
+ }
+
+ ///
+ public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
+
+ ///
+ protected override Task 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(user)));
+ }
+
+ ///
+ public override Task 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(user));
+
+ return Task.FromResult(result);
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ public override async Task 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;
+ }
+
+ ///
+ public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ IMember member = _memberService.GetByEmail(email);
+ MembersIdentityUser result = member == null
+ ? null
+ : _mapper.Map(member);
+
+ return Task.FromResult(AssignLoginsCallback(result));
+ }
+
+ ///
+ public override Task GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken)
+ => GetEmailAsync(user, cancellationToken);
+
+ ///
+ public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
+ => SetEmailAsync(user, normalizedEmail, cancellationToken);
+
+ ///
+ 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 logins = user.Logins;
+ var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString());
+ IdentityUserLogin userLogin = instance;
+ logins.Add(userLogin);
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ public override Task> GetLoginsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
+ }
+
+ ///
+ protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
+ if (user == null)
+ {
+ return null;
+ }
+
+ IList logins = await GetLoginsAsync(user, cancellationToken);
+ UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider);
+ if (found == null)
+ {
+ return null;
+ }
+
+ return new IdentityUserLogin
+ {
+ LoginProvider = found.LoginProvider,
+ ProviderKey = found.ProviderKey,
+ ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null
+ UserId = user.Id
+ };
+ }
+
+ ///
+ protected override Task> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ var logins = new List();
+
+ // TODO: external login needed?
+ //_externalLoginService.Find(loginProvider, providerKey).ToList();
+ if (logins.Count == 0)
+ {
+ return Task.FromResult((IdentityUserLogin)null);
+ }
+
+ IIdentityUserLogin found = logins[0];
+ return Task.FromResult(new IdentityUserLogin
+ {
+ LoginProvider = found.LoginProvider,
+ ProviderKey = found.ProviderKey,
+ ProviderDisplayName = null, // TODO: We don't store this value so it will be null
+ UserId = found.UserId
+ });
+ }
+
+ ///
+ 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 userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
+
+ if (userRole == null)
+ {
+ _memberService.AssignRole(user.UserName, role);
+ user.AddRole(role);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ 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 userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
+
+ if (userRole != null)
+ {
+ _memberService.DissociateRole(user.UserName, userRole.RoleId);
+ user.Roles.Remove(userRole);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Gets a list of role names the specified user belongs to.
+ ///
+ public override Task> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IEnumerable currentRoles = _memberService.GetAllRoles(user.UserName);
+ ICollection> roles = currentRoles.Select(role => new IdentityUserRole
+ {
+ RoleId = role,
+ UserId = user.Id
+ }).ToList();
+
+ user.Roles = roles;
+ return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList());
+ }
+
+ ///
+ /// Returns true if a user is in the role
+ ///
+ public override Task 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));
+ }
+
+ ///
+ /// Lists all users of a given role.
+ ///
+ public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (normalizedRoleName == null)
+ {
+ throw new ArgumentNullException(nameof(normalizedRoleName));
+ }
+
+ IEnumerable members = _memberService.GetMembersByMemberType(normalizedRoleName);
+
+ IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList();
+
+ return Task.FromResult(membersIdentityUsers);
+ }
+
+ ///
+ protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
+ {
+ IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName);
+ if (group == null)
+ {
+ return Task.FromResult((IdentityRole)null);
+ }
+
+ return Task.FromResult(new IdentityRole(group.Name)
+ {
+ //TODO: what should the alias be?
+ Id = @group.Id.ToString()
+ });
+ }
+
+ ///
+ protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken)
+ {
+ MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
+ if (user == null)
+ {
+ return null;
+ }
+
+ IdentityUserRole found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId));
+ return found;
+ }
+
+ ///
+ public override Task 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>(() => _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 attempt = userId.TryConvertTo();
+ 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());
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task> GetClaimsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task AddClaimsAsync(MembersIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task ReplaceClaimAsync(MembersIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task RemoveClaimsAsync(MembersIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task> FindTokenAsync(MembersIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task AddUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException();
+
+ ///
+ /// Not supported in Umbraco
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ protected override Task RemoveUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException();
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs
similarity index 76%
rename from src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs
rename to src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs
index 27047a48fc..abc8758fb8 100644
--- a/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs
+++ b/src/Umbraco.Infrastructure/Security/NoOpLookupNormalizer.cs
@@ -5,10 +5,8 @@ namespace Umbraco.Cms.Core.Security
///
/// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
///
- 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;
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
index 78f10b3fc6..5d91736643 100644
--- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
+++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs
@@ -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 passwordHasher,
IEnumerable> userValidators,
IEnumerable> passwordValidators,
- ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger> logger,
IOptions 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
///
/// The user id
- /// The sesion id
- /// True if the sesion is valid, else false
+ /// The session id
+ /// True if the session is valid, else false
public virtual async Task ValidateSessionIdAsync(string userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore;
@@ -88,26 +88,62 @@ namespace Umbraco.Cms.Core.Security
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
- ///
- /// This will determine which password hasher to use based on what is defined in config
- ///
- /// The
- /// An
- protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher();
-
///
/// Helper method to generate a password for a user based on the current password validator
///
/// The generated password
public string GeneratePassword()
{
- if (_passwordGenerator == null)
+ _passwordGenerator ??= new PasswordGenerator(PasswordConfiguration);
+
+ string password = _passwordGenerator.GeneratePassword();
+ return password;
+ }
+
+ ///
+ /// Generates a hashed password based on the default password hasher
+ /// No existing identity user is required and this does not validate the password
+ ///
+ /// The password to hash
+ /// The hashed password
+ public string HashPassword(string password)
+ {
+ string hashedPassword = PasswordHasher.HashPassword(null, password);
+ return hashedPassword;
+ }
+
+ ///
+ /// Used to validate the password without an identity user
+ /// Validation code is based on the default ValidatePasswordAsync code
+ /// Should return if validation is successful
+ ///
+ /// The password.
+ /// A representing whether validation was successful.
+ public async Task ValidatePasswordAsync(string password)
+ {
+ var errors = new List();
+ var isValid = true;
+ foreach (IPasswordValidator 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;
}
///
diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs
index 73435fd9d0..e57ffc04f3 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs
@@ -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
/// Email of the Member to create
/// Name of the Member to create
/// Alias of the MemberType the Member should be based on
+ /// Thrown when a member type for the given alias isn't found
///
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(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
///
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(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 GetAllRoles()
+ ///
+ /// Returns a list of all member roles
+ ///
+ /// A list of member roles
+
+ public IEnumerable 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();
}
}
+ ///
+ /// Returns a list of all member roles for a given member ID
+ ///
+ ///
+ /// A list of member roles
public IEnumerable 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 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 result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Name).Distinct();
}
}
public IEnumerable 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 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 result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable 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 result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable 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 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 membersInRole = _memberRepository.GetByMemberGroup(roleName);
if (membersInRole.Any())
+ {
throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
+ }
}
- var query = Query().Where(g => g.Name == roleName);
- var found = _memberGroupRepository.Get(query).ToArray();
+ IQuery query = Query().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
///
public MemberExportModel ExportMember(Guid key)
{
- using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
- var query = Query().Where(x => x.Key == key);
- var member = _memberRepository.Get(query).FirstOrDefault();
+ IQuery query = Query().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 GetPropertyExportItems(IMember member)
{
- if (member == null) throw new ArgumentNullException(nameof(member));
+ if (member == null)
+ {
+ throw new ArgumentNullException(nameof(member));
+ }
var exportProperties = new List();
- 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().Where(x => x.ContentTypeId == memberTypeId);
+ IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId);
- var members = _memberRepository.Get(query).ToArray();
+ IMember[] members = _memberRepository.Get(query).ToArray();
var deleteEventArgs = new DeleteEventArgs(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
}
}
diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
index df0ef4600c..caed95ae52 100644
--- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
+++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
@@ -156,6 +156,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
+ .AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme)
.AddPreviewSupport()
.AddMvcAndRazor(mvcBuilding: mvcBuilder =>
diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
index b45a232a5c..eab085f67e 100644
--- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
+++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
@@ -216,6 +216,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing
.AddRuntimeMinifier()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
+ .AddMembersIdentity()
.AddTestServices(TestHelper, GetAppCaches());
if (TestOptions.Mapper)
diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs
index 3cb8cf56db..6ccdff2f0a 100644
--- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs
+++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs
@@ -185,10 +185,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
{
MemberService.AddRole("MyTestRole");
- IEnumerable found = MemberService.GetAllRoles();
+ IEnumerable 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 found = MemberService.GetAllRoles();
+ IEnumerable 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 found = MemberService.GetAllRoles();
+ IEnumerable 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 memberRoles = MemberService.GetAllRoles();
+ IEnumerable memberRoles = MemberService.GetAllRoles();
Assert.AreEqual(0, memberRoles.Count());
}
diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000000..2b63f9c0bc
--- /dev/null
+++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs
@@ -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 userStore = Services.GetService>();
+
+ Assert.IsNotNull(userStore);
+ Assert.AreEqual(typeof(MembersUserStore), userStore.GetType());
+ }
+
+ [Test]
+ public void AddMembersIdentity_ExpectMembersUserManagerResolvable()
+ {
+ IMemberManager userManager = Services.GetService();
+
+ Assert.NotNull(userManager);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
index 471307fa54..d7fd849e71 100644
--- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
+++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
@@ -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());
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs
index ce47b490a6..810cc8b99e 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs
@@ -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
{
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs
similarity index 100%
rename from src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs
rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs
deleted file mode 100644
index 1d05640ff3..0000000000
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs
new file mode 100644
index 0000000000..af1df95b4d
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs
@@ -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> _mockMemberStore;
+ private Mock> _mockIdentityOptions;
+ private Mock> _mockPasswordHasher;
+ private Mock> _mockUserValidators;
+ private Mock>> _mockPasswordValidators;
+ private Mock _mockNormalizer;
+ private IdentityErrorDescriber _mockErrorDescriber;
+ private Mock _mockServiceProviders;
+ private Mock>> _mockLogger;
+ private Mock> _mockPasswordConfiguration;
+
+ public MemberManager CreateSut()
+ {
+ _mockMemberStore = new Mock>();
+ _mockIdentityOptions = new Mock>();
+
+ var idOptions = new MembersIdentityOptions { Lockout = { AllowedForNewUsers = false } };
+ _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions);
+ _mockPasswordHasher = new Mock>();
+
+ var userValidators = new List>();
+ _mockUserValidators = new Mock>();
+ var validator = new Mock>();
+ userValidators.Add(validator.Object);
+
+ _mockPasswordValidators = new Mock>>();
+ _mockNormalizer = new Mock();
+ _mockErrorDescriber = new IdentityErrorDescriber();
+ _mockServiceProviders = new Mock();
+ _mockLogger = new Mock>>();
+ _mockPasswordConfiguration = new Mock>();
+ _mockPasswordConfiguration.Setup(x => x.Value).Returns(() =>
+ new MemberPasswordConfigurationSettings()
+ {
+
+ });
+
+ var pwdValidators = new List>
+ {
+ new PasswordValidator()
+ };
+
+ var userManager = new MemberManager(
+ new Mock().Object,
+ _mockMemberStore.Object,
+ _mockIdentityOptions.Object,
+ _mockPasswordHasher.Object,
+ userValidators,
+ pwdValidators,
+ new BackOfficeIdentityErrorDescriber(),
+ _mockServiceProviders.Object,
+ new Mock().Object,
+ new Mock>>().Object,
+ _mockPasswordConfiguration.Object);
+
+ validator.Setup(v => v.ValidateAsync(
+ userManager,
+ It.IsAny()))
+ .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>(() => 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());
+ }
+ }
+}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs
new file mode 100644
index 0000000000..40ed171da9
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserStoreTests.cs
@@ -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 _mockMemberService;
+
+ public MembersUserStore CreateSut()
+ {
+ _mockMemberService = new Mock();
+ return new MembersUserStore(
+ _mockMemberService.Object,
+ new UmbracoMapper(new MapDefinitionCollection(new List())),
+ new Mock().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(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(), It.IsAny(), It.IsAny(), It.IsAny())).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
+ }
+}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs
similarity index 79%
rename from src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs
rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs
index d7dbd1baff..fd8907a451 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/NoOpLookupNormalizerTests.cs
@@ -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);
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
new file mode 100644
index 0000000000..dd748e546b
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
@@ -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(() => 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(), It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.ValidatePasswordAsync(It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+
+ var value = new MemberDisplay();
+ string reason = "Validation failed";
+
+ // act
+ ActionResult 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(), It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.ValidatePasswordAsync(It.IsAny()))
+ .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()))
+ .Returns(() => null)
+ .Returns(() => member);
+ Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member);
+
+ MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
+
+ // act
+ ActionResult 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(), It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.ValidatePasswordAsync(It.IsAny()))
+ .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()))
+ .Returns(() => null)
+ .Returns(() => member);
+ Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member);
+
+ MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
+
+ // act
+ ActionResult 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()))
+ .ReturnsAsync(() => new MembersIdentityUser());
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.ValidatePasswordAsync(It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+
+ string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.HashPassword(It.IsAny()))
+ .Returns(password);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.UpdateAsync(It.IsAny()))
+ .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())).Returns(() => member);
+ Mock.Get(memberService).SetupSequence(
+ x => x.GetByEmail(It.IsAny()))
+ .Returns(() => null)
+ .Returns(() => member);
+
+ MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
+
+ // act
+ ActionResult 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()))
+ .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()))
+ .ReturnsAsync(() => IdentityResult.Success);
+
+ Mock.Get(memberService).SetupSequence(
+ x => x.GetByEmail(It.IsAny()))
+ .Returns(() => member);
+
+ MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
+ string reason = "Validation failed";
+
+ // act
+ ActionResult 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()
+ {
+ roleName
+ };
+ var membersIdentityUser = new MembersIdentityUser();
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.FindByIdAsync(It.IsAny()))
+ .ReturnsAsync(() => membersIdentityUser);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.ValidatePasswordAsync(It.IsAny()))
+ .ReturnsAsync(() => IdentityResult.Success);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.HashPassword(It.IsAny()))
+ .Returns(password);
+ Mock.Get(umbracoMembersUserManager)
+ .Setup(x => x.UpdateAsync(It.IsAny()))
+ .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())).Returns(() => member);
+
+ Mock.Get(memberService).SetupSequence(
+ x => x.GetByEmail(It.IsAny()))
+ .Returns(() => null)
+ .Returns(() => member);
+ Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny())).Returns(() => member);
+ MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
+
+ // act
+ ActionResult 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(), true));
+ AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
+ }
+
+ ///
+ /// Create member controller to test
+ ///
+ /// Member service
+ /// Member type service
+ /// Member group service
+ /// Members user manager
+ /// Data type service
+ /// Back office security accessor
+ /// A member controller for the tests
+ private MemberController CreateSut(
+ IMemberService memberService,
+ IMemberTypeService memberTypeService,
+ IMemberGroupService memberGroupService,
+ IMemberManager membersUserManager,
+ IDataTypeService dataTypeService,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+ {
+ var mockShortStringHelper = new MockShortStringHelper();
+
+ var textService = new Mock();
+ var contentTypeBaseServiceProvider = new Mock();
+ contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny())).Returns(new ContentType(mockShortStringHelper, 123));
+ var contentAppFactories = new Mock>();
+ var mockContentAppFactoryCollection = new Mock>();
+ var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache());
+ var contentAppFactoryCollection = new ContentAppFactoryCollection(
+ contentAppFactories.Object,
+ mockContentAppFactoryCollection.Object,
+ hybridBackOfficeSecurityAccessor);
+ var mockUserService = new Mock();
+ var commonMapper = new CommonMapper(
+ mockUserService.Object,
+ contentTypeBaseServiceProvider.Object,
+ contentAppFactoryCollection,
+ textService.Object);
+ var mockCultureDictionary = new Mock();
+
+ var mockPasswordConfig = new Mock>();
+ mockPasswordConfig.Setup(x => x.Value).Returns(() => new MemberPasswordConfigurationSettings());
+ IDataEditor dataEditor = Mock.Of(
+ x => x.Type == EditorType.PropertyValue
+ && x.Alias == Constants.PropertyEditors.Aliases.Label);
+ Mock.Get(dataEditor).Setup(x => x.GetValueEditor()).Returns(new TextOnlyValueEditor(Mock.Of(), Mock.Of(), new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox, "Test Textbox", "textbox"), textService.Object, Mock.Of(), Mock.Of()));
+
+ var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
+
+ IMapDefinition memberMapDefinition = new MemberMapDefinition(
+ commonMapper,
+ new CommonTreeNodeMapper(Mock.Of()),
+ new MemberTabsAndPropertiesMapper(
+ mockCultureDictionary.Object,
+ backOfficeSecurityAccessor,
+ textService.Object,
+ memberTypeService,
+ memberService,
+ memberGroupService,
+ mockPasswordConfig.Object,
+ contentTypeBaseServiceProvider.Object,
+ propertyEditorCollection),
+ new HttpContextAccessor());
+
+ var map = new MapDefinitionCollection(new List()
+ {
+ new global::Umbraco.Core.Models.Mapping.MemberMapDefinition(),
+ memberMapDefinition,
+ new ContentTypeMapDefinition(
+ commonMapper,
+ propertyEditorCollection,
+ dataTypeService,
+ new Mock().Object,
+ new Mock().Object,
+ new Mock().Object,
+ memberTypeService,
+ new Mock().Object,
+ mockShortStringHelper,
+ new Mock>().Object,
+ new Mock().Object)
+ });
+ _mapper = new UmbracoMapper(map);
+
+ return new MemberController(
+ new DefaultCultureDictionary(
+ new Mock().Object,
+ new HttpRequestAppCache(() => null)),
+ new LoggerFactory(),
+ mockShortStringHelper,
+ new DefaultEventMessagesFactory(
+ new Mock().Object),
+ textService.Object,
+ propertyEditorCollection,
+ _mapper,
+ memberService,
+ memberTypeService,
+ membersUserManager,
+ dataTypeService,
+ backOfficeSecurityAccessor,
+ new ConfigurationEditorJsonSerializer());
+ }
+
+
+ ///
+ /// Setup all standard member data for test
+ ///
+ 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(),
+ //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>()
+ {
+ new Tab()
+ {
+ Alias = "test",
+ Id = 77,
+ Properties = new List()
+ {
+ 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;
+ }
+
+ ///
+ /// Check all member properties are equal
+ ///
+ ///
+ ///
+ 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));
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
index b6c8a4a2b9..22ca9d073d 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
@@ -70,10 +70,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
///
protected ILocalizedTextService LocalizedTextService { get; }
+ ///
+ /// Handles if the content for the specified ID isn't found
+ ///
+ /// The content ID to find
+ /// Whether to throw an exception
+ /// The error response
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
{
// 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(propertyDto.DataType.Configuration);
- if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
+ TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs(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
///
protected TPersisted GetObjectFromRequest(Func 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();
}
///
/// Returns true if the action passed in means we need to create something new
///
- ///
- ///
- internal static bool IsCreatingAction(ContentSaveAction action)
- {
- return (action.ToString().EndsWith("New"));
- }
+ /// The content action
+ /// Returns true if this is a creating action
+ 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,
+ ///
+ /// Adds a cancelled message to the display
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
}
}
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
index e80ba6d292..eff8259e47 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
@@ -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;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The culture dictionary
+ /// The logger factory
+ /// The string helper
+ /// The event messages factory
+ /// The entry point for localizing key services
+ /// The property editors
+ /// The mapper
+ /// The member service
+ /// The member type service
+ /// The member manager
+ /// The data-type service
+ /// The back office security accessor
+ /// The JSON serializer
public MemberController(
ICultureDictionary cultureDictionary,
ILoggerFactory loggerFactory,
IShortStringHelper shortStringHelper,
IEventMessagesFactory eventMessages,
ILocalizedTextService localizedTextService,
- IOptions 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;
}
+ ///
+ /// The paginated list of members
+ ///
+ /// The page number to display
+ /// The size of the page
+ /// The ordering of the member list
+ /// The direction of the member list
+ /// The system field to order by
+ /// The current filter for the list
+ /// The member type
+ /// The paged result of members
public PagedResult 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(0, 0, 0);
@@ -110,8 +144,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize)
{
- Items = members
- .Select(x => _umbracoMapper.Map(x))
+ Items = members.Select(x => _umbracoMapper.Map(x))
};
return pagedResult;
}
@@ -119,15 +152,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
///
/// Returns a display node with a list view to render members
///
- ///
- ///
+ /// The member type to list
+ /// The member list for display
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();
- 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
///
/// Gets the content json for the member
///
- ///
- ///
+ /// The Guid key of the member
+ /// The member for display
[OutgoingEditorModelEvent]
public MemberDisplay GetByKey(Guid key)
{
- var foundMember = _memberService.GetByKey(key);
+ IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
HandleContentNotFound(key);
}
+
return _umbracoMapper.Map(foundMember);
}
///
/// Gets an empty content item for the
///
- ///
- ///
+ /// The content type
+ /// The empty member for display
[OutgoingEditorModelEvent]
public ActionResult 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(emptyContent);
}
///
/// Saves member
///
- ///
+ /// The content item to save as a member
+ /// The resulting member display object
[FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
[MemberSaveValidation]
- public async Task> PostSave(
- [ModelBinder(typeof(MemberBinder))]
- MemberSave contentItem)
+ public async Task> 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(contentItem.PersistedContent);
+ MemberDisplay forDisplay = _umbracoMapper.Map(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 updateSuccessful = await UpdateMemberAsync(contentItem);
+ if (!(updateSuccessful.Result is null))
+ {
+ return updateSuccessful.Result;
+ }
+
break;
case ContentSaveAction.SaveNew:
- contentItem.PersistedContent = CreateMemberData(contentItem);
+ ActionResult 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(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(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
///
/// Maps the property values to the persisted entity
///
- ///
+ /// The member content item to map properties from
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(
+ // use the base method to map the rest of the properties
+ MapPropertyValuesForPersistence(
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)
+ ///
+ /// Create a member from the supplied member content data
+ ///
+ /// All member password processing and creation is done via the identity manager
+ ///
+ /// Member content data
+ /// The identity result of the created member
+ private async Task> 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(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;
}
///
/// Update the member security data
- ///
- ///
- ///
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
- ///
- private void UpdateMemberData(MemberSave contentItem)
+ ///
+ /// The member to save
+ private async Task> 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 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 result)
+ {
+ var sb = new StringBuilder();
+ IEnumerable identityErrors = result.ToList();
+ foreach (IdentityError error in identityErrors)
+ {
+ string errorString = $"{error.Description}";
+ sb.AppendLine(errorString);
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Add or update the identity roles
+ ///
+ /// The member content item
+ /// The member as an identity user
+ 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 currentRoles = await _memberManager.GetRolesAsync(identityMember);
+
+ // find the ones to remove and remove them
+ IEnumerable 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);
+ }
+ }
+
///
/// Permanently deletes a member
///
- ///
- ///
+ /// Guid of the member to delete
+ /// The result of the deletion
///
[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);
}
}
-
-
}
diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs
index d07147a778..5ebada2c98 100644
--- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs
@@ -56,7 +56,7 @@ namespace Umbraco.Extensions
services.TryAddScoped, UserClaimsPrincipalFactory>();
// CUSTOM:
- services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddSingleton();
@@ -68,7 +68,7 @@ namespace Umbraco.Extensions
* To validate the container the following registrations are required (dependencies of UserManager)
* Perhaps we shouldn't be registering UserManager at all and only registering/depending the UmbracoBackOffice prefixed types.
*/
- services.TryAddScoped();
+ services.TryAddScoped();
services.TryAddScoped();
return new BackOfficeIdentityBuilder(services);
diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
index 9ceaa099ed..e28c8e4196 100644
--- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -39,6 +39,7 @@ namespace Umbraco.Extensions
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
+ .AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies()
.AddUmbracoProfiler()
.AddMvcAndRazor()
@@ -95,6 +96,16 @@ namespace Umbraco.Extensions
return builder;
}
+ ///
+ /// Adds Identity support for Umbraco members
+ ///
+ public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder)
+ {
+ builder.Services.AddMembersIdentity();
+
+ return builder;
+ }
+
///
/// Adds Umbraco back office authorization policies
///
diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
index c17d834226..e88503794e 100644
--- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
+++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
@@ -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(source.Properties);
}
-
-
-
}
}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs
index cc37b686cd..2d2eb548a6 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs
@@ -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;
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs
index a21036743e..e8eaba75a9 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
index 4cc9195d14..d9ebdfaae6 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
diff --git a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
index e545366af1..504c74d90e 100644
--- a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
+++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Configuration.Models;
diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs
index b78cc4329e..18e749bddb 100644
--- a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs
index 276195d844..a77ad8f0a8 100644
--- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs
@@ -54,12 +54,14 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
///
/// Gets an individual tree node
///
- ///
- ///
- ///
- public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
+ public ActionResult GetTreeNode([FromRoute]string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
{
- var node = GetSingleTreeNode(id, queryStrings);
+ ActionResult 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;
diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
index b764dbec40..1c12390ada 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
@@ -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;
}
+ ///
+ /// Adds the services required for using Members Identity
+ ///
+ public static void AddMembersIdentity(this IServiceCollection services) =>
+ services.BuildMembersIdentity()
+ .AddDefaultTokenProviders()
+ .AddUserStore()
+ .AddMembersManager();
+
+
+ private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services)
+ {
+ // Services used by Umbraco members identity
+ services.TryAddScoped, UserValidator>();
+ services.TryAddScoped, PasswordValidator>();
+ services.TryAddScoped, PasswordHasher>();
+ return new MembersIdentityBuilder(services);
+ }
+
private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue)
{
if (commands.TryGetValue(parameter, out var command))
diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs
new file mode 100644
index 0000000000..1491002ae2
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Core.Security;
+
+namespace Umbraco.Extensions
+{
+ ///
+ /// Extension methods for
+ ///
+ public static class IdentityBuilderExtensions
+ {
+ ///
+ /// Adds a for the .
+ ///
+ /// The usermanager interface
+ /// The usermanager type
+ /// The current instance.
+ public static IdentityBuilder AddMembersManager(this IdentityBuilder identityBuilder)
+ where TUserManager : UserManager, TInterface
+ {
+ identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
+ return identityBuilder;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs
index 718d2f3a4c..476bb272b2 100644
--- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs
+++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs
@@ -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;
diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
index be2c200b29..4a81a8976b 100644
--- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
+++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
@@ -26,13 +26,12 @@ namespace Umbraco.Cms.Web.Common.Security
IPasswordHasher passwordHasher,
IEnumerable> userValidators,
IEnumerable> passwordValidators,
- BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger> logger,
IOptions 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;
}
-
+
///
public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd)
{
diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs
new file mode 100644
index 0000000000..386b1ba231
--- /dev/null
+++ b/src/Umbraco.Web.Common/Security/MemberManager.cs
@@ -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, IMemberManager
+ {
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public MemberManager(
+ IIpResolver ipResolver,
+ IUserStore store,
+ IOptions optionsAccessor,
+ IPasswordHasher passwordHasher,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ BackOfficeIdentityErrorDescriber errors,
+ IServiceProvider services,
+ IHttpContextAccessor httpContextAccessor,
+ ILogger> logger,
+ IOptions 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() ?? 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; }
+ }
+}
diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json
index a3e57978da..382ee11590 100644
--- a/src/Umbraco.Web.UI.NetCore/appsettings.json
+++ b/src/Umbraco.Web.UI.NetCore/appsettings.json
@@ -71,4 +71,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs
index 961a8cd86d..4f48b5cc80 100644
--- a/src/Umbraco.Web/Security/MembershipHelper.cs
+++ b/src/Umbraco.Web/Security/MembershipHelper.cs
@@ -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
+
+ ///
+ /// Helper class containing logic relating to the built-in Umbraco members macros and controllers for:
+ /// - Registration
+ /// - Updating
+ /// - Logging in
+ /// - Current status
+ ///
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();
- 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;
diff --git a/src/Umbraco.Web/Security/MembershipProviderBase.cs b/src/Umbraco.Web/Security/MembershipProviderBase.cs
index d664dc527e..b0ddac250e 100644
--- a/src/Umbraco.Web/Security/MembershipProviderBase.cs
+++ b/src/Umbraco.Web/Security/MembershipProviderBase.cs
@@ -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
///
/// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing.
///
+ [Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
public abstract class MembershipProviderBase : MembershipProvider
{
private readonly IHostingEnvironment _hostingEnvironment;
diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs
index 008d81623d..97ab5f9adc 100644
--- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs
+++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs
@@ -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")]
///
/// Custom Membership Provider for Umbraco Members (User authentication for Frontend applications NOT umbraco CMS)
///
@@ -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)
diff --git a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs
index 8a7c91097a..f28aa75e48 100644
--- a/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs
+++ b/src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs
@@ -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 _roleService;
+ private string _applicationName;
public MembersRoleProvider(IMembershipRoleService 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);
- }
+ ///
+ /// Returns true if the specified member role name exists
+ ///
+ /// Member role name
+ /// True if member role exists, otherwise false
+ 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();
- }
+ ///
+ /// Gets all the member roles
+ ///
+ /// A list of member roles
+ 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))
diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs
index 5c21e43b2a..68e4f9916d 100644
--- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs
+++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs
@@ -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")]
///
/// Abstract Membership Provider that users any implementation of IMembershipMemberService{TEntity} service
///
@@ -30,7 +32,7 @@ namespace Umbraco.Web.Security.Providers
protected IMembershipMemberService MemberService { get; private set; }
protected UmbracoMembershipProvider(IMembershipMemberService memberService, IUmbracoVersion umbracoVersion, IHostingEnvironment hostingEnvironment, IIpResolver ipResolver)
- :base(hostingEnvironment)
+ : base(hostingEnvironment)
{
_umbracoVersion = umbracoVersion;
_ipResolver = ipResolver;
@@ -53,9 +55,11 @@ namespace Umbraco.Web.Security.Providers
/// The name of the provider has a length of zero.
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;
diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings
index 2f99fe6350..6fb927035e 100644
--- a/src/umbraco.sln.DotSettings
+++ b/src/umbraco.sln.DotSettings
@@ -5,5 +5,6 @@
HINT
False
Default
+ True
True
True