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,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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user