using System.Data;
using System.Data.Common;
using System.Globalization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security;
///
/// The user store for back office users
///
public class BackOfficeUserStore :
UmbracoUserStore>,
IUserSessionStore,
IBackOfficeUserStore
{
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;
private readonly IUserGroupService _userGroupService;
private readonly IUserRepository _userRepository;
private readonly IRuntimeState _runtimeState;
private readonly IEventMessagesFactory _eventMessagesFactory;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
[ActivatorUtilitiesConstructor]
public BackOfficeUserStore(
ICoreScopeProvider scopeProvider,
IEntityService entityService,
IExternalLoginWithKeyService externalLoginService,
IOptionsSnapshot globalSettings,
IUmbracoMapper mapper,
BackOfficeErrorDescriber describer,
AppCaches appCaches,
ITwoFactorLoginService twoFactorLoginService,
IUserGroupService userGroupService,
IUserRepository userRepository,
IRuntimeState runtimeState,
IEventMessagesFactory eventMessagesFactory,
ILogger logger)
: base(describer)
{
_scopeProvider = scopeProvider;
_entityService = entityService;
_externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService));
_globalSettings = globalSettings.Value;
_mapper = mapper;
_appCaches = appCaches;
_twoFactorLoginService = twoFactorLoginService;
_userGroupService = userGroupService;
_userRepository = userRepository;
_runtimeState = runtimeState;
_eventMessagesFactory = eventMessagesFactory;
_logger = logger;
_externalLoginService = externalLoginService;
}
///
public async Task ValidateSessionIdAsync(string? userId, string? sessionId)
{
if (!Guid.TryParse(sessionId, out Guid guidSessionId))
{
return false;
}
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;
}
///
public override async Task GetTwoFactorEnabledAsync(
BackOfficeIdentityUser user,
CancellationToken cancellationToken = default)
{
if (!int.TryParse(user.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intUserId))
{
return await base.GetTwoFactorEnabledAsync(user, cancellationToken);
}
return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key);
}
///
public override Task CreateAsync(
BackOfficeIdentityUser user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.Email is null || user.UserName is null)
{
throw new InvalidOperationException("Email and UserName is required.");
}
// 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();
var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix +
aspHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
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,
Key = user.Key,
Kind = user.Kind
};
// 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));
UpdateMemberProperties(userEntity, user);
SaveAsync(userEntity).GetAwaiter().GetResult();
if (!userEntity.HasIdentity)
{
throw new DataException("Could not create the user, check logs for details");
}
// re-assign id and key
user.Id = UserIdToString(userEntity.Id);
user.Key = userEntity.Key;
if (isLoginsPropertyDirty)
{
_externalLoginService.Save(
userEntity.Key,
user.Logins.Select(x => new ExternalLogin(
x.LoginProvider,
x.ProviderKey,
x.UserData)));
}
if (isTokensPropertyDirty)
{
_externalLoginService.Save(
userEntity.Key,
user.LoginTokens.Select(x => new ExternalLoginToken(
x.LoginProvider,
x.Name,
x.Value)));
}
return Task.FromResult(IdentityResult.Success);
}
///
public Task 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);
}
///
public Task DisableAsync(IUser user)
{
// disable
user.IsApproved = false;
return SaveAsync(user);
}
public Task GetAsync(int id)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
try
{
return Task.FromResult(_userRepository.Get(id));
}
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> GetUsersAsync(params int[]? ids)
{
if (ids is null || ids.Length <= 0)
{
return Task.FromResult(Enumerable.Empty());
}
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
IQuery query = _scopeProvider.CreateQuery().Where(x => ids.Contains(x.Id));
IEnumerable users = _userRepository.Get(query);
return Task.FromResult(users);
}
public Task> GetUsersAsync(params Guid[]? keys)
{
if (keys is null || keys.Length <= 0)
{
return Task.FromResult(Enumerable.Empty());
}
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
IEnumerable users = _userRepository.GetMany(keys);
return Task.FromResult(users);
}
///
public Task GetAsync(Guid key)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_userRepository.Get(key));
}
///
public Task 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;
}
}
///
public Task GetByEmailAsync(string email)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
try
{
IQuery query = _scopeProvider.CreateQuery().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;
}
}
///
public Task> GetAllInGroupAsync(int groupId)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
IEnumerable usersInGroup = _userRepository.GetAllInGroup(groupId);
return Task.FromResult(usersInGroup);
}
private bool IsUpgrading =>
_runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade;
///
public override Task UpdateAsync(
BackOfficeIdentityUser user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
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");
}
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
IUser? found = GetAsync(asInt).GetAwaiter().GetResult();
if (found != null)
{
// 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))
{
SaveAsync(found).GetAwaiter().GetResult();
}
if (isLoginsPropertyDirty)
{
_externalLoginService.Save(
found.Key,
user.Logins.Select(x => new ExternalLogin(
x.LoginProvider,
x.ProviderKey,
x.UserData)));
}
if (isTokensPropertyDirty)
{
_externalLoginService.Save(
found.Key,
user.LoginTokens.Select(x => new ExternalLoginToken(
x.LoginProvider,
x.Name,
x.Value)));
}
}
scope.Complete();
}
return Task.FromResult(IdentityResult.Success);
}
///
public override Task DeleteAsync(
BackOfficeIdentityUser user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IUser? found = FindUserFromString(user.Id);
if (found is not null)
{
DisableAsync(found).GetAwaiter().GetResult();
}
_externalLoginService.DeleteUserLogins(user.Key);
return Task.FromResult(IdentityResult.Success);
}
///
public override async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IUser? user = await GetByUserNameAsync(userName);
if (user == null)
{
return null;
}
BackOfficeIdentityUser? result = AssignLoginsCallback(_mapper.Map(user));
return result;
}
///
protected override Task FindUserAsync(string userId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IUser? user = FindUserFromString(userId);
if (user == null)
{
return Task.FromResult((BackOfficeIdentityUser?)null)!;
}
return Task.FromResult(AssignLoginsCallback(_mapper.Map(user)))!;
}
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))
{
return GetAsync(id).GetAwaiter().GetResult();
}
// 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))
{
return GetAsync(key).GetAwaiter().GetResult();
}
throw new InvalidOperationException($"Unable to resolve user with ID {userId}");
}
protected override async Task 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))
{
IUser? user = await GetAsync(key);
if (user is not null)
{
return user.Id;
}
}
throw new InvalidOperationException($"Unable to resolve a user id from {identityId}");
}
///
public override async Task FindByEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
IUser? user = await GetByEmailAsync(email);
BackOfficeIdentityUser? result = user == null
? null
: _mapper.Map(user);
return AssignLoginsCallback(result);
}
///
public override async Task SetPasswordHashAsync(BackOfficeIdentityUser 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;
}
///
public override Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
ICollection logins = user.Logins;
var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id);
IdentityUserLogin userLogin = instance;
logins.Add(userLogin);
return Task.CompletedTask;
}
///
public override Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserLogin? userLogin =
user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
if (userLogin != null)
{
user.Logins.Remove(userLogin);
}
return Task.CompletedTask;
}
///
public override Task> GetLoginsAsync(
BackOfficeIdentityUser user,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult((IList)user.Logins
.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
}
///
/// Lists all users of a given role.
///
///
/// Identity Role names are equal to Umbraco UserGroup alias.
///
public override async Task> GetUsersInRoleAsync(
string normalizedRoleName,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
IUserGroup? userGroup = _userGroupService.GetAsync(normalizedRoleName).GetAwaiter().GetResult();
if (userGroup is null)
{
return new List();
}
IEnumerable users = await GetAllInGroupAsync(userGroup.Id);
IList backOfficeIdentityUsers =
users.Select(x => _mapper.Map(x)).Where(x => x != null).ToList()!;
return backOfficeIdentityUsers;
}
///
/// Overridden to support Umbraco's own data storage requirements
///
///
/// 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.
///
///
public override Task SetTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, string? value, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserToken? token = user.LoginTokens.FirstOrDefault(x =>
x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
// 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)
{
// The token hasn't changed, so there's no reason for us to re-add it.
if (token.Value == value)
{
return Task.CompletedTask;
}
user.LoginTokens.Remove(token);
}
user.LoginTokens.Add(new IdentityUserToken(loginProvider, name, value, user.Id));
return Task.CompletedTask;
}
///
protected override async Task?> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
BackOfficeIdentityUser? user = await FindUserAsync(userId, cancellationToken);
if (user?.Id is null)
{
return null;
}
IList logins = await GetLoginsAsync(user, cancellationToken);
UserLoginInfo? found =
logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider);
if (found == null)
{
return null;
}
return new IdentityUserLogin
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = found.ProviderDisplayName, // TODO: We don't store this value so it will be null
UserId = user.Id,
};
}
///
protected override Task?> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var logins = _externalLoginService.Find(loginProvider, providerKey).ToList();
if (logins.Count == 0)
{
return Task.FromResult?>(null);
}
IIdentityUserLogin found = logins[0];
return Task.FromResult?>(new IdentityUserLogin
{
LoginProvider = found.LoginProvider,
ProviderKey = found.ProviderKey,
ProviderDisplayName = null, // TODO: We don't store this value so it will be null
UserId = found.UserId,
});
}
///
protected override Task?> FindRoleAsync(
string normalizedRoleName,
CancellationToken cancellationToken)
{
IUserGroup? group = _userGroupService.GetAsync(normalizedRoleName).GetAwaiter().GetResult();
if (group?.Name is null)
{
return Task.FromResult?>(null);
}
return Task.FromResult?>(new IdentityRole(group.Name)
{
Id = group.Alias,
});
}
///
protected override async Task?> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken)
{
BackOfficeIdentityUser? user = await FindUserAsync(userId, cancellationToken);
if (user == null)
{
return null!;
}
IdentityUserRole? found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId));
return found;
}
private BackOfficeIdentityUser? AssignLoginsCallback(BackOfficeIdentityUser? user)
{
if (user != null)
{
user.SetLoginsCallback(
new Lazy?>(() => _externalLoginService.GetExternalLogins(user.Key)));
user.SetTokensCallback(
new Lazy?>(() => _externalLoginService.GetExternalLoginTokens(user.Key)));
}
return user;
}
private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser)
{
var anythingChanged = false;
// 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;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime? dt = identityUser.LastLoginDateUtc?.ToLocalTime();
user.LastLoginDate = dt;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.InviteDateUtc))
|| user.InvitedDate?.ToUniversalTime() != identityUser.InviteDateUtc)
{
anythingChanged = true;
user.InvitedDate = identityUser.InviteDateUtc?.ToLocalTime();
}
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;
}
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;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name))
&& user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Name = identityUser.Name ?? string.Empty;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email))
&& user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Email = identityUser.Email!;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.AccessFailedCount))
&& user.FailedPasswordAttempts != identityUser.AccessFailedCount)
{
anythingChanged = true;
user.FailedPasswordAttempts = identityUser.AccessFailedCount;
}
if (user.IsApproved != identityUser.IsApproved)
{
anythingChanged = true;
user.IsApproved = identityUser.IsApproved;
}
if (user.IsLockedOut != identityUser.IsLockedOut)
{
anythingChanged = true;
user.IsLockedOut = identityUser.IsLockedOut;
if (user.IsLockedOut)
{
// need to set the last lockout date
user.LastLockoutDate = DateTime.Now;
}
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName))
&& user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Username = identityUser.UserName!;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))
&& user.RawPasswordValue != identityUser.PasswordHash &&
identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.RawPasswordValue = identityUser.PasswordHash;
user.PasswordConfiguration = identityUser.PasswordConfig;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture))
&& user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
user.Language = identityUser.Culture;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds))
&& user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false)
{
anythingChanged = true;
user.StartMediaIds = identityUser.StartMediaIds;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds))
&& user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false)
{
anythingChanged = true;
user.StartContentIds = identityUser.StartContentIds;
}
if (user.SecurityStamp != identityUser.SecurityStamp)
{
anythingChanged = true;
user.SecurityStamp = identityUser.SecurityStamp;
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)))
{
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).Where(x => x is not null).ToArray();
anythingChanged = true;
// clear out the current groups (need to ToArray since we are modifying the iterator)
user.ClearGroups();
// go lookup all these groups
IReadOnlyUserGroup[] groups = _userGroupService.GetAsync(identityUserRoles).GetAwaiter().GetResult()
.Select(x => x.ToReadOnlyGroup()).ToArray();
// use all of the ones assigned and add them
foreach (IReadOnlyUserGroup group in groups)
{
user.AddGroup(group);
}
// re-assign
identityUser.SetGroups(groups);
}
// we should re-set the calculated start nodes
identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
// reset all changes
identityUser.ResetDirtyProperties(false);
return anythingChanged;
}
///
/// Overridden to support Umbraco's own data storage requirements
///
///
/// 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.
///
///
public override Task RemoveTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserToken? token = user.LoginTokens.FirstOrDefault(x =>
x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
if (token != null)
{
user.LoginTokens.Remove(token);
}
return Task.CompletedTask;
}
///
/// Overridden to support Umbraco's own data storage requirements
///
///
/// 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.
///
///
public override Task GetTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
IIdentityUserToken? token = user.LoginTokens.FirstOrDefault(x =>
x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
return Task.FromResult(token?.Value);
}
}