Compare commits

...
Author SHA1 Message Date
Bjarke Berg cf64f39c33 Merge remote-tracking branch 'origin/netcore/netcore' into netcore/alpha003 2021-02-08 15:43:47 +01:00
Bjarke Berg 6653b92bb3 Merge remote-tracking branch 'origin/netcore/netcore' into netcore/alpha003 2021-02-08 12:08:52 +01:00
Bjarke Berg 0a8819a503 Merge remote-tracking branch 'emmagarland/netcore/members-userstore' into netcore/alpha003 2021-02-08 08:20:26 +01:00
Emma Garland 8a9c3d0c2e Fix tests (although all properties aren't checked) 2021-02-04 17:01:21 +00:00
Emma Garland 512361f504 Added controller role (member group) tests. Fix for custom member properties not saving. Updated custom member type saving. 2021-02-03 17:33:48 +00:00
Emma Garland 5053c97e02 Await expressions 2021-02-01 18:13:23 +00:00
Emma Garland ecf3650f53 Remove comment 2021-02-01 18:11:13 +00:00
Emma Garland 2cdbc6b54d Merge branch 'netcore/members-userstore' into netcore/feature/members-roles 2021-02-01 18:05:15 +00:00
Emma Garland 0c787973ca Merge branch 'netcore/netcore' into netcore/members-userstore 2021-02-01 17:48:47 +00:00
Emma Garland 69da581260 Merge branch 'netcore/members-userstore' into netcore/feature/members-roles 2021-02-01 17:47:41 +00:00
Emma Garland 152ad9684c Username passed into roles. Added initial roles store functionality. Updated user roles functionality to persist the member group. 2021-02-01 17:43:11 +00:00
Emma Garland 8caf2a0e62 Initial check in of roles work in the store. Not currently functional 2021-01-29 16:43:50 +00:00
Emma Garland 6df67cd93e Corrected password logic 2021-01-29 15:03:06 +00:00
Emma Garland 7dce742b96 UTC date 2021-01-29 14:54:43 +00:00
Emma Garland af07790528 Remove comment 2021-01-29 14:18:34 +00:00
Emma Garland c8d1d16257 Datetime.now, since identity member doesn't have lastupdated date 2021-01-29 13:10:02 +00:00
Emma Garland f30d6f4cad Back to hashing the password instead of using the token approach 2021-01-29 13:01:59 +00:00
Emma Garland 2893dcb8b8 Ensure last password updated date is set when password is changed during member update. Moved password logic to use identity instead of just hashing the password. 2021-01-29 11:41:58 +00:00
Emma Garland 6ff38582d6 Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-28 11:37:47 +00:00
Emma Garland ce43c979b5 Added MemberController unit tests. Removed TODO. 2021-01-28 11:35:02 +00:00
Emma Garland 90899c68f8 Removed todos 2021-01-26 11:58:02 +00:00
Emma Garland 9b9a3d9197 Remove todo since bug is not happening 2021-01-26 11:55:12 +00:00
Emma Garland c7725953f3 Remove TODOs since have been covered by other tickets 2021-01-26 11:52:32 +00:00
Bjarke Berg 0d0a10d861 Merge remote-tracking branch 'origin/netcore/netcore' into netcore/members-userstore 2021-01-26 08:26:15 +01:00
Bjarke Berg d224071dda Merge remote-tracking branch 'origin/netcore/netcore' into netcore/members-userstore 2021-01-26 08:25:31 +01:00
Emma Garland 7df0040127 Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-25 10:07:53 +00:00
Bjarke Berg 74efd7fbda Fixed issues with GetTreeNode 2021-01-22 09:21:42 +01:00
Emma Garland b8ef38d287 Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-20 17:57:16 +00:00
Emma Garland 6dd5ae910a Fixed tests, merged latest 2021-01-20 17:42:31 +00:00
Emma Garland 4cd9fcc36d Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-20 14:27:54 +00:00
Emma Garland 9ba78497a0 Merge branch 'netcore/netcore' into netcore/members-userstore
# Conflicts:
#	src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs
#	src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MemberServiceTests.cs
2021-01-19 15:29:40 +00:00
Emma Garland 1431a0a076 Merge branch 'netcore/netcore' into netcore/members-userstore
# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
2021-01-15 16:51:23 +00:00
Emma Garland e5d7f53a1e Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-12 14:28:47 +00:00
Emma Garland 5b4f9a5100 Separated properties by a line 2021-01-11 17:03:34 +00:00
Emma Garland 1a866a3b1e Explicit types, braces style and expression bodies in the MemberService, and renamed rolestyped method. Namespace amendments. 2021-01-11 17:01:44 +00:00
Emma Garland da8f8091fa Namespace update. 2021-01-11 15:12:50 +00:00
Emma Garland d285f626a0 Pull request peer review changes - renamed password method, removed unneeded non-strongly typed Member GetAllRoles() method, removed duplicate Normalizer test and rename normalizer, removed currently unneeded events on MembersUserManager. Fixed namespaces and ensured tests in correct location. 2021-01-11 14:24:49 +00:00
Emma Garland 14b1046c17 Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-11 10:46:50 +00:00
Emma Garland d1a90ad0ff Merge branch 'netcore/netcore' into netcore/members-userstore 2021-01-08 10:30:50 +00:00
Emma Garland ba317b24dc Merge branch 'netcore/netcore' into netcore/members-userstore
# Conflicts:
#	src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
#	src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs
#	src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
#	src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
#	src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
2021-01-04 17:04:05 +00:00
Emma Garland c5c01146b5 Remove commented out 2020-12-11 14:35:25 +00:00
Emma Garland 67efe92858 Fixed tests, took out default data 2020-12-11 14:32:13 +00:00
Emma Garland 2c006ea99f PR comments updated. Reorganised logic. Removed unused functionality. Started to reorganise update and save roles functionality. 2020-12-09 18:36:39 +00:00
Emma Garland 6fb8724886 Renamed tests 2020-12-09 10:44:30 +00:00
Emma Garland 3f0e2f9054 typo in tests 2020-12-09 09:53:34 +00:00
Emma Garland ec09c9b09f removed unwanted members folder 2020-12-09 09:52:58 +00:00
Emma Garland aa1fd298cd Re-included password for comparison 2020-12-08 17:27:22 +00:00
Emma Garland 205edf57b7 Added store functionality based on backoffice user (to be revisited as a lot of it may be shareable) and also added a new members service method for membergroups. 2020-12-08 17:18:22 +00:00
Emma Garland cea1ed9771 Merge branch 'netcore/netcore' into netcore/members-userstore 2020-12-08 11:26:48 +00:00
Emma Garland 2f7766a972 Updated correct class, commented 2020-12-08 11:25:01 +00:00
Emma Garland 9531550e86 Merge branch 'netcore/netcore' into netcore/members-userstore 2020-12-08 11:04:17 +00:00
emmagarland f24dad1f30 More renaming, however if the password isn't validated then there will be an exception thrown in identity. 2020-12-08 02:07:27 +00:00
emmagarland 3f0e7ab315 Merged and updated according to shared latest work, renamed to Members instead of UmbracoMembers. Tests currently red, fixing next. Empty appsettings again. 2020-12-08 01:57:14 +00:00
emmagarland bcccbd3c73 Removed roles as not currently used for member 2020-12-07 17:40:21 +00:00
emmagarland b59935c799 Merge branch 'netcore/netcore' into netcore/members-userstore 2020-12-07 17:26:15 +00:00
emmagarland 79dccc87b1 Enable change tracking via mapper.
Added more unit tests for member controller.
2020-12-06 17:23:38 +00:00
emmagarland 40f2a881ab Added is dirty properties and updated to reflect linter update 2020-12-05 23:44:50 +00:00
emmagarland 0560bef48c Merge branch 'netcore/netcore' into netcore/members-userstore 2020-12-05 18:14:09 +00:00
Emma Garland 57684a8672 Removed unwanted comments 2020-12-03 17:54:02 +00:00
Emma Garland 785570761a Removed identity work for now 2020-12-03 15:59:54 +00:00
Emma Garland cf2252f967 Reordered for better comparison 2020-12-03 14:59:57 +00:00
emmagarland f1fbeb8ad3 Set hasIdentity if there is an ID, and logical adjustments to set passwords corectly 2020-12-03 01:27:54 +00:00
emmagarland 55ddc8cc49 Reordered to ensure password isn't always empty, and renamed for clarity s per UmbracoIdentity 2020-12-01 21:31:37 +00:00
emmagarland 8a552a10d9 Added comments to understand where to implement 2020-12-01 20:51:13 +00:00
emmagarland cf56731e42 Reverted back so avoid unwanted diffs 2020-12-01 20:44:21 +00:00
emmagarland fba4126a62 Switched back to correct services 2020-12-01 20:41:12 +00:00
emmagarland 75bee025ce Fixed tests 2020-12-01 19:47:56 +00:00
Emma Garland f0f17b3cf8 Stop duplicate saving 2020-12-01 19:06:36 +00:00
Emma Garland 9c73429da7 Merge branch 'netcore/netcore' into netcore/members-userstore
# Conflicts:
#	src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
#	src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
2020-12-01 18:19:36 +00:00
Emma Garland 9c41d335ea Merge branch 'netcore/netcore' into netcore/members-userstore
# Conflicts:
#	src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
#	src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
2020-12-01 17:37:23 +00:00
emmagarland 17df4eb607 Members mapping definition updated, and commented out duplicate password work 2020-11-30 01:07:02 +00:00
emmagarland 5b4b948543 Added more layers to have a vertical slice through Umbraco for CreateMember via backoffice.
Still lots to implement and test, not complete and needs early review.
2020-11-30 00:45:38 +00:00
emmagarland e7a21bcc18 Check for no errors 2020-11-29 13:59:08 +00:00
emmagarland 8572d7bbd9 Updated member type, and added start of unit tests for new store. Removed all unused properties for now to implement later. 2020-11-29 13:56:58 +00:00
emmagarland c95e560c54 Initial creation of templates for the member identity user, store and manager. Very much in flux, needs tests and implementations.
Based on BackOfficeIdentityUser and UmbracoIdentity.
2020-11-23 16:05:12 +00:00
86 changed files with 2742 additions and 478 deletions
@@ -3,7 +3,7 @@
namespace Umbraco.Core.Configuration
{
/// <summary>
/// The password configuration for back office users
/// The password configuration for members
/// </summary>
public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration
{
+3
View File
@@ -41,6 +41,9 @@ namespace Umbraco.Core
public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
public const string DefaultMemberTypeAlias = "Member";
/// <summary>
/// The prefix used for external identity providers for their authentication type
/// </summary>
@@ -0,0 +1,32 @@
using Umbraco.Core.Mapping;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Core.Models.Mapping
{
/// <inheritdoc />
public class MemberMapDefinition : IMapDefinition
{
/// <inheritdoc />
public void DefineMaps(UmbracoMapper mapper) => mapper.Define<MemberSave, IMember>(Map);
private static void Map(MemberSave source, IMember target, MapperContext context)
{
target.IsApproved = source.IsApproved;
target.Name = source.Name;
target.Email = source.Email;
target.Key = source.Key;
target.Username = source.Username;
target.Comments = source.Comments;
target.CreateDate = source.CreateDate;
target.UpdateDate = source.UpdateDate;
target.Email = source.Email;
// TODO: ensure all properties are mapped as required
//target.Id = source.Id;
//target.ParentId = -1;
//target.Path = "-1," + source.Id;
//TODO: add groups as required
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
@@ -5,7 +5,6 @@ using System.Security.Claims;
namespace Umbraco.Core.Security
{
/// <summary>
/// A custom user identity for the Umbraco backoffice
/// </summary>
@@ -1,29 +1,47 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.Querying;
namespace Umbraco.Core.Services
{
public interface IMembershipRoleService<out T>
public interface IMembershipRoleService<out T>
where T : class, IMembershipUser
{
void AddRole(string roleName);
IEnumerable<string> GetAllRoles();
IEnumerable<IMemberGroup> GetAllRoles();
IEnumerable<string> GetAllRoles(int memberId);
IEnumerable<string> GetAllRoles(string username);
IEnumerable<int> GetAllRolesIds();
IEnumerable<int> GetAllRolesIds(int memberId);
IEnumerable<int> GetAllRolesIds(string username);
IEnumerable<T> GetMembersInRole(string roleName);
IEnumerable<T> FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
bool DeleteRole(string roleName, bool throwIfBeingUsed);
void AssignRole(string username, string roleName);
void AssignRoles(string[] usernames, string[] roleNames);
void DissociateRole(string username, string roleName);
void DissociateRoles(string[] usernames, string[] roleNames);
void AssignRole(int memberId, string roleName);
void AssignRoles(int[] memberIds, string[] roleNames);
void DissociateRole(int memberId, string roleName);
void DissociateRoles(int[] memberIds, string[] roleNames);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -8,6 +8,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Services.Implement;
using Umbraco.Infrastructure.Services.Implement;
namespace Umbraco.Web.Cache
{
@@ -11,7 +11,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Services.Implement;
using Umbraco.Net;
namespace Umbraco.Core.Compose
@@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.DependencyInjection;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models.Mapping;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Models.Mapping;
namespace Umbraco.Infrastructure.DependencyInjection
@@ -30,6 +32,7 @@ namespace Umbraco.Infrastructure.DependencyInjection
.Add<TagMapDefinition>()
.Add<TemplateMapDefinition>()
.Add<UserMapDefinition>()
.Add<MemberMapDefinition>()
.Add<LanguageMapDefinition>()
.Add<IdentityMapDefinition>();
@@ -9,12 +9,12 @@ using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.DependencyInjection;
using Umbraco.Core.Events;
using Umbraco.Core.Hosting;
using Umbraco.Core.Packaging;
using Umbraco.Core.Routing;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Infrastructure.Services.Implement;
namespace Umbraco.Infrastructure.DependencyInjection
{
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Net.Http;
using System.Text;
@@ -12,6 +12,7 @@ using Umbraco.Core.Security;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Install.InstallSteps
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -336,7 +336,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
protected override void PersistUpdatedItem(IMember entity)
{
{
// update
entity.UpdatingEntity();
@@ -347,7 +347,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
NodeDto parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
@@ -355,10 +355,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// create the dto
var dto = ContentBaseFactory.BuildDto(entity);
MemberDto dto = ContentBaseFactory.BuildDto(entity);
// update the node dto
var nodeDto = dto.ContentDto.NodeDto;
NodeDto nodeDto = dto.ContentDto.NodeDto;
Database.Update(nodeDto);
// update the content dto
@@ -409,7 +409,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//get the group id
var grpQry = Query<IMemberGroup>().Where(group => group.Name.Equals(roleName));
var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault();
if (memberGroup == null) return Enumerable.Empty<IMember>();
if (memberGroup == null)
return Enumerable.Empty<IMember>();
// get the members by username
var query = Query<IMember>();
@@ -464,7 +465,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
var grpQry = Query<IMemberGroup>().Where(group => group.Name.Equals(groupName));
var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault();
if (memberGroup == null) return Enumerable.Empty<IMember>();
if (memberGroup == null)
return Enumerable.Empty<IMember>();
var subQuery = Sql().Select("Member").From<Member2MemberGroupDto>().Where<Member2MemberGroupDto>(dto => dto.MemberGroup == memberGroup.Id);
@@ -539,7 +541,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s
.Select<ContentVersionDto>(x => x.Id)
.From<ContentVersionDto>()
.From<ContentVersionDto>()
.InnerJoin<NodeDto>().On<NodeDto, ContentVersionDto>((l, r) => l.NodeId == r.NodeId)
.InnerJoin<MemberDto>().On<MemberDto, NodeDto>((l, r) => l.NodeId == r.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType"))
@@ -614,7 +616,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var cached = IsolatedCache.GetCacheItem<IMember>(RepositoryCacheKeys.GetKey<IMember>(dto.NodeId));
if (cached != null && cached.VersionId == dto.ContentVersionDto.Id)
{
content[i] = (Member) cached;
content[i] = (Member)cached;
continue;
}
}
@@ -656,7 +658,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// get properties - indexed by version id
var versionId = dto.ContentVersionDto.Id;
var temp = new TempContent<Member>(dto.ContentDto.NodeId,versionId, 0, memberType);
var temp = new TempContent<Member>(dto.ContentDto.NodeId, versionId, 0, memberType);
var properties = GetPropertyCollections(new List<TempContent<Member>> { temp });
member.Properties = properties[versionId];
@@ -7,6 +7,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Infrastructure.Services.Implement;
namespace Umbraco.Web.PropertyEditors
{
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Core.Security;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// A <see cref="UserClaimsPrincipalFactory{TUser}" for the back office/>
@@ -3,7 +3,7 @@ using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
public class BackOfficeIdentityBuilder : IdentityBuilder
{
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Umbraco back office specific <see cref="IdentityErrorDescriber"/>
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Identity options specifically for the back office identity implementation
@@ -2,12 +2,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// The identity user used for the back office
@@ -93,7 +93,9 @@ namespace Umbraco.Core.Security
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
@@ -186,13 +188,13 @@ namespace Umbraco.Core.Security
{
get
{
var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating the IUser IsApproved
/// Gets or sets a value indicating whether the IUser IsApproved
/// </summary>
public bool IsApproved { get; set; }
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
@@ -16,7 +17,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
// TODO: Make this into a base class that can be re-used
@@ -437,7 +438,7 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Returns the roles (user groups) for this user
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Core.Security
{
@@ -1,6 +1,4 @@
using Umbraco.Core.Security;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// The user manager for the back office
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Umbraco.Core.Security;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily
@@ -0,0 +1,9 @@
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// The user manager for members
/// </summary>
public interface IMembersUserManager : IUmbracoUserManager<MembersIdentityUser>
{
}
}
@@ -4,11 +4,11 @@ using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
@@ -16,7 +16,7 @@ namespace Umbraco.Core.Security
/// </summary>
/// <typeparam name="TUser">The type of user</typeparam>
public interface IUmbracoUserManager<TUser> : IDisposable
where TUser : BackOfficeIdentityUser
where TUser : UmbracoIdentityUser
{
/// <summary>
/// Gets the user id of a user
@@ -223,12 +223,64 @@ namespace Umbraco.Core.Security
/// </returns>
Task<IdentityResult> CreateAsync(TUser user);
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
/// <param name="user">The user whose role names to retrieve.</param>
/// <returns>The Task that represents the asynchronous operation, containing a list of role names.</returns>
Task<IList<string>> GetRolesAsync(TUser user);
/// <summary>
/// Removes the specified user from the named roles.
/// </summary>
/// <param name="user">The user to remove from the named roles.</param>
/// <param name="roles">The name of the roles to remove the user from.</param>
/// <returns>The Task that represents the asynchronous operation, containing the IdentityResult of the operation.</returns>
Task<IdentityResult> RemoveFromRolesAsync(TUser user, IEnumerable<string> roles);
/// <summary>
/// Add the specified user to the named roles
/// </summary>
/// <param name="user">The user to add to the named roles</param>
/// <param name="roles">The name of the roles to add the user to.</param>
/// <returns>The Task that represents the asynchronous operation, containing the IdentityResult of the operation</returns>
Task<IdentityResult> AddToRolesAsync(TUser user, IEnumerable<string> roles);
/// <summary>
/// Creates the specified <paramref name="user"/> in the backing store with a password,
/// as an asynchronous operation.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="password">The password to add to the user.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
/// of the operation.
/// </returns>
Task<IdentityResult> CreateAsync(TUser user, string password);
/// <summary>
/// Generate a password for a user based on the current password validator
/// </summary>
/// <returns>A generated password</returns>
string GeneratePassword();
/// <summary>
/// Hashes a password for a null user based on the default password hasher
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
string HashPassword(string password);
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code
/// Should return <see cref="IdentityResult.Success"/> if validation is successful
/// </summary>
/// <param name="password">The password.</param>
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
Task<IdentityResult> ValidatePasswordAsync(string password);
/// <summary>
/// Generates an email confirmation token for the specified user.
/// </summary>
@@ -1,6 +1,6 @@
using System.Threading.Tasks;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// An IUserStore interface part to implement if the store supports validating user session Ids
@@ -1,9 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Extensions
namespace Umbraco.Infrastructure.Security
{
public static class IdentityExtensions
{
@@ -1,13 +1,13 @@
using System;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
public class IdentityMapDefinition : IMapDefinition
{
@@ -37,6 +37,20 @@ namespace Umbraco.Core.Security
target.ResetDirtyProperties(true);
target.EnableChangeTracking();
});
mapper.Define<IMember, MembersIdentityUser>(
(source, context) =>
{
var target = new MembersIdentityUser(source.Id);
target.DisableChangeTracking();
return target;
},
(source, target, context) =>
{
Map(source, target);
target.ResetDirtyProperties(true);
target.EnableChangeTracking();
});
}
// Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled
@@ -76,9 +90,24 @@ namespace Umbraco.Core.Security
//target.Roles =;
}
private static string GetPasswordHash(string storedPass)
private void Map(IMember source, MembersIdentityUser target)
{
return storedPass.StartsWith(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;
}
}
@@ -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.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MemberRolesUserStore : RoleStoreBase<IdentityRole<string>, string, IdentityUserRole<string>, IdentityRoleClaim<string>>
{
private readonly IMemberService _memberService;
private readonly IMemberGroupService _memberGroupService;
private readonly IScopeProvider _scopeProvider;
public MemberRolesUserStore(IMemberService memberService, IMemberGroupService memberGroupService, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
}
/// <inheritdoc />
public override IQueryable<IdentityRole<string>> Roles { get; }
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityResult> UpdateAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityResult> DeleteAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityRole<string>> FindByIdAsync(string id, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IdentityRole<string>> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task<IList<Claim>> GetClaimsAsync(IdentityRole<string> role, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task AddClaimAsync(IdentityRole<string> role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
/// <inheritdoc />
public override Task RemoveClaimAsync(IdentityRole<string> role, Claim claim, CancellationToken cancellationToken = new CancellationToken()) => throw new System.NotImplementedException();
}
}
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Infrastructure.Security
{
public class MembersIdentityBuilder : IdentityBuilder
{
public MembersIdentityBuilder(IServiceCollection services) : base(typeof(MembersIdentityUser), services)
{
}
public MembersIdentityBuilder(Type role, IServiceCollection services) : base(typeof(MembersIdentityUser), role, services)
{
}
/// <summary>
/// Adds a token provider for the <seealso cref="MembersIdentityUser"/>.
/// </summary>
/// <param name="providerName">The name of the provider to add.</param>
/// <param name="provider">The type of the <see cref="IUserTwoFactorTokenProvider{UmbracoMembersIdentityUser}"/> to add.</param>
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
public override IdentityBuilder AddTokenProvider(string providerName, Type provider)
{
if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
{
throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}");
}
Services.Configure<MembersIdentityOptions>(options =>
{
options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider);
});
Services.AddTransient(provider);
return this;
}
}
}
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Identity options specifically for the Umbraco members identity implementation
/// </summary>
public class MembersIdentityOptions : IdentityOptions
{
}
}
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// The identity user used for the member
/// </summary>
public class MembersIdentityUser : UmbracoIdentityUser
{
private string _name;
private string _passwordConfig;
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
// Custom comparer for enumerables
private static readonly DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>> s_groupsComparer = new DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>>(
(groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
groups => groups.GetHashCode());
/// <summary>
/// Initializes a new instance of the <see cref="MembersIdentityUser"/> class.
/// </summary>
public MembersIdentityUser(int userId)
{
// use the property setters - they do more than just setting a field
Id = UserIdToString(userId);
}
public MembersIdentityUser()
{
}
/// <summary>
/// Used to construct a new instance without an identity
/// </summary>
public static MembersIdentityUser CreateNew(string username, string email, string memberTypeAlias, string name = null)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
}
var user = new MembersIdentityUser();
user.DisableChangeTracking();
user.UserName = username;
user.Email = email;
user.MemberTypeAlias = memberTypeAlias;
user.Id = null;
user.HasIdentity = false;
user._name = name;
user.EnableChangeTracking();
return user;
}
/// <summary>
/// Gets or sets the member's real name
/// </summary>
public string Name
{
get => _name;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
}
/// <summary>
/// Gets or sets the password config
/// </summary>
public string PasswordConfig
{
get => _passwordConfig;
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <summary>
/// Gets or sets the user groups
/// </summary>
public IReadOnlyCollection<IReadOnlyUserGroup> Groups
{
get => _groups;
set
{
_groups = value.Where(x => x.Alias != null).ToArray();
var roles = new List<IdentityUserRole<string>>();
foreach (IdentityUserRole<string> identityUserRole in _groups.Select(x => new IdentityUserRole<string>
{
RoleId = x.Alias,
UserId = Id
}))
{
roles.Add(identityUserRole);
}
// now reset the collection
Roles = roles;
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
}
}
/// <summary>
/// Gets a value indicating whether the member is locked out
/// </summary>
public bool IsLockedOut
{
get
{
bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now;
return isLocked;
}
}
/// <summary>
/// Gets or sets a value indicating whether the member is approved
/// </summary>
public bool IsApproved { get; set; }
/// <summary>
/// Gets or sets the alias of the member type
/// </summary>
public string MemberTypeAlias { get; set; }
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
}
}
@@ -0,0 +1,700 @@
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.Core;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MembersUserStore : UserStoreBase<MembersIdentityUser, IdentityRole<string>, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
{
private readonly IMemberService _memberService;
private readonly UmbracoMapper _mapper;
private readonly IScopeProvider _scopeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="MembersUserStore"/> class for the members identity store
/// </summary>
/// <param name="memberService">The member service</param>
/// <param name="mapper">The mapper for properties</param>
/// <param name="scopeProvider">The scope provider</param>
/// <param name="describer">The error describer</param>
public MembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override IQueryable<MembersIdentityUser> Users => throw new NotImplementedException();
/// <inheritdoc />
public override Task<string> GetNormalizedUserNameAsync(MembersIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedUserNameAsync(MembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// create member
IMember memberEntity = _memberService.CreateMember(
user.UserName,
user.Email,
user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name,
user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
UpdateMemberProperties(memberEntity, user);
// create the member
_memberService.Save(memberEntity);
if (!memberEntity.HasIdentity)
{
throw new DataException("Could not create the member, check logs for details");
}
// re-assign id
user.Id = UserIdToString(memberEntity.Id);
// [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
// var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
// TODO: confirm re externallogins implementation
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// user.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<IdentityResult> UpdateAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Attempt<int> asInt = user.Id.TryConvertTo<int>();
if (asInt == false)
{
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
}
using (IScope scope = _scopeProvider.CreateScope())
{
IMember found = _memberService.GetById(asInt.Result);
if (found != null)
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
if (UpdateMemberProperties(found, user))
{
_memberService.Save(found);
}
// TODO: when to implement external login service?
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// found.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
}
scope.Complete();
}
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<IdentityResult> DeleteAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IMember found = _memberService.GetById(UserIdToInt(user.Id));
if (found != null)
{
_memberService.Delete(found);
}
// TODO: when to implement external login service?
//_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id));
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
/// <inheritdoc />
protected override Task<MembersIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember user = _memberService.GetById(UserIdToInt(userId));
if (user == null)
{
return Task.FromResult((MembersIdentityUser)null);
}
return Task.FromResult(AssignLoginsCallback(_mapper.Map<MembersIdentityUser>(user)));
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByNameAsync(string userName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember user = _memberService.GetByUsername(userName);
if (user == null)
{
return Task.FromResult((MembersIdentityUser)null);
}
MembersIdentityUser result = AssignLoginsCallback(_mapper.Map<MembersIdentityUser>(user));
return Task.FromResult(result);
}
/// <inheritdoc />
public override async Task SetPasswordHashAsync(MembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
}
/// <inheritdoc />
public override async Task<bool> HasPasswordAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
// This checks if it's null
var result = await base.HasPasswordAsync(user, cancellationToken);
if (result)
{
// we also want to check empty
return string.IsNullOrEmpty(user.PasswordHash) == false;
}
return false;
}
/// <inheritdoc />
public override Task<MembersIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IMember member = _memberService.GetByEmail(email);
MembersIdentityUser result = member == null
? null
: _mapper.Map<MembersIdentityUser>(member);
return Task.FromResult(AssignLoginsCallback(result));
}
/// <inheritdoc />
public override Task<string> GetNormalizedEmailAsync(MembersIdentityUser user, CancellationToken cancellationToken)
=> GetEmailAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedEmailAsync(MembersIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
/// <inheritdoc />
public override Task AddLoginAsync(MembersIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
ICollection<IIdentityUserLogin> logins = user.Logins;
var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString());
IdentityUserLogin userLogin = instance;
logins.Add(userLogin);
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task RemoveLoginAsync(MembersIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
if (userLogin != null)
{
user.Logins.Remove(userLogin);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task<IList<UserLoginInfo>> GetLoginsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult((IList<UserLoginInfo>)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
}
/// <inheritdoc />
protected override async Task<IdentityUserLogin<string>> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
if (user == null)
{
return null;
}
IList<UserLoginInfo> logins = await GetLoginsAsync(user, cancellationToken);
UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider);
if (found == null)
{
return null;
}
return new IdentityUserLogin<string>
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null
UserId = user.Id
};
}
/// <inheritdoc />
protected override Task<IdentityUserLogin<string>> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var logins = new List<IIdentityUserLogin>();
// TODO: external login needed?
//_externalLoginService.Find(loginProvider, providerKey).ToList();
if (logins.Count == 0)
{
return Task.FromResult((IdentityUserLogin<string>)null);
}
IIdentityUserLogin found = logins[0];
return Task.FromResult(new IdentityUserLogin<string>
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = null, // TODO: We don't store this value so it will be null
UserId = found.UserId
});
}
/// <inheritdoc />
public override Task AddToRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole == null)
{
_memberService.AssignRole(user.UserName, role);
user.AddRole(role);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public override Task RemoveFromRoleAsync(MembersIdentityUser user, string role, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole != null)
{
_memberService.DissociateRole(user.UserName, userRole.RoleId);
user.Roles.Remove(userRole);
}
return Task.CompletedTask;
}
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IEnumerable<string> currentRoles = _memberService.GetAllRoles(user.UserName);
ICollection<IdentityUserRole<string>> roles = currentRoles.Select(role => new IdentityUserRole<string>
{
RoleId = role,
UserId = user.Id
}).ToList();
user.Roles = roles;
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
}
/// <summary>
/// Returns true if a user is in the role
/// </summary>
public override Task<bool> IsInRoleAsync(MembersIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
}
/// <summary>
/// Lists all users of a given role.
/// </summary>
public override Task<IList<MembersIdentityUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
IEnumerable<IMember> members = _memberService.GetMembersByMemberType(normalizedRoleName);
IList<MembersIdentityUser> membersIdentityUsers = members.Select(x => _mapper.Map<MembersIdentityUser>(x)).ToList();
return Task.FromResult(membersIdentityUsers);
}
/// <inheritdoc/>
protected override Task<IdentityRole<string>> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == normalizedRoleName);
if (group == null)
{
return Task.FromResult((IdentityRole<string>)null);
}
return Task.FromResult(new IdentityRole<string>(group.Name)
{
//TODO: what should the alias be?
Id = @group.Id.ToString()
});
}
/// <inheritdoc/>
protected override async Task<IdentityUserRole<string>> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken)
{
MembersIdentityUser user = await FindUserAsync(userId, cancellationToken);
if (user == null)
{
return null;
}
IdentityUserRole<string> found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId));
return found;
}
/// <inheritdoc />
public override Task<string> GetSecurityStampAsync(MembersIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
? user.PasswordHash.GenerateHash()
: user.SecurityStamp);
}
private MembersIdentityUser AssignLoginsCallback(MembersIdentityUser user)
{
if (user != null)
{
//TODO: when to
//user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetAll(UserIdToInt(user.Id))));
}
return user;
}
private bool UpdateMemberProperties(IMember member, MembersIdentityUser identityUserMember)
{
var anythingChanged = false;
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastLoginDateUtc))
|| (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false)
|| (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value))
{
anythingChanged = true;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUserMember.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUserMember.LastLoginDateUtc.Value.ToLocalTime();
member.LastLoginDate = dt;
}
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.LastPasswordChangeDateUtc))
|| (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value))
{
anythingChanged = true;
member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime();
}
//if (identityUser.IsPropertyDirty(nameof(MembersIdentityUser.EmailConfirmed))
// || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false)
// || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed))
//{
// anythingChanged = true;
// user.EmailConfirmedDate = identityUser.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 (user.SecurityStamp != identityUser.SecurityStamp)
//{
// anythingChanged = true;
// user.SecurityStamp = identityUser.SecurityStamp;
//}
// TODO: Fix this for Groups too (as per backoffice comment)
if (identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MembersIdentityUser.Groups)))
{
}
// reset all changes
identityUserMember.ResetDirtyProperties(false);
return anythingChanged;
}
private static int UserIdToInt(string userId)
{
Attempt<int> attempt = userId.TryConvertTo<int>();
if (attempt.Success)
{
return attempt.Result;
}
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
}
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<Claim>> GetClaimsAsync(MembersIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task AddClaimsAsync(MembersIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task ReplaceClaimAsync(MembersIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task RemoveClaimsAsync(MembersIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<MembersIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task<IdentityUserToken<string>> FindTokenAsync(MembersIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
}
}
@@ -1,12 +1,12 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
/// </summary>
public class BackOfficeLookupNormalizer : ILookupNormalizer
public class NoOpLookupNormalizer : ILookupNormalizer
{
// TODO: Do we need this?
@@ -1,8 +1,8 @@
using Umbraco.Core;
using Umbraco.Core.Security;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Event args used when signing out
/// </summary>
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
@@ -7,11 +8,11 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Net;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
/// <summary>
/// Abstract class for Umbraco User Managers for back office users or front-end members
/// </summary>
@@ -33,12 +34,11 @@ namespace Umbraco.Core.Security
IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<TUser>> logger,
IOptions<TPasswordConfig> passwordConfiguration)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, new NoOpLookupNormalizer(), errors, services, logger)
{
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
@@ -49,7 +49,7 @@ namespace Umbraco.Core.Security
/// <inheritdoc />
public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// </summary>
@@ -73,8 +73,8 @@ namespace Umbraco.Core.Security
/// Used to validate a user's session
/// </summary>
/// <param name="userId">The user id</param>
/// <param name="sessionId">The sesion id</param>
/// <returns>True if the sesion is valid, else false</returns>
/// <param name="sessionId">The session id</param>
/// <returns>True if the session is valid, else false</returns>
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore<TUser>;
@@ -102,13 +102,58 @@ namespace Umbraco.Core.Security
/// <returns>The generated password</returns>
public string GeneratePassword()
{
if (_passwordGenerator == null)
_passwordGenerator ??= new PasswordGenerator(PasswordConfiguration);
string password = _passwordGenerator.GeneratePassword();
return password;
}
/// <summary>
/// Generates a hashed password based on the default password hasher
/// No existing identity user is required and this does not validate the password
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
public string HashPassword(string password)
{
IPasswordHasher<TUser> passwordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
string hashedPassword = passwordHasher.HashPassword(null, password);
return hashedPassword;
}
/// <summary>
/// Used to validate the password without an identity user
/// Validation code is based on the default ValidatePasswordAsync code
/// Should return <see cref="IdentityResult.Success"/> if validation is successful
/// </summary>
/// <param name="password">The password.</param>
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
public async Task<IdentityResult> ValidatePasswordAsync(string password)
{
var errors = new List<IdentityError>();
var isValid = true;
foreach (IPasswordValidator<TUser> v in PasswordValidators)
{
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
IdentityResult result = await v.ValidateAsync(this, null, password);
if (!result.Succeeded)
{
if (result.Errors.Any())
{
errors.AddRange(result.Errors);
}
isValid = false;
}
}
var password = _passwordGenerator.GeneratePassword();
return password;
if (!isValid)
{
Logger.LogWarning(14, "Password validation failed: {errors}.", string.Join(";", errors.Select(e => e.Code)));
return IdentityResult.Failed(errors.ToArray());
}
return IdentityResult.Success;
}
/// <inheritdoc />
@@ -2,7 +2,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Core.Security
namespace Umbraco.Infrastructure.Security
{
public class UserInviteEventArgs : IdentityAuditEventArgs
{
@@ -1,17 +1,18 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Composing;
using Umbraco.Core;
using Umbraco.Core.Events;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
namespace Umbraco.Core.Services.Implement
namespace Umbraco.Infrastructure.Services.Implement
{
/// <summary>
/// Represents the MemberService.
@@ -109,19 +110,20 @@ namespace Umbraco.Core.Services.Implement
/// <param name="email">Email of the Member to create</param>
/// <param name="name">Name of the Member to create</param>
/// <param name="memberTypeAlias">Alias of the MemberType the Member should be based on</param>
/// <exception cref="ArgumentException">Thrown when a member type for the given alias isn't found</exception>
/// <returns><see cref="IMember"/></returns>
public IMember CreateMember(string username, string email, string name, string memberTypeAlias)
{
var memberType = GetMemberType(memberTypeAlias);
IMemberType memberType = GetMemberType(memberTypeAlias);
if (memberType == null)
{
throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias));
}
var member = new Member(name, email.ToLower().Trim(), username, memberType);
using (var scope = ScopeProvider.CreateScope())
{
CreateMember(scope, member, 0, false);
scope.Complete();
}
using IScope scope = ScopeProvider.CreateScope();
CreateMember(scope, member, 0, false);
scope.Complete();
return member;
}
@@ -312,7 +314,9 @@ namespace Umbraco.Core.Services.Implement
// if saving is cancelled, media remains without an identity
var saveEventArgs = new SaveEventArgs<IMember>(member);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
return;
}
_memberRepository.Save(member);
@@ -321,7 +325,9 @@ namespace Umbraco.Core.Services.Implement
}
if (withIdentity == false)
{
return;
}
Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}");
}
@@ -801,11 +807,11 @@ namespace Umbraco.Core.Services.Implement
/// <inheritdoc />
public void Save(IMember member, bool raiseEvents = true)
{
//trimming username and email to make sure we have no trailing space
// trimming username and email to make sure we have no trailing space
member.Username = member.Username.Trim();
member.Email = member.Email.Trim();
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new SaveEventArgs<IMember>(member);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
@@ -828,6 +834,7 @@ namespace Umbraco.Core.Services.Implement
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs);
}
Audit(AuditType.Save, 0, member.Id);
scope.Complete();
@@ -924,18 +931,28 @@ namespace Umbraco.Core.Services.Implement
}
}
public IEnumerable<string> GetAllRoles()
/// <summary>
/// Returns a list of all member roles
/// </summary>
/// <returns>A list of member roles</returns>
public IEnumerable<IMemberGroup> GetAllRoles()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
return _memberGroupRepository.GetMany().Select(x => x.Name).Distinct();
return _memberGroupRepository.GetMany().Select(x=>x).Distinct();
}
}
/// <summary>
/// Returns a list of all member roles for a given member ID
/// </summary>
/// <param name="memberId"></param>
/// <returns>A list of member roles</returns>
public IEnumerable<string> GetAllRoles(int memberId)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
@@ -945,17 +962,17 @@ namespace Umbraco.Core.Services.Implement
public IEnumerable<string> GetAllRoles(string username)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(username);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Name).Distinct();
}
}
public IEnumerable<int> GetAllRolesIds()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
@@ -964,27 +981,27 @@ namespace Umbraco.Core.Services.Implement
public IEnumerable<int> GetAllRolesIds(int memberId)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable<int> GetAllRolesIds(string username)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
var result = _memberGroupRepository.GetMemberGroupsForMember(username);
IEnumerable<IMemberGroup> result = _memberGroupRepository.GetMemberGroupsForMember(username);
return result.Select(x => x.Id).Distinct();
}
}
public IEnumerable<IMember> GetMembersInRole(string roleName)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
return _memberRepository.GetByMemberGroup(roleName);
@@ -993,7 +1010,7 @@ namespace Umbraco.Core.Services.Implement
public IEnumerable<IMember> FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(Constants.Locks.MemberTree);
return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
@@ -1002,71 +1019,66 @@ namespace Umbraco.Core.Services.Implement
public bool DeleteRole(string roleName, bool throwIfBeingUsed)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Constants.Locks.MemberTree);
if (throwIfBeingUsed)
{
// get members in role
var membersInRole = _memberRepository.GetByMemberGroup(roleName);
IEnumerable<IMember> membersInRole = _memberRepository.GetByMemberGroup(roleName);
if (membersInRole.Any())
{
throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
}
}
var query = Query<IMemberGroup>().Where(g => g.Name == roleName);
var found = _memberGroupRepository.Get(query).ToArray();
IQuery<IMemberGroup> query = Query<IMemberGroup>().Where(g => g.Name == roleName);
IMemberGroup[] found = _memberGroupRepository.Get(query).ToArray();
foreach (var memberGroup in found)
foreach (IMemberGroup memberGroup in found)
{
_memberGroupService.Delete(memberGroup);
}
scope.Complete();
return found.Length > 0;
}
}
public void AssignRole(string username, string roleName)
{
AssignRoles(new[] { username }, new[] { roleName });
}
public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName });
public void AssignRoles(string[] usernames, string[] roleNames)
{
using (var scope = ScopeProvider.CreateScope())
using (IScope scope = ScopeProvider.CreateScope())
{
scope.WriteLock(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(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(Constants.Locks.MemberTree);
_memberGroupRepository.AssignRoles(memberIds, roleNames);
@@ -1075,14 +1087,11 @@ namespace Umbraco.Core.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(Constants.Locks.MemberTree);
_memberGroupRepository.DissociateRoles(memberIds, roleNames);
@@ -1095,10 +1104,7 @@ namespace Umbraco.Core.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
@@ -1153,12 +1159,15 @@ namespace Umbraco.Core.Services.Implement
/// </remarks>
public MemberExportModel ExportMember(Guid key)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
using (IScope scope = ScopeProvider.CreateScope(autoComplete: true))
{
var query = Query<IMember>().Where(x => x.Key == key);
var member = _memberRepository.Get(query).FirstOrDefault();
IQuery<IMember> query = Query<IMember>().Where(x => x.Key == key);
IMember member = _memberRepository.Get(query).FirstOrDefault();
if (member == null) return null;
if (member == null)
{
return null;
}
var model = new MemberExportModel
{
@@ -1182,11 +1191,14 @@ namespace Umbraco.Core.Services.Implement
private static IEnumerable<MemberExportProperty> GetPropertyExportItems(IMember member)
{
if (member == null) throw new ArgumentNullException(nameof(member));
if (member == null)
{
throw new ArgumentNullException(nameof(member));
}
var exportProperties = new List<MemberExportProperty>();
foreach (var property in member.Properties)
foreach (IProperty property in member.Properties)
{
var propertyExportModel = new MemberExportProperty
{
@@ -1214,15 +1226,14 @@ namespace Umbraco.Core.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(Constants.Locks.MemberTree);
// TODO: What about content that has the contenttype as part of its composition?
var query = Query<IMember>().Where(x => x.ContentTypeId == memberTypeId);
IQuery<IMember> query = Query<IMember>().Where(x => x.ContentTypeId == memberTypeId);
var members = _memberRepository.Get(query).ToArray();
IMember[] members = _memberRepository.Get(query).ToArray();
var deleteEventArgs = new DeleteEventArgs<IMember>(members);
if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs))
@@ -1231,43 +1242,58 @@ namespace Umbraco.Core.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(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
}
}
@@ -12,6 +12,7 @@ using Umbraco.Core.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Common.Security;
namespace Umbraco.Tests.Integration.TestServerTest
@@ -158,6 +158,7 @@ namespace Umbraco.Tests.Integration.TestServerTest
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
.AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme)
.AddPreviewSupport()
.AddMvcAndRazor(mvcBuilding: mvcBuilder =>
@@ -229,6 +229,8 @@ namespace Umbraco.Tests.Integration.Testing
}
services.AddSignalR();
builder.AddMembersIdentity();
services.AddMvc();
CustomTestSetup(builder);
@@ -183,10 +183,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services
{
MemberService.AddRole("MyTestRole");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(1, found.Count());
Assert.AreEqual("MyTestRole", found.Single());
Assert.AreEqual("MyTestRole", found.Single().Name);
}
[Test]
@@ -195,10 +195,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.AddRole("MyTestRole");
MemberService.AddRole("MyTestRole");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(1, found.Count());
Assert.AreEqual("MyTestRole", found.Single());
Assert.AreEqual("MyTestRole", found.Single().Name);
}
[Test]
@@ -208,7 +208,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.AddRole("MyTestRole2");
MemberService.AddRole("MyTestRole3");
IEnumerable<string> found = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> found = MemberService.GetAllRoles();
Assert.AreEqual(3, found.Count());
}
@@ -292,7 +292,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.DeleteRole("MyTestRole1", false);
IEnumerable<string> memberRoles = MemberService.GetAllRoles();
IEnumerable<IMemberGroup> memberRoles = MemberService.GetAllRoles();
Assert.AreEqual(0, memberRoles.Count());
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Core.DependencyInjection;
using Umbraco.Infrastructure.Security;
using Umbraco.Tests.Integration.Testing;
using Umbraco.Web.BackOffice.DependencyInjection;
namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice
{
[TestFixture]
public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest
{
protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddMembersIdentity();
[Test]
public void AddMembersIdentity_ExpectMembersUserStoreResolvable()
{
IUserStore<MembersIdentityUser> userStore = Services.GetService<IUserStore<MembersIdentityUser>>();
Assert.IsNotNull(userStore);
Assert.AreEqual(typeof(MembersUserStore), userStore.GetType());
}
[Test]
public void AddMembersIdentity_ExpectMembersUserManagerResolvable()
{
IMembersUserManager userManager = Services.GetService<IMembersUserManager>();
Assert.NotNull(userManager);
}
}
}
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Core.DependencyInjection;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Tests.Integration.Testing;
using Umbraco.Web.BackOffice.DependencyInjection;
@@ -15,6 +15,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Hosting;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Routing;
using Umbraco.Web.Common.Install;
@@ -51,8 +52,10 @@ namespace Umbraco.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(MembersUserManager), new GreedyConstructorQuery()));
fixture.Customize(new AutoMoqCustomization());
@@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using NUnit.Framework;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
{
@@ -14,8 +14,9 @@ using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.BackOffice
{
[TestFixture]
public class BackOfficeClaimsPrincipalFactoryTests
@@ -1,60 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using NUnit.Framework;
using Umbraco.Core.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice
{
public class BackOfficeLookupNormalizerTests
{
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
Assert.AreEqual(name, normalizedName);
}
[Test]
public void NormalizeName_Expect_Input_Returned()
{
var name = Guid.NewGuid().ToString();
var sut = new BackOfficeLookupNormalizer();
var normalizedName = sut.NormalizeName(name);
Assert.AreEqual(name, normalizedName);
}
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
[Test]
public void NormalizeEmail_Expect_Input_Returned()
{
var email = $"{Guid.NewGuid()}@umbraco";
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
}
}
@@ -0,0 +1,169 @@
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.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.Common.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberIdentityUserManagerTests
{
private Mock<IUserStore<MembersIdentityUser>> _mockMemberStore;
private Mock<IOptions<MembersIdentityOptions>> _mockIdentityOptions;
private Mock<IPasswordHasher<MembersIdentityUser>> _mockPasswordHasher;
private Mock<IUserValidator<MembersIdentityUser>> _mockUserValidators;
private Mock<IEnumerable<IPasswordValidator<MembersIdentityUser>>> _mockPasswordValidators;
private Mock<ILookupNormalizer> _mockNormalizer;
private IdentityErrorDescriber _mockErrorDescriber;
private Mock<IServiceProvider> _mockServiceProviders;
private Mock<ILogger<UserManager<MembersIdentityUser>>> _mockLogger;
private Mock<IOptions<MemberPasswordConfigurationSettings>> _mockPasswordConfiguration;
public MembersUserManager CreateSut()
{
_mockMemberStore = new Mock<IUserStore<MembersIdentityUser>>();
_mockIdentityOptions = new Mock<IOptions<MembersIdentityOptions>>();
var idOptions = new MembersIdentityOptions { Lockout = { AllowedForNewUsers = false } };
_mockIdentityOptions.Setup(o => o.Value).Returns(idOptions);
_mockPasswordHasher = new Mock<IPasswordHasher<MembersIdentityUser>>();
var userValidators = new List<IUserValidator<MembersIdentityUser>>();
_mockUserValidators = new Mock<IUserValidator<MembersIdentityUser>>();
var validator = new Mock<IUserValidator<MembersIdentityUser>>();
userValidators.Add(validator.Object);
_mockPasswordValidators = new Mock<IEnumerable<IPasswordValidator<MembersIdentityUser>>>();
_mockNormalizer = new Mock<ILookupNormalizer>();
_mockErrorDescriber = new IdentityErrorDescriber();
_mockServiceProviders = new Mock<IServiceProvider>();
_mockLogger = new Mock<ILogger<UserManager<MembersIdentityUser>>>();
_mockPasswordConfiguration = new Mock<IOptions<MemberPasswordConfigurationSettings>>();
_mockPasswordConfiguration.Setup(x => x.Value).Returns(() =>
new MemberPasswordConfigurationSettings()
{
});
var pwdValidators = new List<PasswordValidator<MembersIdentityUser>>
{
new PasswordValidator<MembersIdentityUser>()
};
var userManager = new MembersUserManager(
new Mock<IIpResolver>().Object,
_mockMemberStore.Object,
_mockIdentityOptions.Object,
_mockPasswordHasher.Object,
userValidators,
pwdValidators,
new BackOfficeIdentityErrorDescriber(),
_mockServiceProviders.Object,
new Mock<IHttpContextAccessor>().Object,
new Mock<ILogger<UserManager<MembersIdentityUser>>>().Object,
_mockPasswordConfiguration.Object);
validator.Setup(v => v.ValidateAsync(
userManager,
It.IsAny<MembersIdentityUser>()))
.Returns(Task.FromResult(IdentityResult.Success)).Verifiable();
return userManager;
}
[Test]
public async Task GivenICreateUser_AndTheIdentityResultFailed_ThenIShouldGetAFailedResultAsync()
{
//arrange
MembersUserManager 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
MembersUserManager sut = CreateSut();
CancellationToken fakeCancellationToken = new CancellationToken() { };
IdentityError[] identityErrors =
{
new IdentityError()
{
Code = "IdentityError1",
Description = "There was an identity error when creating a user"
}
};
_mockMemberStore.Setup(x =>
x.CreateAsync(null, fakeCancellationToken))
.ReturnsAsync(IdentityResult.Failed(identityErrors));
//act
var identityResult = new Func<Task<IdentityResult>>(() => sut.CreateAsync(null));
//assert
Assert.That(identityResult, Throws.ArgumentNullException);
}
[Test]
public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync()
{
//arrange
MembersUserManager sut = CreateSut();
MembersIdentityUser fakeUser = new MembersIdentityUser()
{
PasswordConfig = "testConfig"
};
CancellationToken fakeCancellationToken = new CancellationToken() { };
_mockMemberStore.Setup(x =>
x.CreateAsync(fakeUser, fakeCancellationToken))
.ReturnsAsync(IdentityResult.Success);
//act
IdentityResult identityResult = await sut.CreateAsync(fakeUser);
//assert
Assert.IsTrue(identityResult.Succeeded);
Assert.IsTrue(!identityResult.Errors.Any());
}
}
}
@@ -0,0 +1,81 @@
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.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Infrastructure.Security;
using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberIdentityUserStoreTests
{
private Mock<IMemberService> _mockMemberService;
public MembersUserStore CreateSut()
{
_mockMemberService = new Mock<IMemberService>();
return new MembersUserStore(
_mockMemberService.Object,
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())),
new Mock<IScopeProvider>().Object,
new IdentityErrorDescriber());
}
[Test]
public void GivenICreateUser_AndTheUserIsNull_ThenIShouldGetAFailedResultAsync()
{
// arrange
MembersUserStore sut = CreateSut();
CancellationToken fakeCancellationToken = new CancellationToken(){};
// act
Action actual = () => sut.CreateAsync(null, fakeCancellationToken);
// assert
Assert.That(actual, Throws.ArgumentNullException);
}
[Test]
public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync()
{
// arrange
MembersUserStore sut = CreateSut();
var fakeUser = new MembersIdentityUser() { };
var fakeCancellationToken = new CancellationToken() { };
IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77);
IMember mockMember = Mock.Of<IMember>(m =>
m.Name == "fakeName" &&
m.Email == "fakeemail@umbraco.com" &&
m.Username == "fakeUsername" &&
m.RawPasswordValue == "fakePassword" &&
m.ContentTypeAlias == fakeMemberType.Alias &&
m.HasIdentity == true);
bool raiseEvents = false;
_mockMemberService.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(mockMember);
_mockMemberService.Setup(x => x.Save(mockMember, raiseEvents));
// act
IdentityResult identityResult = await sut.CreateAsync(fakeUser, fakeCancellationToken);
// assert
Assert.IsTrue(identityResult.Succeeded);
Assert.IsTrue(!identityResult.Errors.Any());
}
//GetPasswordHashAsync
//GetUserIdAsync
}
}
@@ -3,19 +3,17 @@
using System;
using NUnit.Framework;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
namespace Umbraco.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.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.Tests.UnitTests.Umbraco.Core.BackOffice
[TestCase(" ")]
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
{
var sut = new BackOfficeLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
Assert.AreEqual(email, normalizedEmail);
}
[Test]
public void NormalizeEmail_Expect_Input_Returned()
{
var email = $"{Guid.NewGuid()}@umbraco";
var sut = new BackOfficeLookupNormalizer();
var sut = new NoOpLookupNormalizer();
var normalizedEmail = sut.NormalizeEmail(email);
@@ -0,0 +1,564 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Common;
using AutoFixture.NUnit3;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Events;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Infrastructure.Security;
using Umbraco.Tests.Common.Builders;
using Umbraco.Tests.UnitTests.AutoFixture;
using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
using Umbraco.Web;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Mapping;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Mapping;
using Umbraco.Web.PropertyEditors;
using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
{
[TestFixture]
public class MemberControllerUnitTests
{
private UmbracoMapper _mapper;
[Test]
[AutoMoqData]
public void PostSaveMember_WhenMemberIsNull_ExpectFailureResponse(
MemberController sut)
{
// arrange
// act
ArgumentNullException exception = Assert.ThrowsAsync<ArgumentNullException>(() => sut.PostSave(null));
// assert
Assert.That(exception.Message, Is.EqualTo("Value cannot be null. (Parameter 'The member content item was null')"));
}
[Test]
[AutoMoqData]
public void PostSaveMember_WhenModelStateIsNotValid_ExpectFailureResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
sut.ModelState.AddModelError("key", "Invalid model state");
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
var value = new MemberDisplay();
string reason = "Validation failed";
// act
ActionResult<MemberDisplay> result = sut.PostSave(fakeMemberData).Result;
var validation = result.Result as ValidationErrorResult;
// assert
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Value);
Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveNew_NoCustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveNew_CustomField_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>(), It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveExisting_WhenAllIsSetupCorrectly_ExpectSuccessResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(() => new MembersIdentityUser());
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
[Test]
[AutoMoqData]
public void PostSaveMember_SaveNew_WhenMemberEmailAlreadyExists_ExpectFailResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
Member member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.SaveNew);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.CreateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
string reason = "Validation failed";
// act
ActionResult<MemberDisplay> result = sut.PostSave(fakeMemberData).Result;
var validation = result.Result as ValidationErrorResult;
// assert
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Value);
Assert.AreEqual(StatusCodes.Status400BadRequest, validation?.StatusCode);
}
[Test]
[AutoMoqData]
public async Task PostSaveMember_SaveExistingMember_WithNoRoles_Add1Role_ExpectSuccessResponse(
[Frozen] IMembersUserManager umbracoMembersUserManager,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IBackOfficeSecurity backOfficeSecurity)
{
// arrange
string password = "fakepassword9aw89rnyco3938cyr^%&*()i8Y";
var roleName = "anyrole";
IMember member = SetupMemberTestData(out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction.Save);
fakeMemberData.Groups = new List<string>()
{
roleName
};
var membersIdentityUser = new MembersIdentityUser();
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
.ReturnsAsync(() => membersIdentityUser);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.ValidatePasswordAsync(It.IsAny<string>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.HashPassword(It.IsAny<string>()))
.Returns(password);
Mock.Get(umbracoMembersUserManager)
.Setup(x => x.UpdateAsync(It.IsAny<MembersIdentityUser>()))
.ReturnsAsync(() => IdentityResult.Success);
Mock.Get(memberTypeService).Setup(x => x.GetDefault()).Returns("fakeAlias");
Mock.Get(backOfficeSecurityAccessor).Setup(x => x.BackOfficeSecurity).Returns(backOfficeSecurity);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
Mock.Get(memberService).SetupSequence(
x => x.GetByEmail(It.IsAny<string>()))
.Returns(() => null)
.Returns(() => member);
Mock.Get(memberService).Setup(x => x.GetByUsername(It.IsAny<string>())).Returns(() => member);
MemberController sut = CreateSut(memberService, memberTypeService, memberGroupService, umbracoMembersUserManager, dataTypeService, backOfficeSecurityAccessor);
// act
ActionResult<MemberDisplay> result = await sut.PostSave(fakeMemberData);
// assert
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Value);
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.GetRolesAsync(membersIdentityUser));
Mock.Get(umbracoMembersUserManager)
.Verify(u => u.AddToRolesAsync(membersIdentityUser, new[] { roleName }));
Mock.Get(memberService)
.Verify(m => m.Save(It.IsAny<Member>(), true));
AssertMemberDisplayPropertiesAreEqual(memberDisplay, result.Value);
}
/// <summary>
/// Create member controller to test
/// </summary>
/// <param name="memberService">Member service</param>
/// <param name="memberTypeService">Member type service</param>
/// <param name="memberGroupService">Member group service</param>
/// <param name="membersUserManager">Members user manager</param>
/// <param name="dataTypeService">Data type service</param>
/// <param name="backOfficeSecurityAccessor">Back office security accessor</param>
/// <returns>A member controller for the tests</returns>
private MemberController CreateSut(
IMemberService memberService,
IMemberTypeService memberTypeService,
IMemberGroupService memberGroupService,
IMembersUserManager membersUserManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
var mockShortStringHelper = new MockShortStringHelper();
var textService = new Mock<ILocalizedTextService>();
var contentTypeBaseServiceProvider = new Mock<IContentTypeBaseServiceProvider>();
contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny<IContentBase>())).Returns(new ContentType(mockShortStringHelper, 123));
var contentAppFactories = new Mock<List<IContentAppFactory>>();
var mockContentAppFactoryCollection = new Mock<ILogger<ContentAppFactoryCollection>>();
var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache());
var contentAppFactoryCollection = new ContentAppFactoryCollection(
contentAppFactories.Object,
mockContentAppFactoryCollection.Object,
hybridBackOfficeSecurityAccessor);
var mockUserService = new Mock<IUserService>();
var commonMapper = new CommonMapper(
mockUserService.Object,
contentTypeBaseServiceProvider.Object,
contentAppFactoryCollection,
textService.Object);
var mockCultureDictionary = new Mock<ICultureDictionary>();
var mockPasswordConfig = new Mock<IOptions<MemberPasswordConfigurationSettings>>();
mockPasswordConfig.Setup(x => x.Value).Returns(() => new MemberPasswordConfigurationSettings());
IDataEditor dataEditor = Mock.Of<IDataEditor>(
x => x.Type == EditorType.PropertyValue
&& x.Alias == Constants.PropertyEditors.Aliases.Label);
Mock.Get(dataEditor).Setup(x => x.GetValueEditor()).Returns(new TextOnlyValueEditor(Mock.Of<IDataTypeService>(), Mock.Of<ILocalizationService>(), new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox, "Test Textbox", "textbox"), textService.Object, Mock.Of<IShortStringHelper>(), Mock.Of<IJsonSerializer>()));
var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
IMapDefinition memberMapDefinition = new MemberMapDefinition(
commonMapper,
new CommonTreeNodeMapper(Mock.Of<LinkGenerator>()),
new MemberTabsAndPropertiesMapper(
mockCultureDictionary.Object,
backOfficeSecurityAccessor,
textService.Object,
memberTypeService,
memberService,
memberGroupService,
mockPasswordConfig.Object,
contentTypeBaseServiceProvider.Object,
propertyEditorCollection),
new HttpContextAccessor());
var map = new MapDefinitionCollection(new List<IMapDefinition>()
{
new global::Umbraco.Core.Models.Mapping.MemberMapDefinition(),
memberMapDefinition,
new ContentTypeMapDefinition(
commonMapper,
propertyEditorCollection,
dataTypeService,
new Mock<IFileService>().Object,
new Mock<IContentTypeService>().Object,
new Mock<IMediaTypeService>().Object,
memberTypeService,
new Mock<ILoggerFactory>().Object,
mockShortStringHelper,
new Mock<IOptions<GlobalSettings>>().Object,
new Mock<IHostingEnvironment>().Object)
});
_mapper = new UmbracoMapper(map);
return new MemberController(
new DefaultCultureDictionary(
new Mock<ILocalizationService>().Object,
new HttpRequestAppCache(() => null)),
new LoggerFactory(),
mockShortStringHelper,
new DefaultEventMessagesFactory(
new Mock<IEventMessagesAccessor>().Object),
textService.Object,
propertyEditorCollection,
_mapper,
memberService,
memberTypeService,
membersUserManager,
dataTypeService,
backOfficeSecurityAccessor,
new ConfigurationEditorJsonSerializer());
}
/// <summary>
/// Setup all standard member data for test
/// </summary>
private Member SetupMemberTestData(
out MemberSave fakeMemberData,
out MemberDisplay memberDisplay,
ContentSaveAction contentAction)
{
// arrange
MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "test@example.com", "123", "test");
int memberId = 123;
member.Id = memberId;
//TODO: replace with builder for MemberSave and MemberDisplay
fakeMemberData = new MemberSave()
{
Id = memberId,
SortOrder = member.SortOrder,
ContentTypeId = memberType.Id,
Key = member.Key,
Password = new ChangingPasswordModel()
{
Id = 456,
NewPassword = member.RawPasswordValue,
OldPassword = null
},
Name = member.Name,
Email = member.Email,
Username = member.Username,
PersistedContent = member,
PropertyCollectionDto = new ContentPropertyCollectionDto()
{
},
Groups = new List<string>(),
//Alias = "fakeAlias",
ContentTypeAlias = member.ContentTypeAlias,
Action = contentAction,
Icon = "icon-document",
Path = member.Path
};
memberDisplay = new MemberDisplay()
{
Id = memberId,
SortOrder = member.SortOrder,
ContentTypeId = memberType.Id,
Key = member.Key,
Name = member.Name,
Email = member.Email,
Username = member.Username,
//Alias = "fakeAlias",
ContentTypeAlias = member.ContentTypeAlias,
ContentType = new ContentTypeBasic(),
ContentTypeName = member.ContentType.Name,
Icon = fakeMemberData.Icon,
Path = member.Path,
Tabs = new List<Tab<ContentPropertyDisplay>>()
{
new Tab<ContentPropertyDisplay>()
{
Alias = "test",
Id = 77,
Properties = new List<ContentPropertyDisplay>()
{
new ContentPropertyDisplay()
{
Alias = "_umb_id",
View = "idwithguid",
Value = new []
{
"123",
"guid"
}
},
new ContentPropertyDisplay()
{
Alias = "_umb_doctype"
},
new ContentPropertyDisplay()
{
Alias = "_umb_login"
},
new ContentPropertyDisplay()
{
Alias= "_umb_email"
},
new ContentPropertyDisplay()
{
Alias = "_umb_password"
},
new ContentPropertyDisplay()
{
Alias = "_umb_membergroup"
}
}
}
}
};
return member;
}
/// <summary>
/// Check all member properties are equal
/// </summary>
/// <param name="memberDisplay"></param>
/// <param name="resultValue"></param>
private void AssertMemberDisplayPropertiesAreEqual(MemberDisplay memberDisplay, MemberDisplay resultValue)
{
Assert.AreNotSame(memberDisplay, resultValue);
Assert.AreEqual(memberDisplay.Id, resultValue.Id);
Assert.AreEqual(memberDisplay.Alias, resultValue.Alias);
Assert.AreEqual(memberDisplay.Username, resultValue.Username);
Assert.AreEqual(memberDisplay.Email, resultValue.Email);
Assert.AreEqual(memberDisplay.AdditionalData, resultValue.AdditionalData);
Assert.AreEqual(memberDisplay.ContentApps, resultValue.ContentApps);
Assert.AreEqual(memberDisplay.ContentType.Alias, resultValue.ContentType.Alias);
Assert.AreEqual(memberDisplay.ContentTypeAlias, resultValue.ContentTypeAlias);
Assert.AreEqual(memberDisplay.ContentTypeName, resultValue.ContentTypeName);
Assert.AreEqual(memberDisplay.ContentTypeId, resultValue.ContentTypeId);
Assert.AreEqual(memberDisplay.Icon, resultValue.Icon);
Assert.AreEqual(memberDisplay.Errors, resultValue.Errors);
Assert.AreEqual(memberDisplay.Key, resultValue.Key);
Assert.AreEqual(memberDisplay.Name, resultValue.Name);
Assert.AreEqual(memberDisplay.Path, resultValue.Path);
Assert.AreEqual(memberDisplay.SortOrder, resultValue.SortOrder);
Assert.AreEqual(memberDisplay.Trashed, resultValue.Trashed);
Assert.AreEqual(memberDisplay.TreeNodeUrl, resultValue.TreeNodeUrl);
//TODO: can we check create/update dates when saving?
//Assert.AreEqual(memberDisplay.CreateDate, resultValue.CreateDate);
//Assert.AreEqual(memberDisplay.UpdateDate, resultValue.UpdateDate);
//TODO: check all properties
Assert.AreEqual(memberDisplay.Properties.Count(), resultValue.Properties.Count());
Assert.AreNotSame(memberDisplay.Properties, resultValue.Properties);
for (var index = 0; index < resultValue.Properties.Count(); index++)
{
Assert.AreNotSame(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index));
//Assert.AreEqual(memberDisplay.Properties.GetItemByIndex(index), resultValue.Properties.GetItemByIndex(index));
}
}
}
}
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Tests.UnitTests.AutoFixture;
using Umbraco.Web.BackOffice.Controllers;
@@ -20,6 +20,7 @@ using Umbraco.Core.Models.Security;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.Security;
@@ -24,6 +24,7 @@ using Umbraco.Core.Serialization;
using Umbraco.Core.Services;
using Umbraco.Core.WebAssets;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.BackOffice.ActionResults;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.Security;
@@ -70,10 +70,18 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </summary>
protected ILocalizedTextService LocalizedTextService { get; }
/// <summary>
/// Handles if the content for the specified ID isn't found
/// </summary>
/// <param name="id">The content ID to find</param>
/// <param name="throwException">Whether to throw an exception</param>
/// <returns>The error response</returns>
protected NotFoundObjectResult HandleContentNotFound(object id)
{
ModelState.AddModelError("id", $"content with id: {id} was not found");
var errorResponse = NotFound(ModelState);
NotFoundObjectResult errorResponse = NotFound(ModelState);
return errorResponse;
}
@@ -90,7 +98,7 @@ namespace Umbraco.Web.BackOffice.Controllers
where TSaved : IContentSave<TPersisted>
{
// map the property values
foreach (var propertyDto in dto.Properties)
foreach (ContentPropertyDto propertyDto in dto.Properties)
{
// get the property editor
if (propertyDto.PropertyEditor == null)
@@ -101,42 +109,53 @@ namespace Umbraco.Web.BackOffice.Controllers
// get the value editor
// nothing to save/map if it is readonly
var valueEditor = propertyDto.PropertyEditor.GetValueEditor();
if (valueEditor.IsReadOnly) continue;
IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor();
if (valueEditor.IsReadOnly)
{
continue;
}
// get the property
var property = contentItem.PersistedContent.Properties[propertyDto.Alias];
IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias];
// prepare files, if any matching property and culture
var files = contentItem.UploadedFiles
ContentPropertyFile[] files = contentItem.UploadedFiles
.Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment)
.ToArray();
foreach (var file in files)
foreach (ContentPropertyFile file in files)
{
file.FileName = file.FileName.ToSafeFileName(ShortStringHelper);
}
// create the property data for the property editor
var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration)
{
ContentKey = contentItem.PersistedContent.Key,
PropertyTypeKey = property.PropertyType.Key,
Files = files
Files = files
};
// let the editor convert the value that was received, deal with files, etc
var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
// set the value - tags are special
var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
if (tagAttribute != null)
{
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
if (tagConfiguration.Delimiter == default)
{
tagConfiguration.Delimiter = tagAttribute.Delimiter;
}
var tagCulture = property.PropertyType.VariesByCulture() ? culture : null;
property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture);
}
else
{
savePropertyValue(contentItem, property, value);
}
}
}
@@ -153,38 +172,45 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </remarks>
protected TPersisted GetObjectFromRequest<TPersisted>(Func<TPersisted> getFromService)
{
//checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
// checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
// it from the callback
return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null
? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()]
? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()]
: getFromService();
}
/// <summary>
/// Returns true if the action passed in means we need to create something new
/// </summary>
/// <param name="action"></param>
/// <returns></returns>
internal static bool IsCreatingAction(ContentSaveAction action)
{
return (action.ToString().EndsWith("New"));
}
/// <param name="action">The content action</param>
/// <returns>Returns true if this is a creating action</returns>
internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New");
protected void AddCancelMessage(INotificationModel display,
string header = "speechBubbles/operationCancelledHeader",
string message = "speechBubbles/operationCancelledText",
bool localizeHeader = true,
/// <summary>
/// Adds a cancelled message to the display
/// </summary>
/// <param name="display"></param>
/// <param name="header"></param>
/// <param name="message"></param>
/// <param name="localizeHeader"></param>
/// <param name="localizeMessage"></param>
/// <param name="headerParams"></param>
/// <param name="messageParams"></param>
protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true,
bool localizeMessage = true,
string[] headerParams = null,
string[] messageParams = null)
{
//if there's already a default event message, don't add our default one
var msgs = EventMessages;
if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return;
// if there's already a default event message, don't add our default one
IEventMessagesFactory messages = EventMessages;
if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage))
{
return;
}
display.AddWarningNotification(
localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header,
localizeMessage ? LocalizedTextService.Localize(message, messageParams): message);
localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message);
}
}
}
@@ -21,6 +21,7 @@ using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionsResults;
@@ -8,23 +8,24 @@ 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.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Events;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Infrastructure.Services.Implement;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.ModelBinders;
using Umbraco.Web.Common.ActionsResults;
@@ -45,46 +46,72 @@ namespace Umbraco.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 IMembersUserManager _memberManager;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IJsonSerializer _jsonSerializer;
private readonly IShortStringHelper _shortStringHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MemberController"/> class.
/// </summary>
/// <param name="cultureDictionary">The culture dictionary</param>
/// <param name="loggerFactory">The logger factory</param>
/// <param name="shortStringHelper">The string helper</param>
/// <param name="eventMessages">The event messages factory</param>
/// <param name="localizedTextService">The entry point for localizing key services</param>
/// <param name="propertyEditors">The property editors</param>
/// <param name="umbracoMapper">The mapper</param>
/// <param name="memberService">The member service</param>
/// <param name="memberTypeService">The member type service</param>
/// <param name="memberManager">The member manager</param>
/// <param name="dataTypeService">The data-type service</param>
/// <param name="backOfficeSecurityAccessor">The back office security accessor</param>
/// <param name="jsonSerializer">The JSON serializer</param>
public MemberController(
ICultureDictionary cultureDictionary,
ILoggerFactory loggerFactory,
IShortStringHelper shortStringHelper,
IEventMessagesFactory eventMessages,
ILocalizedTextService localizedTextService,
IOptions<MemberPasswordConfigurationSettings> passwordConfig,
PropertyEditorCollection propertyEditors,
LegacyPasswordSecurity passwordSecurity,
UmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
IMembersUserManager memberManager,
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IJsonSerializer jsonSerializer)
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
{
_passwordConfig = passwordConfig.Value;
_propertyEditors = propertyEditors;
_passwordSecurity = passwordSecurity;
_umbracoMapper = umbracoMapper;
_memberService = memberService;
_memberTypeService = memberTypeService;
_memberManager = memberManager;
_dataTypeService = dataTypeService;
_localizedTextService = localizedTextService;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_jsonSerializer = jsonSerializer;
_shortStringHelper = shortStringHelper;
}
/// <summary>
/// The paginated list of members
/// </summary>
/// <param name="pageNumber">The page number to display</param>
/// <param name="pageSize">The size of the page</param>
/// <param name="orderBy">The ordering of the member list</param>
/// <param name="orderDirection">The direction of the member list</param>
/// <param name="orderBySystemField">The system field to order by</param>
/// <param name="filter">The current filter for the list</param>
/// <param name="memberTypeAlias">The member type</param>
/// <returns>The paged result of members</returns>
public PagedResult<MemberBasic> GetPagedResults(
int pageNumber = 1,
int pageSize = 100,
@@ -100,8 +127,15 @@ namespace Umbraco.Web.BackOffice.Controllers
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
}
var members = _memberService
.GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray();
IMember[] members = _memberService.GetAll(
pageNumber - 1,
pageSize,
out var totalRecords,
orderBy,
orderDirection,
orderBySystemField,
memberTypeAlias,
filter).ToArray();
if (totalRecords == 0)
{
return new PagedResult<MemberBasic>(0, 0, 0);
@@ -109,8 +143,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var pagedResult = new PagedResult<MemberBasic>(totalRecords, pageNumber, pageSize)
{
Items = members
.Select(x => _umbracoMapper.Map<MemberBasic>(x))
Items = members.Select(x => _umbracoMapper.Map<MemberBasic>(x))
};
return pagedResult;
}
@@ -118,15 +151,22 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Returns a display node with a list view to render members
/// </summary>
/// <param name="listName"></param>
/// <returns></returns>
/// <param name="listName">The member type to list</param>
/// <returns>The member list for display</returns>
public MemberListDisplay GetListNodeDisplay(string listName)
{
var foundType = _memberTypeService.Get(listName);
IMemberType foundType = _memberTypeService.Get(listName);
var name = foundType != null ? foundType.Name : listName;
var apps = new List<ContentApp>();
apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView));
var apps = new List<ContentApp>
{
ListViewContentAppFactory.CreateContentApp(
_dataTypeService,
_propertyEditors,
listName,
Constants.Security.DefaultMemberTypeAlias.ToLower(),
Constants.DataTypes.DefaultMembersListView)
};
apps[0].Active = true;
var display = new MemberListDisplay
@@ -147,138 +187,120 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Gets the content json for the member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <param name="key">The Guid key of the member</param>
/// <returns>The member for display</returns>
[OutgoingEditorModelEvent]
public MemberDisplay GetByKey(Guid key)
{
var foundMember = _memberService.GetByKey(key);
IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
HandleContentNotFound(key);
}
return _umbracoMapper.Map<MemberDisplay>(foundMember);
}
/// <summary>
/// Gets an empty content item for the
/// </summary>
/// <param name="contentTypeAlias"></param>
/// <returns></returns>
/// <param name="contentTypeAlias">The content type</param>
/// <returns>The empty member for display</returns>
[OutgoingEditorModelEvent]
public ActionResult<MemberDisplay> GetEmpty(string contentTypeAlias = null)
{
IMember emptyContent;
if (contentTypeAlias == null)
{
return NotFound();
}
var contentType = _memberTypeService.Get(contentTypeAlias);
IMemberType contentType = _memberTypeService.Get(contentTypeAlias);
if (contentType == null)
{
return NotFound();
}
var passwordGenerator = new PasswordGenerator(_passwordConfig);
string newPassword = _memberManager.GeneratePassword();
emptyContent = new Member(contentType);
emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword();
IMember emptyContent = new Member(contentType);
emptyContent.AdditionalData["NewPassword"] = newPassword;
return _umbracoMapper.Map<MemberDisplay>(emptyContent);
}
/// <summary>
/// Saves member
/// </summary>
/// <returns></returns>
/// <param name="contentItem">The content item to save as a member</param>
/// <returns>The resulting member display object</returns>
[FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
[MemberSaveValidation]
public async Task<ActionResult<MemberDisplay>> PostSave(
[ModelBinder(typeof(MemberBinder))]
MemberSave contentItem)
public async Task<ActionResult<MemberDisplay>> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
{
if (contentItem == null)
{
throw new ArgumentNullException("The member content item was null");
}
//If we've reached here it means:
// If we've reached here it means:
// * Our model has been bound
// * and validated
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
//map the properties to the persisted entity
// map the properties to the persisted entity
MapPropertyValues(contentItem);
await ValidateMemberDataAsync(contentItem);
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
// Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
if (ModelState.IsValid == false)
{
var forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
MemberDisplay forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
forDisplay.Errors = ModelState.ToErrorDictionary();
return new ValidationErrorResult(forDisplay);
}
//We're gonna look up the current roles now because the below code can cause
// events to be raised and developers could be manually adding roles to members in
// their handlers. If we don't look this up now there's a chance we'll just end up
// removing the roles they've assigned.
var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
//find the ones to remove and remove them
var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray();
//Depending on the action we need to first do a create or update using the membership provider
// Depending on the action we need to first do a create or update using the membership manager
// this ensures that passwords are formatted correctly and also performs the validation on the provider itself.
switch (contentItem.Action)
{
case ContentSaveAction.Save:
UpdateMemberData(contentItem);
Task<ActionResult<bool>> updateSuccessful = UpdateMemberAsync(contentItem);
break;
case ContentSaveAction.SaveNew:
contentItem.PersistedContent = CreateMemberData(contentItem);
Task<ActionResult<bool>> createSuccessful = CreateMemberAsync(contentItem);
break;
default:
//we don't support anything else for members
// we don't support anything else for members
return NotFound();
}
//TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope
// TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope
// but it would be nicer to have this taken care of within the Save method itself
//create/save the IMember
_memberService.Save(contentItem.PersistedContent);
// return the updated model
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
//Now let's do the role provider stuff - now that we've saved the content item (that is important since
// if we are changing the username, it must be persisted before looking up the member roles).
if (rolesToRemove.Any())
{
_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove);
}
//find the ones to add and add them
var toAdd = contentItem.Groups.Except(currRoles).ToArray();
if (toAdd.Any())
{
//add the ones submitted
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
}
//return the updated model
var display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
if (!ModelState.IsValid)
{
display.Errors = ModelState.ToErrorDictionary();
return new ValidationErrorResult(display, StatusCodes.Status403Forbidden);
}
var localizedTextService = _localizedTextService;
//put the correct messages in
ILocalizedTextService localizedTextService = _localizedTextService;
// put the correct messages in
switch (contentItem.Action)
{
case ContentSaveAction.Save:
case ContentSaveAction.SaveNew:
display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved"));
display.AddSuccessNotification(
localizedTextService.Localize("speechBubbles/editMemberSaved"),
localizedTextService.Localize("speechBubbles/editMemberSaved"));
break;
}
@@ -288,81 +310,121 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <summary>
/// Maps the property values to the persisted entity
/// </summary>
/// <param name="contentItem"></param>
/// <param name="contentItem">The member content item to map properties from</param>
private void MapPropertyValues(MemberSave contentItem)
{
UpdateName(contentItem);
// Don't update the name if it is empty
if (contentItem.Name.IsNullOrWhiteSpace() == false)
{
contentItem.PersistedContent.Name = contentItem.Name;
}
//map the custom properties - this will already be set for new entities in our member binder
// map the custom properties - this will already be set for new entities in our member binder
contentItem.PersistedContent.Email = contentItem.Email;
contentItem.PersistedContent.Username = contentItem.Username;
//use the base method to map the rest of the properties
base.MapPropertyValuesForPersistence<IMember, MemberSave>(
// use the base method to map the rest of the properties
MapPropertyValuesForPersistence<IMember, MemberSave>(
contentItem,
contentItem.PropertyCollectionDto,
(save, property) => property.GetValue(), //get prop val
(save, property, v) => property.SetValue(v), //set prop val
(save, property) => property.GetValue(), // get prop val
(save, property, v) => property.SetValue(v), // set prop val
null); // member are all invariant
}
private IMember CreateMemberData(MemberSave contentItem)
/// <summary>
/// Create a member from the supplied member content data
///
/// All member password processing and creation is done via the identity manager
/// </summary>
/// <param name="contentItem">Member content data</param>
/// <returns>The identity result of the created member</returns>
private async Task<ActionResult<bool>> CreateMemberAsync(MemberSave contentItem)
{
throw new NotImplementedException("Members have not been migrated to netcore");
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
if (memberType == null)
{
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
}
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
var identityMember = MembersIdentityUser.CreateNew(
contentItem.Username,
contentItem.Email,
memberType.Alias,
contentItem.Name);
//var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
//if (memberType == null)
// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
//var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
//{
// CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
// Comments = contentItem.Comments,
// IsApproved = contentItem.IsApproved
//};
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
//return member;
if (created.Succeeded == false)
{
return new ValidationErrorResult(created.Errors.ToErrorMessage());
}
// now re-look up the member, which will now exist
IMember member = _memberService.GetByEmail(contentItem.Email);
// map the save info over onto the user
member = _umbracoMapper.Map<MemberSave, IMember>(contentItem, member);
int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
member.CreatorId = creatorId;
// assign the mapped property values that are not part of the identity properties
string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
foreach (ContentPropertyBasic property in contentItem.Properties)
{
if (builtInAliases.Contains(property.Alias) == false)
{
member.Properties[property.Alias].SetValue(property.Value);
}
}
//TODO: do we need to resave the key?
//contentItem.PersistedContent.Key = contentItem.Key;
// now the member has been saved via identity, resave the member with mapped content properties
_memberService.Save(member);
contentItem.PersistedContent = member;
await AddOrUpdateRoles(contentItem, identityMember);
return true;
}
/// <summary>
/// Update the member security data
/// </summary>
/// <param name="contentItem"></param>
/// <returns>
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
/// </returns>
private void UpdateMemberData(MemberSave contentItem)
/// </summary>
/// <param name="contentItem">The member to save</param>
private async Task<ActionResult<bool>> UpdateMemberAsync(MemberSave contentItem)
{
contentItem.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
// If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
// have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
// There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut
// but we will take care of this in a generic way below so that it works for all props.
if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
{
var memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId);
IMemberType memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId);
var sensitiveProperties = memberType
.PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias))
.ToList();
foreach (var sensitiveProperty in sensitiveProperties)
foreach (IPropertyType sensitiveProperty in sensitiveProperties)
{
var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
ContentPropertyBasic destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
if (destProp != null)
{
//if found, change the value of the contentItem model to the persisted value so it remains unchanged
var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
// if found, change the value of the contentItem model to the persisted value so it remains unchanged
object origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
destProp.Value = origValue;
}
}
}
var isLockedOut = contentItem.IsLockedOut;
bool isLockedOut = contentItem.IsLockedOut;
//if they were locked but now they are trying to be unlocked
// if they were locked but now they are trying to be unlocked
if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false)
{
contentItem.PersistedContent.IsLockedOut = false;
@@ -370,90 +432,153 @@ namespace Umbraco.Web.BackOffice.Controllers
}
else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut)
{
//NOTE: This should not ever happen unless someone is mucking around with the request data.
//An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
// NOTE: This should not ever happen unless someone is mucking around with the request data.
// An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
ModelState.AddModelError("custom", "An admin cannot lock a user");
}
//no password changes then exit ?
if (contentItem.Password == null)
return;
throw new NotImplementedException("Members have not been migrated to netcore");
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
// set the password
//contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword);
}
private static void UpdateName(MemberSave memberSave)
{
//Don't update the name if it is empty
if (memberSave.Name.IsNullOrWhiteSpace() == false)
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
if (identityMember == null)
{
memberSave.PersistedContent.Name = memberSave.Name;
return new ValidationErrorResult("Member was not found");
}
if (contentItem.Password != null)
{
IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
if (validatePassword.Succeeded == false)
{
return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage());
}
string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword);
identityMember.PasswordHash = newPassword;
contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash;
if (identityMember.LastPasswordChangeDateUtc != null)
{
contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow;
identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate;
}
}
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);
if (updatedResult.Succeeded == false)
{
return new ValidationErrorResult(updatedResult.Errors.ToErrorMessage());
}
_memberService.Save(contentItem.PersistedContent);
await AddOrUpdateRoles(contentItem, identityMember);
return true;
}
// TODO: This logic should be pulled into the service layer
private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
{
if (contentItem.Name.IsNullOrWhiteSpace())
{
ModelState.AddPropertyError(
new ValidationResult("Invalid user name", new[] { "value" }),
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Invalid user name", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
return false;
}
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
{
//TODO implement when NETCORE members are implemented
throw new NotImplementedException("TODO implement when members are implemented");
// var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword);
// if (!validPassword)
// {
// ModelState.AddPropertyError(
// new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }),
// string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
// return false;
// }
IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
if (!validPassword.Succeeded)
{
ModelState.AddPropertyError(
new ValidationResult("Invalid password: " + MapErrors(validPassword.Errors), new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
return false;
}
}
var byUsername = _memberService.GetByUsername(contentItem.Username);
IMember byUsername = _memberService.GetByUsername(contentItem.Username);
if (byUsername != null && byUsername.Key != contentItem.Key)
{
ModelState.AddPropertyError(
new ValidationResult("Username is already in use", new[] { "value" }),
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Username is already in use", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
return false;
}
var byEmail = _memberService.GetByEmail(contentItem.Email);
IMember byEmail = _memberService.GetByEmail(contentItem.Email);
if (byEmail != null && byEmail.Key != contentItem.Key)
{
ModelState.AddPropertyError(
new ValidationResult("Email address is already in use", new[] { "value" }),
string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
new ValidationResult("Email address is already in use", new[] { "value" }),
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
return false;
}
return true;
}
private string MapErrors(IEnumerable<IdentityError> result)
{
var sb = new StringBuilder();
IEnumerable<IdentityError> identityErrors = result.ToList();
foreach (IdentityError error in identityErrors)
{
string errorString = $"{error.Description}";
sb.AppendLine(errorString);
}
return sb.ToString();
}
/// <summary>
/// Add or update the identity roles
/// </summary>
/// <param name="contentItem">The member content item</param>
/// <param name="identityMember">The member as an identity user</param>
private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember)
{
// We're gonna look up the current roles now because the below code can cause
// events to be raised and developers could be manually adding roles to members in
// their handlers. If we don't look this up now there's a chance we'll just end up
// removing the roles they've assigned.
IEnumerable<string> currentRoles = await _memberManager.GetRolesAsync(identityMember);
// find the ones to remove and remove them
IEnumerable<string> roles = currentRoles.ToList();
string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray();
// Now let's do the role provider stuff - now that we've saved the content item (that is important since
// if we are changing the username, it must be persisted before looking up the member roles).
if (rolesToRemove.Any())
{
IdentityResult rolesIdentityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove);
}
// find the ones to add and add them
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
if (toAdd.Any())
{
// add the ones submitted
IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd);
}
}
/// <summary>
/// Permanently deletes a member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <param name="key">Guid of the member to delete</param>
/// <returns>The result of the deletion</returns>
///
[HttpPost]
public IActionResult DeleteByKey(Guid key)
{
var foundMember = _memberService.GetByKey(key);
//TODO: move to MembersUserStore
IMember foundMember = _memberService.GetByKey(key);
if (foundMember == null)
{
return HandleContentNotFound(key);
}
_memberService.Delete(foundMember);
return Ok();
@@ -467,25 +592,27 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public IActionResult ExportMemberData(Guid key)
{
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
if (currentUser.HasAccessToSensitiveData() == false)
{
return Forbid();
}
var member = ((MemberService)_memberService).ExportMember(key);
if (member is null) throw new NullReferenceException("No member found with key " + key);
MemberExportModel member = ((MemberService)_memberService).ExportMember(key);
if (member is null)
{
throw new NullReferenceException("No member found with key " + key);
}
var json = _jsonSerializer.Serialize(member);
var fileName = $"{member.Name}_{member.Email}.txt";
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
HttpContext.Response.Headers.Add("x-filename", fileName);
return File( Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
return File(Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
}
}
}
@@ -29,6 +29,7 @@ using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.BackOffice.ActionResults;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.ModelBinders;
@@ -6,6 +6,7 @@ using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.Actions;
using Umbraco.Web.BackOffice.Authorization;
@@ -37,6 +38,15 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();
}
/// <summary>
/// Adds the services required for using Members Identity
/// </summary>
public static void AddMembersIdentity(this IServiceCollection services) =>
services.BuildMembersIdentity()
.AddDefaultTokenProviders()
.AddUserStore<MembersUserStore>()
.AddMembersUserManager<IMembersUserManager, MembersUserManager>();
private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services)
{
// Borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs#L33
@@ -57,7 +67,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
services.TryAddScoped<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>, UserClaimsPrincipalFactory<BackOfficeIdentityUser>>();
// CUSTOM:
services.TryAddScoped<BackOfficeLookupNormalizer>();
services.TryAddScoped<NoOpLookupNormalizer>();
services.TryAddScoped<BackOfficeIdentityErrorDescriber>();
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
@@ -69,12 +79,21 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
* To validate the container the following registrations are required (dependencies of UserManager<T>)
* Perhaps we shouldn't be registering UserManager<T> at all and only registering/depending the UmbracoBackOffice prefixed types.
*/
services.TryAddScoped<ILookupNormalizer, BackOfficeLookupNormalizer>();
services.TryAddScoped<ILookupNormalizer, NoOpLookupNormalizer>();
services.TryAddScoped<IdentityErrorDescriber, BackOfficeIdentityErrorDescriber>();
return new BackOfficeIdentityBuilder(services);
}
private static MembersIdentityBuilder BuildMembersIdentity(this IServiceCollection services)
{
// Services used by Umbraco members identity
services.TryAddScoped<IUserValidator<MembersIdentityUser>, UserValidator<MembersIdentityUser>>();
services.TryAddScoped<IPasswordValidator<MembersIdentityUser>, PasswordValidator<MembersIdentityUser>>();
services.TryAddScoped<IPasswordHasher<MembersIdentityUser>, PasswordHasher<MembersIdentityUser>>();
return new MembersIdentityBuilder(services);
}
/// <summary>
/// Add authorization handlers and policies
/// </summary>
@@ -39,6 +39,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
.AddBackOfficeCore()
.AddBackOfficeAuthentication()
.AddBackOfficeIdentity()
.AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies()
.AddUmbracoProfiler()
.AddMvcAndRazor()
@@ -94,6 +95,16 @@ namespace Umbraco.Web.BackOffice.DependencyInjection
return builder;
}
/// <summary>
/// Adds Identity support for Umbraco members
/// </summary>
public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder)
{
builder.Services.AddMembersIdentity();
return builder;
}
/// <summary>
/// Adds Umbraco back office authorization policies
/// </summary>
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Extensions
{
@@ -24,6 +25,19 @@ namespace Umbraco.Extensions
return identityBuilder;
}
/// <summary>
/// Adds a <see cref="UserManager{TUser}"/> for the <seealso cref="MembersIdentityUser"/>.
/// </summary>
/// <typeparam name="TInterface">The usermanager interface</typeparam>
/// <typeparam name="TUserManager">The usermanager type</typeparam>
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
public static IdentityBuilder AddMembersUserManager<TInterface, TUserManager>(this IdentityBuilder identityBuilder)
where TUserManager : UserManager<MembersIdentityUser>, TInterface
{
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
return identityBuilder;
}
/// <summary>
/// Adds a <see cref="SignInManager{TUser}"/> implementation for <seealso cref="BackOfficeIdentityUser"/>
/// </summary>
@@ -17,6 +17,7 @@ using Umbraco.Core.Scoping;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.Security;
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
@@ -93,14 +93,11 @@ namespace Umbraco.Web.BackOffice.Mapping
target.Path = $"-1,{source.Id}";
target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key);
}
// Umbraco.Code.MapAll
private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context)
{
target.Properties = context.MapEnumerable<IProperty, ContentPropertyDto>(source.Properties);
}
}
}
@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Security;
using Umbraco.Core;
using Umbraco.Core.Models.Membership;
using Microsoft.Extensions.Options;
using Umbraco.Core.Serialization;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.BackOffice.Security
{
@@ -1,4 +1,4 @@
using System;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Common.Security;
namespace Umbraco.Web.BackOffice.Security
@@ -12,6 +12,7 @@ using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.BackOffice.Security
{
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
@@ -13,6 +13,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.BackOffice.Security;
@@ -6,6 +6,7 @@ using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.Common.Security
{
@@ -6,6 +6,7 @@ using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.BackOffice.Security
{
@@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity;
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
using SecurityConstants = Umbraco.Core.Constants.Security;
namespace Umbraco.Web.BackOffice.Security
@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.Common.Security
{
@@ -6,6 +6,7 @@ using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Web.Models;
using IUser = Umbraco.Core.Models.Membership.IUser;
@@ -55,12 +55,14 @@ namespace Umbraco.Web.BackOffice.Trees
/// <summary>
/// Gets an individual tree node
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
public ActionResult<TreeNode> GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
public ActionResult<TreeNode> GetTreeNode([FromRoute]string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
{
var node = GetSingleTreeNode(id, queryStrings);
ActionResult<TreeNode> node = GetSingleTreeNode(id, queryStrings);
if (!(node.Result is null))
{
return node.Result;
}
//add the tree alias to the node since it is standalone (has no root for which this normally belongs)
node.Value.AdditionalData["treeAlias"] = TreeAlias;
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
@@ -11,6 +11,7 @@ using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.Models.ContentEditing;
@@ -28,13 +29,12 @@ namespace Umbraco.Web.Common.Security
IPasswordHasher<BackOfficeIdentityUser> passwordHasher,
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<BackOfficeIdentityUser>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
}
@@ -138,7 +138,7 @@ namespace Umbraco.Web.Common.Security
return result;
}
/// <inheritdoc/>
public override async Task<IdentityResult> SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd)
{
@@ -0,0 +1,67 @@
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.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Extensions;
using Umbraco.Infrastructure.Security;
using Umbraco.Net;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Common.Security
{
public class MembersUserManager : UmbracoUserManager<MembersIdentityUser, MemberPasswordConfigurationSettings>, IMembersUserManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MembersUserManager(
IIpResolver ipResolver,
IUserStore<MembersIdentityUser> store,
IOptions<MembersIdentityOptions> optionsAccessor,
IPasswordHasher<MembersIdentityUser> passwordHasher,
IEnumerable<IUserValidator<MembersIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<MembersIdentityUser>> passwordValidators,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<MembersIdentityUser>> logger,
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
}
private string GetCurrentUserId(IPrincipal currentUser)
{
UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity();
var currentUserId = umbIdentity?.GetUserId<string>() ?? 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; }
}
}
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.Security
{
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Umbraco.Core.Security;
using Umbraco.Infrastructure.Security;
namespace Umbraco.Web.Security
{
+35 -16
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -11,7 +11,6 @@ using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Models.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Editors;
using Umbraco.Web.Models;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Security.Providers;
@@ -19,6 +18,15 @@ using Umbraco.Web.Security.Providers;
namespace Umbraco.Web.Security
{
// MIGRATED TO NETCORE
// TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation
/// <summary>
/// Helper class containing logic relating to the built-in Umbraco members macros and controllers for:
/// - Registration
/// - Updating
/// - Logging in
/// - Current status
/// </summary>
public class MembershipHelper
{
private readonly MembersMembershipProvider _membershipProvider;
@@ -118,7 +126,7 @@ namespace Umbraco.Web.Security
var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider);
var result = new Dictionary<string, bool>();
foreach(var path in paths)
foreach (var path in paths)
{
pathsWithAccess.TryGetValue(path, out var hasAccess);
// if it's not found it's false anyways
@@ -144,7 +152,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 +194,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 +267,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 +378,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 +714,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 +758,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 +780,10 @@ namespace Umbraco.Web.Security
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
if (passwordModel == null)
throw new ArgumentNullException(nameof(passwordModel));
if (membershipProvider == null)
throw new ArgumentNullException(nameof(membershipProvider));
var userId = -1;
@@ -1,24 +1,22 @@
using System;
using System;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Configuration.Provider;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
using System.Web.Configuration;
using System.Web.Security;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Web.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Security;
namespace Umbraco.Web.Security
{
//TODO: Delete - should not be used
/// <summary>
/// A base membership provider class offering much of the underlying functionality for initializing and password encryption/hashing.
/// </summary>
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
public abstract class MembershipProviderBase : MembershipProvider
{
private readonly IHostingEnvironment _hostingEnvironment;
@@ -1,4 +1,4 @@
using System.Collections.Specialized;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
using Umbraco.Core;
@@ -7,13 +7,14 @@ using Umbraco.Core.Hosting;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Composing;
using System;
using Umbraco.Net;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete: should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
/// <summary>
/// Custom Membership Provider for Umbraco Members (User authentication for Frontend applications NOT umbraco CMS)
/// </summary>
@@ -120,6 +121,7 @@ namespace Umbraco.Web.Security.Providers
public override LegacyPasswordSecurity PasswordSecurity => _passwordSecurity.Value;
public IPasswordConfiguration PasswordConfiguration => _passwordConfig.Value;
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
private class MembershipProviderPasswordConfiguration : IPasswordConfiguration
{
public MembershipProviderPasswordConfiguration(int requiredLength, bool requireNonLetterOrDigit, bool requireDigit, bool requireLowercase, bool requireUppercase, bool useLegacyEncoding, string hashAlgorithmType, int maxFailedAccessAttemptsBeforeLockout)
@@ -1,8 +1,7 @@
using System.Collections.Specialized;
using System;
using System.Configuration.Provider;
using System.Linq;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Services;
@@ -10,9 +9,12 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete: should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
public class MembersRoleProvider : RoleProvider
{
private readonly IMembershipRoleService<IMember> _roleService;
private string _applicationName;
public MembersRoleProvider(IMembershipRoleService<IMember> roleService)
{
@@ -24,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);
@@ -46,10 +46,12 @@ namespace Umbraco.Web.Security.Providers
return _roleService.DeleteRole(roleName, throwOnPopulatedRole);
}
public override bool RoleExists(string roleName)
{
return _roleService.GetAllRoles().Any(x => x == roleName);
}
/// <summary>
/// Returns true if the specified member role name exists
/// </summary>
/// <param name="roleName">Member role name</param>
/// <returns>True if member role exists, otherwise false</returns>
public override bool RoleExists(string roleName) => _roleService.GetAllRoles().Any(x => x.Name == roleName);
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
@@ -66,10 +68,11 @@ namespace Umbraco.Web.Security.Providers
return _roleService.GetMembersInRole(roleName).Select(x => x.Username).ToArray();
}
public override string[] GetAllRoles()
{
return _roleService.GetAllRoles().ToArray();
}
/// <summary>
/// Gets all the member roles
/// </summary>
/// <returns>A list of member roles</returns>
public override string[] GetAllRoles() => _roleService.GetAllRoles().Select(x => x.Name).ToArray();
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
@@ -87,6 +90,7 @@ namespace Umbraco.Web.Security.Providers
{
return _applicationName;
}
set
{
if (string.IsNullOrEmpty(value))
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Linq;
@@ -17,8 +17,8 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Security.Providers
{
//TODO: Delete - should not be used
[Obsolete("We are now using ASP.NET Core Identity instead of membership providers")]
/// <summary>
/// Abstract Membership Provider that users any implementation of IMembershipMemberService{TEntity} service
/// </summary>
@@ -32,7 +32,7 @@ namespace Umbraco.Web.Security.Providers
protected IMembershipMemberService<TEntity> MemberService { get; private set; }
protected UmbracoMembershipProvider(IMembershipMemberService<TEntity> memberService, IUmbracoVersion umbracoVersion, IHostingEnvironment hostingEnvironment, IIpResolver ipResolver)
:base(hostingEnvironment)
: base(hostingEnvironment)
{
_umbracoVersion = umbracoVersion;
_ipResolver = ipResolver;
@@ -55,9 +55,11 @@ namespace Umbraco.Web.Security.Providers
/// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
public override void Initialize(string name, NameValueCollection config)
{
if (config == null) { throw new ArgumentNullException("config"); }
if (config == null)
{ throw new ArgumentNullException("config"); }
if (string.IsNullOrEmpty(name)) name = ProviderName;
if (string.IsNullOrEmpty(name))
name = ProviderName;
// Initialize base provider class
base.Initialize(name, config);
@@ -80,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);
@@ -174,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;
@@ -423,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;
+1
View File
@@ -5,5 +5,6 @@
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=2DA32DA040A7D74599ABE288C7224CF0/Severity/@EntryValue">HINT</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=37A0B37A0ABAA34AA5CB32A93653C4FE/@KeyIndexDefined">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">Default</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Umbraco/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublishing/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>