using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models.Identity; using Constants = Umbraco.Core.Constants; using IUser = Umbraco.Core.Models.Membership.IUser; using UserLoginInfo = Microsoft.AspNetCore.Identity.UserLoginInfo; namespace Umbraco.Web.Security { public class BackOfficeUserStore : DisposableObjectSlim, IUserPasswordStore, IUserEmailStore, IUserLoginStore, IUserRoleStore, IUserSecurityStampStore, IUserLockoutStore, IUserTwoFactorStore, IUserSessionStore // TODO: This would require additional columns/tables for now people will need to implement this on their own //IUserPhoneNumberStore, // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation //IQueryableUserStore { private readonly IUserService _userService; private readonly IEntityService _entityService; private readonly IExternalLoginService _externalLoginService; private readonly IGlobalSettings _globalSettings; private readonly UmbracoMapper _mapper; private bool _disposed = false; public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, UmbracoMapper mapper) { _userService = userService; _entityService = entityService; _externalLoginService = externalLoginService; _globalSettings = globalSettings; if (userService == null) throw new ArgumentNullException("userService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); _mapper = mapper; _userService = userService; _externalLoginService = externalLoginService; } /// /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// protected override void DisposeResources() { _disposed = true; } public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(UserIdToString(user.Id)); } public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.UserName); } public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.UserName = userName; return Task.CompletedTask; } public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) { return GetUserNameAsync(user, cancellationToken); } public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) { return SetUserNameAsync(user, normalizedName, cancellationToken); } /// /// Insert a new user /// /// /// /// public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); //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, }; UpdateMemberProperties(userEntity, user); // TODO: We should deal with Roles --> User Groups here which we currently are not doing _userService.Save(userEntity); if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); //re-assign id user.Id = userEntity.Id; return Task.FromResult(IdentityResult.Success); } /// /// Update a user /// /// /// /// public async Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); var found = _userService.GetUserById(user.Id); if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. var isLoginsPropertyDirty = user.IsPropertyDirty("Logins"); if (UpdateMemberProperties(found, user)) { _userService.Save(found); } if (isLoginsPropertyDirty) { var logins = await GetLoginsAsync(user); _externalLoginService.SaveUserLogins(found.Id, logins.Select(UserLoginInfoWrapper2.Wrap)); } } return IdentityResult.Success; } /// /// Delete a user /// /// /// public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); var found = _userService.GetUserById(user.Id); if (found != null) { _userService.Delete(found); } _externalLoginService.DeleteUserLogins(user.Id); return Task.FromResult(IdentityResult.Success); } /// /// Finds a user /// /// /// /// public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); var user = _userService.GetUserById(UserIdToInt(userId)); if (user == null) return null; return await Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); } /// /// Find a user by name /// /// /// /// public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); var user = _userService.GetByUsername(userName); if (user == null) { return null; } var result = AssignLoginsCallback(_mapper.Map(user)); return await Task.FromResult(result); } /// /// Set the user password hash /// /// /// /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); user.PasswordHash = passwordHash; return Task.CompletedTask; } /// /// Get the user password hash /// /// /// /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.PasswordHash); } /// /// Returns true if a user has a password set /// /// /// /// public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); } /// /// Set the user email /// /// /// /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); // if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); user.Email = email; return Task.CompletedTask; } /// /// Get the user email /// /// /// /// public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.Email); } /// /// Returns true if the user email is confirmed /// /// /// /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.EmailConfirmed); } /// /// Sets whether the user email is confirmed /// /// /// /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.EmailConfirmed = confirmed; return Task.CompletedTask; } /// /// Returns the user associated with this email /// /// /// /// public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); var user = _userService.GetByEmail(email); var result = user == null ? null : _mapper.Map(user); return Task.FromResult(AssignLoginsCallback(result)); } public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) { return GetEmailAsync(user, cancellationToken); } public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) { return SetEmailAsync(user, normalizedEmail, cancellationToken); } /// /// Adds a user login with the specified provider and key /// /// /// /// /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); if (login == null) throw new ArgumentNullException(nameof(login)); var logins = user.Logins; var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); var userLogin = instance; logins.Add(userLogin); return Task.CompletedTask; } /// /// Removes the user login with the specified combination if it exists /// /// /// /// /// /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); // TODO: SCOTT: Consider adding display name to IIdentityUserLogin var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); if (userLogin != null) user.Logins.Remove(userLogin); return Task.CompletedTask; } /// /// Returns the linked accounts for this user /// /// /// /// public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { 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()); } /// /// Returns the user associated with this login /// /// public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); //get all logins associated with the login id var result = _externalLoginService.Find(UserLoginInfoWrapper2.Wrap(new UserLoginInfo(loginProvider, providerKey, loginProvider))).ToArray(); if (result.Any()) { //return the first user that matches the result BackOfficeIdentityUser output = null; foreach (var l in result) { var user = _userService.GetUserById(l.UserId); if (user != null) { output = _mapper.Map(user); break; } } return Task.FromResult(AssignLoginsCallback(output)); } return Task.FromResult(null); } /// /// Adds a user to a role (user group) /// /// /// /// /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); if (userRole == null) { user.AddRole(normalizedRoleName); } return Task.CompletedTask; } /// /// Removes the role (user group) for the user /// /// /// /// /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); if (user == null) throw new ArgumentNullException(nameof(user)); if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); if (userRole != null) { user.Roles.Remove(userRole); } return Task.CompletedTask; } /// /// Returns the roles (user groups) for this user /// /// /// /// public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } /// /// Returns true if a user is in the role /// /// /// /// /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); } public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) { throw new NotImplementedException(); } /// /// Set the security stamp for the user /// /// /// /// /// public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.SecurityStamp = stamp; return Task.CompletedTask; } /// /// Get the user security stamp /// /// /// /// public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() ? user.PasswordHash.GenerateHash() : user.SecurityStamp); } private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) { if (user != null) { user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(user.Id))); } return user; } /// /// Sets whether two factor authentication is enabled for the user /// /// /// /// /// public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.TwoFactorEnabled = false; return Task.CompletedTask; } /// /// Returns whether two factor authentication is enabled for the user /// /// /// public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); return Task.FromResult(false); } #region IUserLockoutStore /// /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out. /// /// /// /// /// /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status /// public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return user.LockoutEndDateUtc.HasValue ? Task.FromResult(DateTimeOffset.MaxValue) : Task.FromResult(DateTimeOffset.MinValue); } /// /// Locks a user out until the specified end date (set to a past date, to unlock a user) /// /// /// /// /// /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status /// public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime; return Task.CompletedTask; } /// /// Used to record when an attempt to access the user has failed /// /// /// /// public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.AccessFailedCount++; return Task.FromResult(user.AccessFailedCount); } /// /// Used to reset the access failed count, typically after the account is successfully accessed /// /// /// /// public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.AccessFailedCount = 0; return Task.CompletedTask; } /// /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is /// verified or the account is locked out. /// /// /// /// public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.AccessFailedCount); } /// /// Returns true /// /// /// /// public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); return Task.FromResult(user.LockoutEnabled); } /// /// Doesn't actually perform any function, users can always be locked out /// /// /// /// public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) throw new ArgumentNullException(nameof(user)); user.LockoutEnabled = enabled; return Task.CompletedTask; } #endregion 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("LastLoginDateUtc") || (user.LastLoginDate != default(DateTime) && 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 var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); user.LastLoginDate = dt; } if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) { anythingChanged = true; user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); } if (identityUser.IsPropertyDirty("EmailConfirmed") || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) { anythingChanged = true; user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; } if (identityUser.IsPropertyDirty("Name") && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } if (identityUser.IsPropertyDirty("Email") && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Email = identityUser.Email; } if (identityUser.IsPropertyDirty("AccessFailedCount") && user.FailedPasswordAttempts != identityUser.AccessFailedCount) { anythingChanged = true; user.FailedPasswordAttempts = identityUser.AccessFailedCount; } 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("UserName") && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Username = identityUser.UserName; } if (identityUser.IsPropertyDirty("PasswordHash") && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.RawPasswordValue = identityUser.PasswordHash; } if (identityUser.IsPropertyDirty("Culture") && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Language = identityUser.Culture; } if (identityUser.IsPropertyDirty("StartMediaIds") && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) { anythingChanged = true; user.StartMediaIds = identityUser.StartMediaIds; } if (identityUser.IsPropertyDirty("StartContentIds") && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) { anythingChanged = true; user.StartContentIds = identityUser.StartContentIds; } if (user.SecurityStamp != identityUser.SecurityStamp) { anythingChanged = true; user.SecurityStamp = identityUser.SecurityStamp; } // TODO: Fix this for Groups too if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) { var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray(); var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray(); var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray(); if (userGroupAliases.ContainsAll(combinedAliases) == false || combinedAliases.ContainsAll(userGroupAliases) == false) { anythingChanged = true; //clear out the current groups (need to ToArray since we are modifying the iterator) user.ClearGroups(); //go lookup all these groups var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); //use all of the ones assigned and add them foreach (var group in groups) { user.AddGroup(group); } //re-assign identityUser.Groups = groups; } } //we should re-set the calculated start nodes identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); //reset all changes identityUser.ResetDirtyProperties(false); return anythingChanged; } private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(GetType().Name); } public Task ValidateSessionIdAsync(string userId, string sessionId) { Guid guidSessionId; if (Guid.TryParse(sessionId, out guidSessionId)) { return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); } return Task.FromResult(false); } private string UserIdToString(int userId) { var attempt = userId.TryConvertTo(); if (attempt.Success) return attempt.Result; throw new InvalidOperationException("Unable to convert user ID to string", attempt.Exception); } private int UserIdToInt(string userId) { var attempt = userId.TryConvertTo(); if (attempt.Success) return attempt.Result; throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); } } }