using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web.Security; using AutoMapper; using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Core.Security { public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { _userService = userService; _externalLoginService = externalLoginService; if (userService == null) throw new ArgumentNullException("userService"); if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); _userService = userService; _externalLoginService = externalLoginService; if (usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) { throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); } } /// /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// protected override void DisposeResources() { } /// /// Insert a new user /// /// /// public Task CreateAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); var userType = _userService.GetUserTypeByAlias( user.UserTypeAlias.IsNullOrWhiteSpace() ? _userService.GetDefaultMemberType() : user.UserTypeAlias); var member = new User(userType) { DefaultToLiveEditing = false, Email = user.Email, Language = Configuration.GlobalSettings.DefaultUILanguage, Name = user.Name, Username = user.UserName, StartContentId = -1, StartMediaId = -1, IsLockedOut = false, IsApproved = true }; UpdateMemberProperties(member, 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. if (member.RawPasswordValue.IsNullOrWhiteSpace()) { //this will hash the guid with a salt so should be nicely random var aspHasher = new PasswordHasher(); member.RawPasswordValue = "___UIDEMPTYPWORD__" + aspHasher.HashPassword(Guid.NewGuid().ToString("N")); } _userService.Save(member); //re-assign id user.Id = member.Id; return Task.FromResult(0); } /// /// Update a user /// /// /// public async Task UpdateAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("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) { if (UpdateMemberProperties(found, user)) { _userService.Save(found); } if (user.LoginsChanged) { var logins = await GetLoginsAsync(user); _externalLoginService.SaveUserLogins(found.Id, logins); } } } /// /// Delete a user /// /// /// public Task DeleteAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("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(0); } /// /// Finds a user /// /// /// public Task FindByIdAsync(int userId) { var user = _userService.GetUserById(userId); if (user == null) { return null; } return Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); } /// /// Find a user by name /// /// /// public Task FindByNameAsync(string userName) { var user = _userService.GetByUsername(userName); if (user == null) { return null; } var result = AssignLoginsCallback(Mapper.Map(user)); return Task.FromResult(result); } /// /// Set the user password hash /// /// /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) { if (user == null) throw new ArgumentNullException("user"); if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); user.PasswordHash = passwordHash; return Task.FromResult(0); } /// /// Get the user password hash /// /// /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash); } /// /// Returns true if a user has a password set /// /// /// public Task HasPasswordAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); } /// /// Set the user email /// /// /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email) { if (user == null) throw new ArgumentNullException("user"); if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); user.Email = email; return Task.FromResult(0); } /// /// Get the user email /// /// /// public Task GetEmailAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.Email); } /// /// Returns true if the user email is confirmed /// /// /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { throw new NotImplementedException(); } /// /// Sets whether the user email is confirmed /// /// /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { throw new NotImplementedException(); } /// /// Returns the user associated with this email /// /// /// public Task FindByEmailAsync(string email) { var user = _userService.GetByEmail(email); var result = user == null ? null : Mapper.Map(user); return Task.FromResult(AssignLoginsCallback(result)); } /// /// Adds a user login with the specified provider and key /// /// /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); var logins = user.Logins; var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); var userLogin = instance; logins.Add(userLogin); return Task.FromResult(0); } /// /// Removes the user login with the specified combination if it exists /// /// /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); var provider = login.LoginProvider; var key = login.ProviderKey; var userLogin = user.Logins.SingleOrDefault((l => l.LoginProvider == provider && l.ProviderKey == key)); if (userLogin != null) user.Logins.Remove(userLogin); return Task.FromResult(0); } /// /// Returns the linked accounts for this user /// /// /// public Task> GetLoginsAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult((IList) user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); } /// /// Returns the user associated with this login /// /// public Task FindAsync(UserLoginInfo login) { //get all logins associated with the login id var result = _externalLoginService.Find(login).ToArray(); if (result.Any()) { //return the first member that matches the result var output = (from l in result select _userService.GetUserById(l.Id) into user where user != null select Mapper.Map(user)).FirstOrDefault(); return Task.FromResult(AssignLoginsCallback(output)); } return Task.FromResult(null); } private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) { if (user != null) { user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(user.Id))); } return user; } private bool UpdateMemberProperties(Models.Membership.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 (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } if (user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Email = identityUser.Email; } if (user.FailedPasswordAttempts != identityUser.AccessFailedCount) { anythingChanged = true; user.FailedPasswordAttempts = identityUser.AccessFailedCount; } if (user.IsLockedOut != identityUser.LockoutEnabled) { anythingChanged = true; user.IsLockedOut = identityUser.LockoutEnabled; } if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Username = identityUser.UserName; } if (user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.RawPasswordValue = identityUser.PasswordHash; } if (user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Language = identityUser.Culture; } if (user.StartMediaId != identityUser.StartMediaNode) { anythingChanged = true; user.StartMediaId = identityUser.StartMediaNode; } if (user.StartContentId != identityUser.StartContentNode) { anythingChanged = true; user.StartContentId = identityUser.StartContentNode; } if (user.AllowedSections.ContainsAll(identityUser.AllowedApplications) == false || identityUser.AllowedApplications.ContainsAll(user.AllowedSections) == false) { anythingChanged = true; foreach (var allowedSection in user.AllowedSections) { user.RemoveAllowedSection(allowedSection); } foreach (var allowedApplication in identityUser.AllowedApplications) { user.AddAllowedSection(allowedApplication); } } return anythingChanged; } } }