Security stamp implementation for members (#10140)
* Getting new netcore PublicAccessChecker in place * Adds full test coverage for PublicAccessChecker * remove PublicAccessComposer * adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller * Implements the required methods on IMemberManager, removes old migrated code * Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops * adds note * adds note * Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling. * Changes name to IUmbracoEndpointBuilder * adds note * Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect * fixing build * Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker * Merges PR * Fixes up build and notes * Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles. * Security stamp for members is now working * Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware. * adds note * removes unused filter, fixes build * fixes WebPath and tests * Looks up entities in one query * remove usings * Fix test, remove stylesheet * Set status code before we write to response to avoid error * Ensures that users and members are validated when logging in. Shares more code between users and members. * merge changes * oops * Fixes RepositoryCacheKeys to ensure the keys are normalized * oops didn't mean to commit this * Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy * oops didn't mean to comit this * bah, far out this keeps getting recommitted. sorry Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -19,11 +19,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
private int[] _startMediaIds;
|
||||
private int[] _startContentIds;
|
||||
|
||||
// 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());
|
||||
|
||||
private static readonly DelegateEqualityComparer<int[]> s_startIdsComparer = new DelegateEqualityComparer<int[]>(
|
||||
(groups, enumerable) => groups.UnsortedSequenceEqual(enumerable),
|
||||
groups => groups.GetHashCode());
|
||||
@@ -64,8 +59,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
_allowedSections = Array.Empty<string>();
|
||||
_culture = globalSettings.DefaultUILanguage;
|
||||
|
||||
// use the property setters - they do more than just setting a field
|
||||
Groups = groups;
|
||||
SetGroups(groups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,7 +112,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <summary>
|
||||
/// Gets a readonly list of the user's allowed sections which are based on it's user groups
|
||||
/// </summary>
|
||||
public string[] AllowedSections => _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray());
|
||||
public string[] AllowedSections => _allowedSections ??= _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the culture
|
||||
@@ -132,31 +126,25 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <summary>
|
||||
/// Gets or sets the user groups
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<IReadOnlyUserGroup> Groups
|
||||
public void SetGroups(IReadOnlyCollection<IReadOnlyUserGroup> value)
|
||||
{
|
||||
get => _groups;
|
||||
set
|
||||
// so they recalculate
|
||||
_allowedSections = null;
|
||||
|
||||
_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>
|
||||
{
|
||||
// so they recalculate
|
||||
_allowedSections = null;
|
||||
|
||||
_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?.ToString()
|
||||
}))
|
||||
{
|
||||
roles.Add(identityUserRole);
|
||||
}
|
||||
|
||||
// now reset the collection
|
||||
Roles = roles;
|
||||
|
||||
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
|
||||
RoleId = x.Alias,
|
||||
UserId = Id?.ToString()
|
||||
}))
|
||||
{
|
||||
roles.Add(identityUserRole);
|
||||
}
|
||||
|
||||
// now reset the collection
|
||||
Roles = roles;
|
||||
}
|
||||
|
||||
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
|
||||
|
||||
@@ -19,12 +19,11 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
// TODO: Make this into a base class that can be re-used
|
||||
|
||||
/// <summary>
|
||||
/// The user store for back office users
|
||||
/// </summary>
|
||||
public class BackOfficeUserStore : UserStoreBase<BackOfficeIdentityUser, IdentityRole<string>, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, IdentityRole<string>>
|
||||
{
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IUserService _userService;
|
||||
@@ -59,19 +58,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
_externalLoginService = externalLoginService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override IQueryable<BackOfficeIdentityUser> Users => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -215,9 +201,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<BackOfficeIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<BackOfficeIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -249,29 +232,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetPasswordHashAsync(BackOfficeIdentityUser 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(BackOfficeIdentityUser 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 result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<BackOfficeIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -286,12 +246,13 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
|
||||
=> GetEmailAsync(user, cancellationToken);
|
||||
public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
|
||||
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
|
||||
// Clear this so that it's reset at the repository level
|
||||
user.PasswordConfig = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
|
||||
@@ -398,100 +359,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a user to a role (user group)
|
||||
/// </summary>
|
||||
public override Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (normalizedRoleName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole == null)
|
||||
{
|
||||
user.AddRole(normalizedRoleName);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the role (user group) for the user
|
||||
/// </summary>
|
||||
public override Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (normalizedRoleName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole != null)
|
||||
{
|
||||
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(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
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(BackOfficeIdentityUser 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>
|
||||
@@ -543,22 +410,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetSecurityStampAsync(BackOfficeIdentityUser 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 BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user)
|
||||
{
|
||||
if (user != null)
|
||||
@@ -678,36 +529,26 @@ namespace Umbraco.Cms.Core.Security
|
||||
user.SecurityStamp = identityUser.SecurityStamp;
|
||||
}
|
||||
|
||||
// TODO: Fix this for Groups too
|
||||
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
|
||||
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)))
|
||||
{
|
||||
var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray();
|
||||
|
||||
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
|
||||
var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray();
|
||||
|
||||
var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray();
|
||||
anythingChanged = true;
|
||||
|
||||
if (userGroupAliases.ContainsAll(combinedAliases) == false
|
||||
|| combinedAliases.ContainsAll(userGroupAliases) == false)
|
||||
// clear out the current groups (need to ToArray since we are modifying the iterator)
|
||||
user.ClearGroups();
|
||||
|
||||
// go lookup all these groups
|
||||
IReadOnlyUserGroup[] groups = _userService.GetUserGroupsByAlias(identityUserRoles).Select(x => x.ToReadOnlyGroup()).ToArray();
|
||||
|
||||
// use all of the ones assigned and add them
|
||||
foreach (IReadOnlyUserGroup group in groups)
|
||||
{
|
||||
anythingChanged = true;
|
||||
|
||||
// clear out the current groups (need to ToArray since we are modifying the iterator)
|
||||
user.ClearGroups();
|
||||
|
||||
// go lookup all these groups
|
||||
var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
|
||||
|
||||
// use all of the ones assigned and add them
|
||||
foreach (var group in groups)
|
||||
{
|
||||
user.AddGroup(group);
|
||||
}
|
||||
|
||||
// re-assign
|
||||
identityUser.Groups = groups;
|
||||
user.AddGroup(group);
|
||||
}
|
||||
|
||||
// re-assign
|
||||
identityUser.SetGroups(groups);
|
||||
}
|
||||
|
||||
// we should re-set the calculated start nodes
|
||||
@@ -731,54 +572,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
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(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task AddClaimsAsync(BackOfficeIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task ReplaceClaimAsync(BackOfficeIdentityUser 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(BackOfficeIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task<IList<BackOfficeIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to support Umbraco's own data storage requirements
|
||||
@@ -859,25 +652,5 @@ namespace Umbraco.Cms.Core.Security
|
||||
|
||||
return Task.FromResult(token?.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override Task<IdentityUserToken<string>> FindTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Umbraco.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source)
|
||||
public static void MergeClaimsFromCookieIdentity(this ClaimsIdentity destination, ClaimsIdentity source)
|
||||
{
|
||||
foreach (Claim claim in source.Claims
|
||||
.Where(claim => !s_ignoredClaims.Contains(claim.Type))
|
||||
|
||||
@@ -100,6 +100,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
//target.Roles =;
|
||||
}
|
||||
|
||||
// TODO: We need to validate this mapping is OK, we need to get Umbraco.Code working
|
||||
private void Map(IMember source, MemberIdentityUser target)
|
||||
{
|
||||
target.Email = source.Email;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// </summary>
|
||||
public class MemberIdentityUser : UmbracoIdentityUser
|
||||
{
|
||||
private string _comments;
|
||||
private string _comments;
|
||||
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
|
||||
|
||||
// Custom comparer for enumerables
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <summary>
|
||||
/// A custom user store that uses Umbraco member data
|
||||
/// </summary>
|
||||
public class MemberUserStore : UserStoreBase<MemberIdentityUser, UmbracoIdentityRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdentityRole>
|
||||
{
|
||||
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
|
||||
private readonly IMemberService _memberService;
|
||||
@@ -32,7 +32,11 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <param name="mapper">The mapper for properties</param>
|
||||
/// <param name="scopeProvider">The scope provider</param>
|
||||
/// <param name="describer">The error describer</param>
|
||||
public MemberUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
UmbracoMapper mapper,
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer)
|
||||
: base(describer)
|
||||
{
|
||||
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
|
||||
@@ -40,22 +44,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
|
||||
}
|
||||
|
||||
//TODO: why is this not supported?
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override IQueryable<MemberIdentityUser> Users => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
|
||||
=> GetUserNameAsync(user, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default)
|
||||
=> SetUserNameAsync(user, normalizedName, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IdentityResult> CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -68,6 +56,8 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
|
||||
// create member
|
||||
IMember memberEntity = _memberService.CreateMember(
|
||||
user.UserName,
|
||||
@@ -130,36 +120,33 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new InvalidOperationException("The user id must be an integer to work with Umbraco");
|
||||
}
|
||||
|
||||
using (IScope scope = _scopeProvider.CreateScope())
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
|
||||
IMember found = _memberService.GetById(asInt.Result);
|
||||
if (found != null)
|
||||
{
|
||||
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(MemberIdentityUser.Logins));
|
||||
|
||||
if (UpdateMemberProperties(found, user))
|
||||
{
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.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)));
|
||||
//}
|
||||
_memberService.Save(found);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
// TODO: when to implement external login service?
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
//if (isLoginsPropertyDirty)
|
||||
//{
|
||||
// _externalLoginService.Save(
|
||||
// found.Id,
|
||||
// user.Logins.Select(x => new ExternalLogin(
|
||||
// x.LoginProvider,
|
||||
// x.ProviderKey,
|
||||
// x.UserData)));
|
||||
//}
|
||||
}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -196,9 +183,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<MemberIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<MemberIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -235,30 +219,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetPasswordHashAsync(MemberIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
|
||||
|
||||
// Clear this so that it's reset at the repository level
|
||||
user.PasswordConfig = null;
|
||||
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> HasPasswordAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This checks if it's null
|
||||
bool 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<MemberIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -272,14 +232,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return Task.FromResult(AssignLoginsCallback(result));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedEmailAsync(MemberIdentityUser user, CancellationToken cancellationToken)
|
||||
=> GetEmailAsync(user, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedEmailAsync(MemberIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
|
||||
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task AddLoginAsync(MemberIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -434,93 +386,33 @@ namespace Umbraco.Cms.Core.Security
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task AddToRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (cancellationToken != null)
|
||||
{
|
||||
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(MemberIdentityUser 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>
|
||||
/// <remarks>
|
||||
/// This lazy loads the roles for the member
|
||||
/// </remarks>
|
||||
public override Task<IList<string>> GetRolesAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
EnsureRoles(user);
|
||||
return base.GetRolesAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
private void EnsureRoles(MemberIdentityUser user)
|
||||
{
|
||||
if (user.Roles.Count == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
// if there are no roles, they either haven't been loaded since we don't eagerly
|
||||
// load for members, or they just have no roles.
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -528,19 +420,9 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// </summary>
|
||||
public override Task<bool> IsInRoleAsync(MemberIdentityUser user, string roleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
EnsureRoles(user);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(roleName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(roleName));
|
||||
}
|
||||
|
||||
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName));
|
||||
return base.IsInRoleAsync(user, roleName, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -597,22 +479,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetSecurityStampAsync(MemberIdentityUser 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 MemberIdentityUser AssignLoginsCallback(MemberIdentityUser user)
|
||||
{
|
||||
if (user != null)
|
||||
@@ -624,70 +490,70 @@ namespace Umbraco.Cms.Core.Security
|
||||
return user;
|
||||
}
|
||||
|
||||
private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUserMember)
|
||||
private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUser)
|
||||
{
|
||||
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(MemberIdentityUser.LastLoginDateUtc))
|
||||
|| (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false)
|
||||
|| (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value))
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc))
|
||||
|| (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
|
||||
|| (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.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();
|
||||
DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
|
||||
member.LastLoginDate = dt;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc))
|
||||
|| (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false)
|
||||
|| (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value))
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc))
|
||||
|| (member.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
|
||||
|| (identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime();
|
||||
member.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Comments))
|
||||
&& member.Comments != identityUserMember.Comments && identityUserMember.Comments.IsNullOrWhiteSpace() == false)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Comments))
|
||||
&& member.Comments != identityUser.Comments && identityUser.Comments.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Comments = identityUserMember.Comments;
|
||||
member.Comments = identityUser.Comments;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.EmailConfirmed))
|
||||
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false)
|
||||
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed))
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.EmailConfirmed))
|
||||
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false)
|
||||
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed))
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null;
|
||||
member.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Name))
|
||||
&& member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Name))
|
||||
&& member.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Name = identityUserMember.Name;
|
||||
member.Name = identityUser.Name;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Email))
|
||||
&& member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Email))
|
||||
&& member.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Email = identityUserMember.Email;
|
||||
member.Email = identityUser.Email;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount))
|
||||
&& member.FailedPasswordAttempts != identityUserMember.AccessFailedCount)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount))
|
||||
&& member.FailedPasswordAttempts != identityUser.AccessFailedCount)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.FailedPasswordAttempts = identityUserMember.AccessFailedCount;
|
||||
member.FailedPasswordAttempts = identityUser.AccessFailedCount;
|
||||
}
|
||||
|
||||
if (member.IsLockedOut != identityUserMember.IsLockedOut)
|
||||
if (member.IsLockedOut != identityUser.IsLockedOut)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.IsLockedOut = identityUserMember.IsLockedOut;
|
||||
member.IsLockedOut = identityUser.IsLockedOut;
|
||||
|
||||
if (member.IsLockedOut)
|
||||
{
|
||||
@@ -696,112 +562,46 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
}
|
||||
|
||||
if (member.IsApproved != identityUserMember.IsApproved)
|
||||
if (member.IsApproved != identityUser.IsApproved)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.IsApproved = identityUserMember.IsApproved;
|
||||
member.IsApproved = identityUser.IsApproved;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.UserName))
|
||||
&& member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.UserName))
|
||||
&& member.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Username = identityUserMember.UserName;
|
||||
member.Username = identityUser.UserName;
|
||||
}
|
||||
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash))
|
||||
&& member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false)
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash))
|
||||
&& member.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.RawPasswordValue = identityUserMember.PasswordHash;
|
||||
member.PasswordConfiguration = identityUserMember.PasswordConfig;
|
||||
member.RawPasswordValue = identityUser.PasswordHash;
|
||||
member.PasswordConfiguration = identityUser.PasswordConfig;
|
||||
}
|
||||
|
||||
if (member.SecurityStamp != identityUserMember.SecurityStamp)
|
||||
if (member.SecurityStamp != identityUser.SecurityStamp)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.SecurityStamp = identityUserMember.SecurityStamp;
|
||||
member.SecurityStamp = identityUser.SecurityStamp;
|
||||
}
|
||||
|
||||
// TODO: Fix this for Groups too (as per backoffice comment)
|
||||
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Groups)))
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Roles)))
|
||||
{
|
||||
anythingChanged = true;
|
||||
|
||||
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
|
||||
_memberService.ReplaceRoles(new[] { member.Id }, identityUserRoles);
|
||||
}
|
||||
|
||||
// reset all changes
|
||||
identityUserMember.ResetDirtyProperties(false);
|
||||
identityUser.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(MemberIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task AddClaimsAsync(MemberIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task ReplaceClaimAsync(MemberIdentityUser 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(MemberIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task<IList<MemberIdentityUser>> 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(MemberIdentityUser 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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
247
src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs
Normal file
247
src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
public abstract class UmbracoUserStore<TUser, TRole> : UserStoreBase<TUser, TRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
where TUser : UmbracoIdentityUser
|
||||
where TRole : IdentityRole<string>
|
||||
{
|
||||
protected UmbracoUserStore(IdentityErrorDescriber describer) : base(describer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override IQueryable<TUser> Users => throw new NotImplementedException();
|
||||
|
||||
protected 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);
|
||||
}
|
||||
|
||||
protected static string UserIdToString(int userId) => string.Intern(userId.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a user to a role (user group)
|
||||
/// </summary>
|
||||
public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (normalizedRoleName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole == null)
|
||||
{
|
||||
user.AddRole(normalizedRoleName);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken)
|
||||
=> GetEmailAsync(user, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default)
|
||||
=> GetUserNameAsync(user, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of role names the specified user belongs to.
|
||||
/// </summary>
|
||||
public override Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetSecurityStampAsync(TUser 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> HasPasswordAsync(TUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This checks if it's null
|
||||
bool result = await base.HasPasswordAsync(user, cancellationToken);
|
||||
if (result)
|
||||
{
|
||||
// we also want to check empty
|
||||
return string.IsNullOrEmpty(user.PasswordHash) == false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a user is in the role
|
||||
/// </summary>
|
||||
public override Task<bool> IsInRoleAsync(TUser 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>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the role (user group) for the user
|
||||
/// </summary>
|
||||
public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (normalizedRoleName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
}
|
||||
|
||||
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole != null)
|
||||
{
|
||||
user.Roles.Remove(userRole);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken)
|
||||
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default)
|
||||
=> SetUserNameAsync(user, normalizedName, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
|
||||
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task<IdentityUserToken<string>> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user