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