From 4e69e1abe6b517d101784b045b88087b735e4ac1 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 9 Mar 2020 16:01:57 +0000 Subject: [PATCH] Initial user store --- .../Models/Identity/UserLoginInfoWrapper.cs | 24 + .../Security/BackOfficeUserStore2.cs | 915 ++++++++++++++++++ src/Umbraco.Web/Security/IUserSessionStore.cs | 8 +- src/Umbraco.Web/Umbraco.Web.csproj | 5 +- 4 files changed, 948 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web/Security/BackOfficeUserStore2.cs diff --git a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs index cd3cd51d3f..0baac36032 100644 --- a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs +++ b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs @@ -26,4 +26,28 @@ namespace Umbraco.Web.Models.Identity set => _info.ProviderKey = value; } } + + internal class UserLoginInfoWrapper2 : IUserLoginInfo + { + private readonly Microsoft.AspNetCore.Identity.UserLoginInfo _info; + + public static IUserLoginInfo Wrap(Microsoft.AspNetCore.Identity.UserLoginInfo info) => new UserLoginInfoWrapper2(info); + + private UserLoginInfoWrapper2(Microsoft.AspNetCore.Identity.UserLoginInfo info) + { + _info = info; + } + + public string LoginProvider + { + get => _info.LoginProvider; + set => _info.LoginProvider = value; + } + + public string ProviderKey + { + get => _info.ProviderKey; + set => _info.ProviderKey = value; + } + } } diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore2.cs b/src/Umbraco.Web/Security/BackOfficeUserStore2.cs new file mode 100644 index 0000000000..65de3191de --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeUserStore2.cs @@ -0,0 +1,915 @@ +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 BackOfficeUserStore2 : DisposableObjectSlim, + IUserPasswordStore, + IUserEmailStore, + IUserLoginStore, + IUserRoleStore, + IUserSecurityStampStore, + IUserLockoutStore, + IUserTwoFactorStore, + IUserSessionStore2 + + // 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 BackOfficeUserStore2(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(user.Id.ToString()); + } + + 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 Microsoft.AspNet.Identity.PasswordHasher(); + var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + + aspHasher.HashPassword(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 asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + 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 asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + if (found != null) + { + _userService.Delete(found); + } + _externalLoginService.DeleteUserLogins(asInt.Result); + + return Task.FromResult(IdentityResult.Success); + } + + /// + /// Finds a user + /// + /// + /// + /// + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + var asInt = userId.TryConvertTo(); + if (asInt == false) throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + + var user = _userService.GetUserById(asInt.Result); + 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(int userId, string sessionId) + { + Guid guidSessionId; + if (Guid.TryParse(sessionId, out guidSessionId)) + { + return Task.FromResult(_userService.ValidateLoginSession(userId, guidSessionId)); + } + return Task.FromResult(false); + } + } +} diff --git a/src/Umbraco.Web/Security/IUserSessionStore.cs b/src/Umbraco.Web/Security/IUserSessionStore.cs index 3454b19f84..524017d4ad 100644 --- a/src/Umbraco.Web/Security/IUserSessionStore.cs +++ b/src/Umbraco.Web/Security/IUserSessionStore.cs @@ -14,4 +14,10 @@ namespace Umbraco.Core.Security { Task ValidateSessionIdAsync(int userId, string sessionId); } -} \ No newline at end of file + + public interface IUserSessionStore2 : Microsoft.AspNetCore.Identity.IUserStore, IDisposable + where TUser : class + { + Task ValidateSessionIdAsync(int userId, string sessionId); + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d127783ef2..caa38e56d8 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1,4 +1,4 @@ - + @@ -198,6 +198,7 @@ + @@ -606,6 +607,4 @@ - - \ No newline at end of file