2020-05-13 18:30:57 +01:00
using System.Data ;
2023-03-29 08:14:47 +02:00
using System.Data.Common ;
2021-09-20 11:22:51 +02:00
using System.Globalization ;
2020-05-13 18:30:57 +01:00
using Microsoft.AspNetCore.Identity ;
2022-01-19 09:21:50 +01:00
using Microsoft.Extensions.DependencyInjection ;
2023-03-29 08:14:47 +02:00
using Microsoft.Extensions.Logging ;
2020-08-21 14:52:47 +01:00
using Microsoft.Extensions.Options ;
2021-03-05 15:36:27 +01:00
using Umbraco.Cms.Core.Cache ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Configuration.Models ;
2023-03-29 08:14:47 +02:00
using Umbraco.Cms.Core.Events ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Mapping ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Membership ;
2023-03-29 08:14:47 +02:00
using Umbraco.Cms.Core.Notifications ;
using Umbraco.Cms.Core.Persistence.Querying ;
using Umbraco.Cms.Core.Persistence.Repositories ;
2021-02-15 11:41:12 +01:00
using Umbraco.Cms.Core.Scoping ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Services ;
2023-03-29 08:14:47 +02:00
using Umbraco.Cms.Core.Services.OperationStatus ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
namespace Umbraco.Cms.Core.Security ;
/// <summary>
/// The user store for back office users
/// </summary>
2023-03-29 08:14:47 +02:00
public class BackOfficeUserStore :
UmbracoUserStore < BackOfficeIdentityUser , IdentityRole < string > > ,
IUserSessionStore < BackOfficeIdentityUser > ,
2023-04-04 15:41:12 +02:00
IBackOfficeUserStore
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
private readonly AppCaches _appCaches ;
private readonly IEntityService _entityService ;
private readonly IExternalLoginWithKeyService _externalLoginService ;
private readonly GlobalSettings _globalSettings ;
private readonly IUmbracoMapper _mapper ;
private readonly ICoreScopeProvider _scopeProvider ;
private readonly ITwoFactorLoginService _twoFactorLoginService ;
2023-02-16 09:39:17 +01:00
private readonly IUserGroupService _userGroupService ;
2023-03-29 08:14:47 +02:00
private readonly IUserRepository _userRepository ;
private readonly IRuntimeState _runtimeState ;
private readonly IEventMessagesFactory _eventMessagesFactory ;
private readonly ILogger < BackOfficeUserStore > _logger ;
2020-12-04 01:38:36 +11:00
2020-12-04 12:44:27 +11:00
/// <summary>
2022-06-02 08:18:31 +02:00
/// Initializes a new instance of the <see cref="BackOfficeUserStore" /> class.
2020-12-04 12:44:27 +11:00
/// </summary>
2022-06-02 08:18:31 +02:00
[ActivatorUtilitiesConstructor]
public BackOfficeUserStore (
ICoreScopeProvider scopeProvider ,
IEntityService entityService ,
IExternalLoginWithKeyService externalLoginService ,
IOptionsSnapshot < GlobalSettings > globalSettings ,
IUmbracoMapper mapper ,
BackOfficeErrorDescriber describer ,
AppCaches appCaches ,
2023-02-16 09:39:17 +01:00
ITwoFactorLoginService twoFactorLoginService ,
2023-03-29 08:14:47 +02:00
IUserGroupService userGroupService ,
IUserRepository userRepository ,
IRuntimeState runtimeState ,
IEventMessagesFactory eventMessagesFactory ,
ILogger < BackOfficeUserStore > logger )
2022-06-02 08:18:31 +02:00
: base ( describer )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_scopeProvider = scopeProvider ;
_entityService = entityService ;
_externalLoginService = externalLoginService ? ? throw new ArgumentNullException ( nameof ( externalLoginService ) ) ;
_globalSettings = globalSettings . Value ;
_mapper = mapper ;
_appCaches = appCaches ;
_twoFactorLoginService = twoFactorLoginService ;
2023-02-16 09:39:17 +01:00
_userGroupService = userGroupService ;
2023-03-29 08:14:47 +02:00
_userRepository = userRepository ;
_runtimeState = runtimeState ;
_eventMessagesFactory = eventMessagesFactory ;
_logger = logger ;
2022-06-02 08:18:31 +02:00
_externalLoginService = externalLoginService ;
}
2022-04-19 08:33:03 +02:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2023-02-22 12:33:41 +01:00
public async Task < bool > ValidateSessionIdAsync ( string? userId , string? sessionId )
2022-06-02 08:18:31 +02:00
{
2023-03-29 08:14:47 +02:00
if ( ! Guid . TryParse ( sessionId , out Guid guidSessionId ) )
2022-06-02 08:18:31 +02:00
{
2023-03-29 08:14:47 +02:00
return false ;
2022-04-19 08:33:03 +02:00
}
2023-03-29 08:14:47 +02:00
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
// We need to resolve the id from the key here...
var id = await ResolveEntityIdFromIdentityId ( userId ) ;
var sessionIsValid = _userRepository . ValidateLoginSession ( id , guidSessionId ) ;
scope . Complete ( ) ;
return sessionIsValid ;
2022-06-02 08:18:31 +02:00
}
/// <inheritdoc />
public override async Task < bool > GetTwoFactorEnabledAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
if ( ! int . TryParse ( user . Id , NumberStyles . Integer , CultureInfo . InvariantCulture , out var intUserId ) )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
return await base . GetTwoFactorEnabledAsync ( user , cancellationToken ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return await _twoFactorLoginService . IsTwoFactorEnabledAsync ( user . Key ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task < IdentityResult > CreateAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-09-19 16:37:24 +02:00
if ( user . Email is null | | user . UserName is null )
2023-02-22 12:33:41 +01:00
{
throw new InvalidOperationException ( "Email and UserName is required." ) ;
}
2022-09-19 16:37:24 +02:00
2023-02-22 12:33:41 +01:00
// the password must be 'something' it could be empty if authenticating
// with an external provider so we'll just generate one and prefix it, the
// prefix will help us determine if the password hasn't actually been specified yet.
// this will hash the guid with a salt so should be nicely random
var aspHasher = new PasswordHasher < BackOfficeIdentityUser > ( ) ;
var emptyPasswordValue = Constants . Security . EmptyPasswordPrefix +
2022-06-02 08:18:31 +02:00
aspHasher . HashPassword ( user , Guid . NewGuid ( ) . ToString ( "N" ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
var userEntity = new User ( _globalSettings , user . Name , user . Email , user . UserName , emptyPasswordValue )
{
Language = user . Culture ? ? _globalSettings . DefaultUILanguage ,
StartContentIds = user . StartContentIds ? ? new int [ ] { } ,
StartMediaIds = user . StartMediaIds ? ? new int [ ] { } ,
IsLockedOut = user . IsLockedOut ,
2024-02-29 10:40:48 +01:00
Key = user . Key ,
2024-09-03 10:43:09 +02:00
Kind = user . Kind
2022-06-02 08:18:31 +02:00
} ;
2020-05-13 18:30:57 +01:00
2024-02-29 10:40:48 +01:00
2022-06-02 08:18:31 +02:00
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Logins ) ) ;
var isTokensPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LoginTokens ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
UpdateMemberProperties ( userEntity , user ) ;
2020-05-13 18:30:57 +01:00
2023-03-29 08:14:47 +02:00
SaveAsync ( userEntity ) . GetAwaiter ( ) . GetResult ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( ! userEntity . HasIdentity )
{
throw new DataException ( "Could not create the user, check logs for details" ) ;
}
2020-10-23 10:10:02 +11:00
2024-05-24 14:55:56 +02:00
// re-assign id and key
2022-06-02 08:18:31 +02:00
user . Id = UserIdToString ( userEntity . Id ) ;
2024-05-24 14:55:56 +02:00
user . Key = userEntity . Key ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( isLoginsPropertyDirty )
{
_externalLoginService . Save (
userEntity . Key ,
user . Logins . Select ( x = > new ExternalLogin (
x . LoginProvider ,
x . ProviderKey ,
x . UserData ) ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
if ( isTokensPropertyDirty )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_externalLoginService . Save (
userEntity . Key ,
user . LoginTokens . Select ( x = > new ExternalLoginToken (
x . LoginProvider ,
x . Name ,
x . Value ) ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
2023-03-29 08:14:47 +02:00
/// <inheritdoc />
public Task < UserOperationStatus > SaveAsync ( IUser user )
{
EventMessages eventMessages = _eventMessagesFactory . Get ( ) ;
using ICoreScope scope = _scopeProvider . CreateCoreScope ( ) ;
var savingNotification = new UserSavingNotification ( user , eventMessages ) ;
if ( scope . Notifications . PublishCancelable ( savingNotification ) )
{
scope . Complete ( ) ;
return Task . FromResult ( UserOperationStatus . CancelledByNotification ) ;
}
if ( string . IsNullOrWhiteSpace ( user . Username ) )
{
throw new ArgumentException ( "Empty username." , nameof ( user ) ) ;
}
if ( string . IsNullOrWhiteSpace ( user . Name ) )
{
throw new ArgumentException ( "Empty name." , nameof ( user ) ) ;
}
try
{
_userRepository . Save ( user ) ;
scope . Notifications . Publish (
new UserSavedNotification ( user , eventMessages ) . WithStateFrom ( savingNotification ) ) ;
scope . Complete ( ) ;
}
catch ( DbException ex )
{
// if we are upgrading and an exception occurs, log and swallow it
if ( IsUpgrading = = false )
{
throw ;
}
_logger . LogWarning (
ex ,
"An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored" ) ;
// we don't want the uow to rollback its scope!
scope . Complete ( ) ;
}
return Task . FromResult ( UserOperationStatus . Success ) ;
}
/// <inheritdoc />
public Task < UserOperationStatus > DisableAsync ( IUser user )
{
// disable
user . IsApproved = false ;
return SaveAsync ( user ) ;
}
public Task < IUser ? > GetAsync ( int id )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
try
{
2024-04-25 15:56:01 +02:00
return Task . FromResult ( _userRepository . Get ( id ) ) ;
2023-03-29 08:14:47 +02:00
}
catch ( DbException )
{
// TODO: refactor users/upgrade
// currently kinda accepting anything on upgrade, but that won't deal with all cases
// so we need to do it differently, see the custom UmbracoPocoDataBuilder which should
// be better BUT requires that the app restarts after the upgrade!
if ( IsUpgrading )
{
// NOTE: this will not be cached
return Task . FromResult ( _userRepository . GetForUpgrade ( id ) ) ;
}
throw ;
}
}
public Task < IEnumerable < IUser > > GetUsersAsync ( params int [ ] ? ids )
{
2024-04-11 13:53:34 +02:00
if ( ids is null | | ids . Length < = 0 )
2023-03-29 08:14:47 +02:00
{
return Task . FromResult ( Enumerable . Empty < IUser > ( ) ) ;
}
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
2024-04-11 13:53:34 +02:00
IQuery < IUser > query = _scopeProvider . CreateQuery < IUser > ( ) . Where ( x = > ids . Contains ( x . Id ) ) ;
IEnumerable < IUser > users = _userRepository . Get ( query ) ;
2023-03-29 08:14:47 +02:00
return Task . FromResult ( users ) ;
}
public Task < IEnumerable < IUser > > GetUsersAsync ( params Guid [ ] ? keys )
{
if ( keys is null | | keys . Length < = 0 )
{
return Task . FromResult ( Enumerable . Empty < IUser > ( ) ) ;
}
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
2024-04-11 13:53:34 +02:00
IEnumerable < IUser > users = _userRepository . GetMany ( keys ) ;
2023-03-29 08:14:47 +02:00
return Task . FromResult ( users ) ;
}
/// <inheritdoc />
public Task < IUser ? > GetAsync ( Guid key )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
2024-04-11 13:53:34 +02:00
return Task . FromResult ( _userRepository . Get ( key ) ) ;
2023-03-29 08:14:47 +02:00
}
/// <inheritdoc />
public Task < IUser ? > GetByUserNameAsync ( string username )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
try
{
IUser ? user = _userRepository . GetByUsername ( username , true ) ;
return Task . FromResult ( user ) ;
}
catch ( DbException )
{
// TODO: refactor users/upgrade
// currently kinda accepting anything on upgrade, but that won't deal with all cases
// so we need to do it differently, see the custom UmbracoPocoDataBuilder which should
// be better BUT requires that the app restarts after the upgrade!
if ( IsUpgrading )
{
// NOTE: this will not be cached
IUser ? upgradeUser = _userRepository . GetForUpgradeByUsername ( username ) ;
return Task . FromResult ( upgradeUser ) ;
}
throw ;
}
2024-02-27 20:57:02 +00:00
2023-03-29 08:14:47 +02:00
}
/// <inheritdoc />
public Task < IUser ? > GetByEmailAsync ( string email )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
try
{
IQuery < IUser > query = _scopeProvider . CreateQuery < IUser > ( ) . Where ( x = > x . Email . Equals ( email ) ) ;
IUser ? user = _userRepository . Get ( query ) . FirstOrDefault ( ) ;
return Task . FromResult ( user ) ;
}
catch ( DbException )
{
// We also need to catch upgrade state here, because the framework will try to call this to validate the email.
if ( IsUpgrading )
{
IUser ? upgradeUser = _userRepository . GetForUpgradeByEmail ( email ) ;
return Task . FromResult ( upgradeUser ) ;
}
throw ;
}
}
/// <inheritdoc />
public Task < IEnumerable < IUser > > GetAllInGroupAsync ( int groupId )
{
using ICoreScope scope = _scopeProvider . CreateCoreScope ( autoComplete : true ) ;
IEnumerable < IUser > usersInGroup = _userRepository . GetAllInGroup ( groupId ) ;
return Task . FromResult ( usersInGroup ) ;
}
private bool IsUpgrading = >
_runtimeState . Level = = RuntimeLevel . Install | | _runtimeState . Level = = RuntimeLevel . Upgrade ;
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task < IdentityResult > UpdateAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-10-23 10:10:02 +11:00
2022-06-02 08:18:31 +02:00
if ( ! int . TryParse ( user . Id , NumberStyles . Integer , CultureInfo . InvariantCulture , out var asInt ) )
{
throw new InvalidOperationException ( "The user id must be an integer to work with the Umbraco" ) ;
}
2020-10-23 10:10:02 +11:00
2022-06-02 08:18:31 +02:00
using ( ICoreScope scope = _scopeProvider . CreateCoreScope ( ) )
{
2023-03-29 08:14:47 +02:00
IUser ? found = GetAsync ( asInt ) . GetAwaiter ( ) . GetResult ( ) ;
2022-06-02 08:18:31 +02:00
if ( found ! = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Logins ) ) ;
var isTokensPropertyDirty = user . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LoginTokens ) ) ;
if ( UpdateMemberProperties ( found , user ) )
2020-05-13 18:30:57 +01:00
{
2023-03-29 08:14:47 +02:00
SaveAsync ( found ) . GetAwaiter ( ) . GetResult ( ) ;
2020-05-13 18:30:57 +01:00
}
2020-12-01 18:14:37 +11:00
2022-06-02 08:18:31 +02:00
if ( isLoginsPropertyDirty )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
_externalLoginService . Save (
found . Key ,
user . Logins . Select ( x = > new ExternalLogin (
x . LoginProvider ,
x . ProviderKey ,
x . UserData ) ) ) ;
2020-05-13 18:30:57 +01:00
}
2020-12-01 18:14:37 +11:00
2022-06-02 08:18:31 +02:00
if ( isTokensPropertyDirty )
{
_externalLoginService . Save (
found . Key ,
user . LoginTokens . Select ( x = > new ExternalLoginToken (
x . LoginProvider ,
x . Name ,
x . Value ) ) ) ;
}
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
scope . Complete ( ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
/// <inheritdoc />
public override Task < IdentityResult > DeleteAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-05-13 18:30:57 +01:00
2023-02-22 12:33:41 +01:00
IUser ? found = FindUserFromString ( user . Id ) ;
if ( found is not null )
2022-06-02 08:18:31 +02:00
{
2023-03-29 08:14:47 +02:00
DisableAsync ( found ) . GetAwaiter ( ) . GetResult ( ) ;
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2023-02-22 12:33:41 +01:00
_externalLoginService . DeleteUserLogins ( user . Key ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( IdentityResult . Success ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2023-03-29 08:14:47 +02:00
public override async Task < BackOfficeIdentityUser ? > FindByNameAsync ( string userName , CancellationToken cancellationToken = default )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2023-03-29 08:14:47 +02:00
IUser ? user = await GetByUserNameAsync ( userName ) ;
2022-06-02 08:18:31 +02:00
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2023-03-29 08:14:47 +02:00
return null ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
BackOfficeIdentityUser ? result = AssignLoginsCallback ( _mapper . Map < BackOfficeIdentityUser > ( user ) ) ;
2020-05-13 18:30:57 +01:00
2023-03-29 08:14:47 +02:00
return result ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < BackOfficeIdentityUser ? > FindUserAsync ( string userId , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2023-02-22 12:33:41 +01:00
IUser ? user = FindUserFromString ( userId ) ;
2022-06-02 08:18:31 +02:00
if ( user = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
return Task . FromResult ( ( BackOfficeIdentityUser ? ) null ) ! ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( AssignLoginsCallback ( _mapper . Map < BackOfficeIdentityUser > ( user ) ) ) ! ;
}
2020-05-13 18:30:57 +01:00
2023-02-22 12:33:41 +01:00
private IUser ? FindUserFromString ( string userId )
{
// We could use ResolveEntityIdFromIdentityId here, but that would require multiple DB calls, so let's not.
if ( TryConvertIdentityIdToInt ( userId , out var id ) )
{
2023-03-29 08:14:47 +02:00
return GetAsync ( id ) . GetAwaiter ( ) . GetResult ( ) ;
2023-02-22 12:33:41 +01:00
}
// We couldn't directly convert the ID to an int, this is because the user logged in with external login.
// So we need to look up the user by key.
if ( Guid . TryParse ( userId , out Guid key ) )
{
2023-03-29 08:14:47 +02:00
return GetAsync ( key ) . GetAwaiter ( ) . GetResult ( ) ;
2023-02-22 12:33:41 +01:00
}
throw new InvalidOperationException ( $"Unable to resolve user with ID {userId}" ) ;
}
protected override async Task < int > ResolveEntityIdFromIdentityId ( string? identityId )
{
if ( TryConvertIdentityIdToInt ( identityId , out var result ) )
{
return result ;
}
// We couldn't directly convert the ID to an int, this is because the user logged in with external login.
// So we need to look up the user by key, and then get the ID.
if ( Guid . TryParse ( identityId , out Guid key ) )
{
2023-03-29 08:14:47 +02:00
IUser ? user = await GetAsync ( key ) ;
2023-02-22 12:33:41 +01:00
if ( user is not null )
{
return user . Id ;
}
}
throw new InvalidOperationException ( $"Unable to resolve a user id from {identityId}" ) ;
}
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2023-03-29 08:14:47 +02:00
public override async Task < BackOfficeIdentityUser ? > FindByEmailAsync (
2022-06-02 08:18:31 +02:00
string email ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2023-03-29 08:14:47 +02:00
IUser ? user = await GetByEmailAsync ( email ) ;
2022-06-02 08:18:31 +02:00
BackOfficeIdentityUser ? result = user = = null
? null
: _mapper . Map < BackOfficeIdentityUser > ( user ) ;
2023-03-29 08:14:47 +02:00
return AssignLoginsCallback ( result ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override async Task SetPasswordHashAsync ( BackOfficeIdentityUser user , string? passwordHash , CancellationToken cancellationToken = default )
2022-06-02 08:18:31 +02:00
{
await base . SetPasswordHashAsync ( user , passwordHash , cancellationToken ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// Clear this so that it's reset at the repository level
user . PasswordConfig = null ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task AddLoginAsync ( BackOfficeIdentityUser user , UserLoginInfo login , CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
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>
2021-04-20 17:13:40 +10:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
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>
2021-04-20 17:13:40 +10:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( login = = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( login ) ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
ICollection < IIdentityUserLogin > logins = user . Logins ;
var instance = new IdentityUserLogin ( login . LoginProvider , login . ProviderKey , user . Id ) ;
IdentityUserLogin userLogin = instance ;
logins . Add ( userLogin ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task RemoveLoginAsync ( BackOfficeIdentityUser user , string loginProvider , string providerKey , CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserLogin ? userLogin =
user . Logins . SingleOrDefault ( l = > l . LoginProvider = = loginProvider & & l . ProviderKey = = providerKey ) ;
if ( userLogin ! = null )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
user . Logins . Remove ( userLogin ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override Task < IList < UserLoginInfo > > GetLoginsAsync (
BackOfficeIdentityUser user ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return Task . FromResult ( ( IList < UserLoginInfo > ) user . Logins
. Select ( l = > new UserLoginInfo ( l . LoginProvider , l . ProviderKey , l . LoginProvider ) ) . ToList ( ) ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Lists all users of a given role.
/// </summary>
/// <remarks>
/// Identity Role names are equal to Umbraco UserGroup alias.
/// </remarks>
2023-03-29 08:14:47 +02:00
public override async Task < IList < BackOfficeIdentityUser > > GetUsersInRoleAsync (
2022-06-02 08:18:31 +02:00
string normalizedRoleName ,
CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
if ( normalizedRoleName = = null )
{
throw new ArgumentNullException ( nameof ( normalizedRoleName ) ) ;
2020-05-13 18:30:57 +01:00
}
2023-02-16 09:39:17 +01:00
IUserGroup ? userGroup = _userGroupService . GetAsync ( normalizedRoleName ) . GetAwaiter ( ) . GetResult ( ) ;
2020-05-13 18:30:57 +01:00
2023-03-29 08:14:47 +02:00
if ( userGroup is null )
{
return new List < BackOfficeIdentityUser > ( ) ;
}
IEnumerable < IUser > users = await GetAllInGroupAsync ( userGroup . Id ) ;
2022-06-02 08:18:31 +02:00
IList < BackOfficeIdentityUser > backOfficeIdentityUsers =
users . Select ( x = > _mapper . Map < BackOfficeIdentityUser > ( x ) ) . Where ( x = > x ! = null ) . ToList ( ) ! ;
2020-05-13 18:30:57 +01:00
2023-03-29 08:14:47 +02:00
return backOfficeIdentityUsers ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
/// </summary>
/// <remarks>
/// The base class's implementation of this calls into FindTokenAsync and AddUserTokenAsync, both methods will only
/// work with ORMs that are change
/// tracking ORMs like EFCore.
/// </remarks>
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
public override Task SetTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , string? value , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
2020-12-04 12:44:27 +11:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
2020-12-04 12:44:27 +11:00
2022-08-16 15:47:01 +02:00
// We have to remove token and then re-add to ensure that LoginTokens are dirty, which is required for them to save
// This is because we're using an observable collection, which only cares about added/removed items.
if ( token is not null )
2022-06-02 08:18:31 +02:00
{
2022-08-17 14:36:09 +02:00
// The token hasn't changed, so there's no reason for us to re-add it.
if ( token . Value = = value )
2020-12-04 12:44:27 +11:00
{
2022-08-17 14:36:09 +02:00
return Task . CompletedTask ;
2020-12-04 12:44:27 +11:00
}
2020-05-13 18:30:57 +01:00
2022-08-16 15:47:01 +02:00
user . LoginTokens . Remove ( token ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-08-16 15:47:01 +02:00
user . LoginTokens . Add ( new IdentityUserToken ( loginProvider , name , value , user . Id ) ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override async Task < IdentityUserLogin < string > ? > FindUserLoginAsync ( string userId , string loginProvider , string providerKey , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-05-13 18:30:57 +01:00
2022-09-19 16:37:24 +02:00
BackOfficeIdentityUser ? user = await FindUserAsync ( userId , cancellationToken ) ;
if ( user ? . Id is null )
2020-05-13 18:30:57 +01:00
{
2022-09-19 16:37:24 +02:00
return null ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
IList < UserLoginInfo > logins = await GetLoginsAsync ( user , cancellationToken ) ;
UserLoginInfo ? found =
logins . FirstOrDefault ( x = > x . ProviderKey = = providerKey & & x . LoginProvider = = loginProvider ) ;
if ( found = = null )
2020-05-13 18:30:57 +01:00
{
2022-09-19 16:37:24 +02:00
return null ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
return new IdentityUserLogin < string >
2020-12-04 12:44:27 +11:00
{
2022-06-02 08:18:31 +02:00
LoginProvider = found . LoginProvider ,
ProviderKey = found . ProviderKey ,
ProviderDisplayName = found . ProviderDisplayName , // TODO: We don't store this value so it will be null
UserId = user . Id ,
} ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < IdentityUserLogin < string > ? > FindUserLoginAsync ( string loginProvider , string providerKey , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
var logins = _externalLoginService . Find ( loginProvider , providerKey ) . ToList ( ) ;
if ( logins . Count = = 0 )
{
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityUserLogin < string > ? > ( null ) ;
2020-12-04 12:44:27 +11:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserLogin found = logins [ 0 ] ;
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityUserLogin < string > ? > ( new IdentityUserLogin < string >
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
LoginProvider = found . LoginProvider ,
ProviderKey = found . ProviderKey ,
ProviderDisplayName = null , // TODO: We don't store this value so it will be null
UserId = found . UserId ,
} ) ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override Task < IdentityRole < string > ? > FindRoleAsync (
2022-06-02 08:18:31 +02:00
string normalizedRoleName ,
CancellationToken cancellationToken )
{
2023-02-16 09:39:17 +01:00
IUserGroup ? group = _userGroupService . GetAsync ( normalizedRoleName ) . GetAwaiter ( ) . GetResult ( ) ;
2022-09-19 16:37:24 +02:00
if ( group ? . Name is null )
2022-06-02 08:18:31 +02:00
{
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityRole < string > ? > ( null ) ;
2020-05-13 18:30:57 +01:00
}
2022-09-19 16:37:24 +02:00
return Task . FromResult < IdentityRole < string > ? > ( new IdentityRole < string > ( group . Name )
{
Id = group . Alias ,
} ) ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
2022-09-19 16:37:24 +02:00
protected override async Task < IdentityUserRole < string > ? > FindUserRoleAsync ( string userId , string roleId , CancellationToken cancellationToken )
2022-06-02 08:18:31 +02:00
{
2022-09-19 16:37:24 +02:00
BackOfficeIdentityUser ? user = await FindUserAsync ( userId , cancellationToken ) ;
2022-06-02 08:18:31 +02:00
if ( user = = null )
{
return null ! ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
IdentityUserRole < string > ? found = user . Roles . FirstOrDefault ( x = > x . RoleId . InvariantEquals ( roleId ) ) ;
2022-09-19 16:37:24 +02:00
return found ;
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
private BackOfficeIdentityUser ? AssignLoginsCallback ( BackOfficeIdentityUser ? user )
{
if ( user ! = null )
{
user . SetLoginsCallback (
2023-02-22 12:33:41 +01:00
new Lazy < IEnumerable < IIdentityUserLogin > ? > ( ( ) = > _externalLoginService . GetExternalLogins ( user . Key ) ) ) ;
2022-06-02 08:18:31 +02:00
user . SetTokensCallback (
2023-02-22 12:33:41 +01:00
new Lazy < IEnumerable < IIdentityUserToken > ? > ( ( ) = > _externalLoginService . GetExternalLoginTokens ( user . Key ) ) ) ;
2022-06-02 08:18:31 +02:00
}
2021-07-26 11:51:54 -06:00
2022-06-02 08:18:31 +02:00
return user ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
private bool UpdateMemberProperties ( IUser user , BackOfficeIdentityUser identityUser )
{
var anythingChanged = false ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LastLoginDateUtc ) )
| | ( user . LastLoginDate ! = default & & identityUser . LastLoginDateUtc . HasValue = = false )
| | ( identityUser . LastLoginDateUtc . HasValue & &
user . LastLoginDate ? . ToUniversalTime ( ) ! = identityUser . LastLoginDateUtc . Value ) )
{
anythingChanged = true ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime ? dt = identityUser . LastLoginDateUtc ? . ToLocalTime ( ) ;
user . LastLoginDate = dt ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . InviteDateUtc ) )
| | user . InvitedDate ? . ToUniversalTime ( ) ! = identityUser . InviteDateUtc )
{
anythingChanged = true ;
user . InvitedDate = identityUser . InviteDateUtc ? . ToLocalTime ( ) ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . LastPasswordChangeDateUtc ) )
| | ( user . LastPasswordChangeDate . HasValue & & user . LastPasswordChangeDate . Value ! = default & &
identityUser . LastPasswordChangeDateUtc . HasValue = = false )
| | ( identityUser . LastPasswordChangeDateUtc . HasValue & & user . LastPasswordChangeDate ? . ToUniversalTime ( ) ! =
identityUser . LastPasswordChangeDateUtc . Value ) )
{
anythingChanged = true ;
user . LastPasswordChangeDate = identityUser . LastPasswordChangeDateUtc ? . ToLocalTime ( ) ? ? DateTime . Now ;
}
2021-09-21 08:56:10 -06:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . 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 . Now : null ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Name ) )
& & user . Name ! = identityUser . Name & & identityUser . Name . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . Name = identityUser . Name ? ? string . Empty ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Email ) )
& & user . Email ! = identityUser . Email & & identityUser . Email . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
2022-09-19 16:37:24 +02:00
user . Email = identityUser . Email ! ;
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . AccessFailedCount ) )
& & user . FailedPasswordAttempts ! = identityUser . AccessFailedCount )
{
anythingChanged = true ;
user . FailedPasswordAttempts = identityUser . AccessFailedCount ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user . IsApproved ! = identityUser . IsApproved )
{
anythingChanged = true ;
user . IsApproved = identityUser . IsApproved ;
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( user . IsLockedOut ! = identityUser . IsLockedOut )
{
anythingChanged = true ;
user . IsLockedOut = identityUser . IsLockedOut ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( user . IsLockedOut )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
// need to set the last lockout date
user . LastLockoutDate = DateTime . Now ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
}
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . UserName ) )
& & user . Username ! = identityUser . UserName & & identityUser . UserName . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
2022-09-19 16:37:24 +02:00
user . Username = identityUser . UserName ! ;
2022-06-02 08:18:31 +02:00
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . PasswordHash ) )
& & user . RawPasswordValue ! = identityUser . PasswordHash & &
identityUser . PasswordHash . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . RawPasswordValue = identityUser . PasswordHash ;
user . PasswordConfiguration = identityUser . PasswordConfig ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Culture ) )
& & user . Language ! = identityUser . Culture & & identityUser . Culture . IsNullOrWhiteSpace ( ) = = false )
{
anythingChanged = true ;
user . Language = identityUser . Culture ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . StartMediaIds ) )
& & user . StartMediaIds . UnsortedSequenceEqual ( identityUser . StartMediaIds ) = = false )
{
anythingChanged = true ;
user . StartMediaIds = identityUser . StartMediaIds ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . StartContentIds ) )
& & user . StartContentIds . UnsortedSequenceEqual ( identityUser . StartContentIds ) = = false )
{
anythingChanged = true ;
user . StartContentIds = identityUser . StartContentIds ;
}
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
if ( user . SecurityStamp ! = identityUser . SecurityStamp )
{
anythingChanged = true ;
user . SecurityStamp = identityUser . SecurityStamp ;
}
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>
2021-04-20 17:13:40 +10:00
2022-06-02 08:18:31 +02:00
if ( identityUser . IsPropertyDirty ( nameof ( BackOfficeIdentityUser . Roles ) ) )
{
var identityUserRoles = identityUser . Roles . Select ( x = > x . RoleId ) . Where ( x = > x is not null ) . ToArray ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
anythingChanged = true ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// clear out the current groups (need to ToArray since we are modifying the iterator)
user . ClearGroups ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// go lookup all these groups
2023-02-16 09:39:17 +01:00
IReadOnlyUserGroup [ ] groups = _userGroupService . GetAsync ( identityUserRoles ) . GetAwaiter ( ) . GetResult ( )
2022-06-02 08:18:31 +02:00
. Select ( x = > x . ToReadOnlyGroup ( ) ) . ToArray ( ) ;
2020-05-13 18:30:57 +01:00
2022-06-02 08:18:31 +02:00
// use all of the ones assigned and add them
foreach ( IReadOnlyUserGroup group in groups )
2020-05-13 18:30:57 +01:00
{
2022-06-02 08:18:31 +02:00
user . AddGroup ( group ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
// re-assign
identityUser . SetGroups ( groups ) ;
2020-05-13 18:30:57 +01:00
}
2022-06-02 08:18:31 +02:00
// we should re-set the calculated start nodes
identityUser . CalculatedMediaStartNodeIds = user . CalculateMediaStartNodeIds ( _entityService , _appCaches ) ;
identityUser . CalculatedContentStartNodeIds = user . CalculateContentStartNodeIds ( _entityService , _appCaches ) ;
2020-12-04 12:44:27 +11:00
2022-06-02 08:18:31 +02:00
// reset all changes
identityUser . ResetDirtyProperties ( false ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return anythingChanged ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
2022-09-19 16:37:24 +02:00
/// Overridden to support Umbraco's own data storage requirements
2022-06-02 08:18:31 +02:00
/// </summary>
/// <remarks>
2022-09-19 16:37:24 +02:00
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
/// tracking ORMs like EFCore.
2022-06-02 08:18:31 +02:00
/// </remarks>
/// <inheritdoc />
public override Task RemoveTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
2021-03-11 19:35:43 +11:00
}
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
if ( token ! = null )
2021-03-11 19:35:43 +11:00
{
2022-06-02 08:18:31 +02:00
user . LoginTokens . Remove ( token ) ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return Task . CompletedTask ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
/// </summary>
/// <remarks>
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both
/// methods will only work with ORMs that are change
/// tracking ORMs like EFCore.
/// </remarks>
/// <inheritdoc />
public override Task < string? > GetTokenAsync ( BackOfficeIdentityUser user , string loginProvider , string name , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ThrowIfDisposed ( ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
if ( user = = null )
2021-03-11 19:35:43 +11:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( user ) ) ;
}
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
IIdentityUserToken ? token = user . LoginTokens . FirstOrDefault ( x = >
x . LoginProvider . InvariantEquals ( loginProvider ) & & x . Name . InvariantEquals ( name ) ) ;
2021-03-11 19:35:43 +11:00
2022-06-02 08:18:31 +02:00
return Task . FromResult ( token ? . Value ) ;
2020-05-13 18:30:57 +01:00
}
}