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:
Shannon Deminick
2021-04-20 17:13:40 +10:00
committed by GitHub
parent de28fbb0a4
commit 6c660d5721
26 changed files with 670 additions and 709 deletions

View File

@@ -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();
}
}