From 2f9e92eee78219ee698e4a123da0e7a72c38d721 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 3 Dec 2020 20:30:35 +1100 Subject: [PATCH 01/14] Make BackOfficeClaimsPrincipalFactory not be generic, doesn't need to be, cleans up code as per rules --- .../BackOfficeClaimsPrincipalFactory.cs | 39 +++-- ...kOfficeServiceCollectionExtensionsTests.cs | 4 +- .../BackOfficeClaimsPrincipalFactoryTests.cs | 64 +++---- .../Security/BackOfficeCookieManagerTests.cs | 39 +---- .../BackOfficeServiceCollectionExtensions.cs | 4 +- .../Security/BackOfficeUserManager.cs | 165 ++++++++++-------- .../ConfigureBackOfficeCookieOptions.cs | 100 ++++++----- 7 files changed, 216 insertions(+), 199 deletions(-) diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs index 568c028e67..22ea4423d2 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -7,27 +7,43 @@ using Microsoft.Extensions.Options; namespace Umbraco.Core.BackOffice { - public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory - where TUser : BackOfficeIdentityUser + /// + /// A + /// + public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory { - public BackOfficeClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) + + /// + /// Initializes a new instance of the class. + /// + /// The user manager + /// The + public BackOfficeClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) : base(userManager, optionsAccessor) { } - public override async Task CreateAsync(TUser user) + /// + /// + /// Returns a custom and allows flowing claims from the external identity + /// + public override async Task CreateAsync(BackOfficeIdentityUser user) { - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var baseIdentity = await base.GenerateClaimsAsync(user); + ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - foreach (var claim in user.Claims) + foreach (Models.Identity.IdentityUserClaim claim in user.Claims) { baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); } - + // TODO: We want to remove UmbracoBackOfficeIdentity and only rely on ClaimsIdentity, once + // that is done then we'll create a ClaimsIdentity with all of the requirements here instead var umbracoIdentity = new UmbracoBackOfficeIdentity( baseIdentity, user.Id, @@ -43,7 +59,8 @@ namespace Umbraco.Core.BackOffice return new ClaimsPrincipal(umbracoIdentity); } - protected override async Task GenerateClaimsAsync(TUser user) + /// + protected override async Task GenerateClaimsAsync(BackOfficeIdentityUser user) { // TODO: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79 // since it's setting an authentication type that is probably not what we want. @@ -51,7 +68,7 @@ namespace Umbraco.Core.BackOffice // the method above just returns a principal that wraps the identity and we dont use a custom principal, // see https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L66 - var identity = await base.GenerateClaimsAsync(user); + ClaimsIdentity identity = await base.GenerateClaimsAsync(user); return identity; } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 450b3a341a..26c3f7875c 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -26,7 +26,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice var principalFactory = Services.GetService>(); Assert.IsNotNull(principalFactory); - Assert.AreEqual(typeof(BackOfficeClaimsPrincipalFactory), principalFactory.GetType()); + Assert.AreEqual(typeof(BackOfficeClaimsPrincipalFactory), principalFactory.GetType()); } [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index 13c73dfa96..5291c1b12e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; @@ -8,50 +8,41 @@ using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Extensions; -using Umbraco.Tests.Common.Builders; namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { [TestFixture] public class BackOfficeClaimsPrincipalFactoryTests { - private const int _testUserId = 2; - private const string _testUserName = "bob"; - private const string _testUserGivenName = "Bob"; - private const string _testUserCulture = "en-US"; - private const string _testUserSecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"; + private const int TestUserId = 2; + private const string TestUserName = "bob"; + private const string TestUserGivenName = "Bob"; + private const string TestUserCulture = "en-US"; + private const string TestUserSecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"; private BackOfficeIdentityUser _testUser; private Mock> _mockUserManager; + private static Mock> GetMockedUserManager() + => new Mock>(new Mock>().Object, null, null, null, null, null, null, null, null); + [Test] public void Ctor_When_UserManager_Is_Null_Expect_ArgumentNullException() - { - Assert.Throws(() => new BackOfficeClaimsPrincipalFactory( - null, - new OptionsWrapper(new BackOfficeIdentityOptions()))); - } + => Assert.Throws(() => new BackOfficeClaimsPrincipalFactory( + null, + new OptionsWrapper(new BackOfficeIdentityOptions()))); [Test] public void Ctor_When_Options_Are_Null_Expect_ArgumentNullException() - { - Assert.Throws(() => new BackOfficeClaimsPrincipalFactory( - new Mock>(new Mock>().Object, - null, null, null, null, null, null, null, null).Object, - null)); - } + => Assert.Throws(() => new BackOfficeClaimsPrincipalFactory(GetMockedUserManager().Object, null)); [Test] public void Ctor_When_Options_Value_Is_Null_Expect_ArgumentNullException() - { - Assert.Throws(() => new BackOfficeClaimsPrincipalFactory( - new Mock>(new Mock>().Object, - null, null, null, null, null, null, null, null).Object, - new OptionsWrapper(null))); - } + => Assert.Throws(() => new BackOfficeClaimsPrincipalFactory( + GetMockedUserManager().Object, + new OptionsWrapper(null))); [Test] public void CreateAsync_When_User_Is_Null_Expect_ArgumentNullException() @@ -72,8 +63,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice Assert.IsNotNull(umbracoBackOfficeIdentity); } - [TestCase(ClaimTypes.NameIdentifier, _testUserId)] - [TestCase(ClaimTypes.Name, _testUserName)] + [TestCase(ClaimTypes.NameIdentifier, TestUserId)] + [TestCase(ClaimTypes.Name, TestUserName)] public async Task CreateAsync_Should_Include_Claim(string expectedClaimType, object expectedClaimValue) { var sut = CreateSut(); @@ -141,17 +132,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { var globalSettings = new GlobalSettings { DefaultUILanguage = "test" }; - _testUser = new BackOfficeIdentityUser(globalSettings, _testUserId, new List()) + _testUser = new BackOfficeIdentityUser(globalSettings, TestUserId, new List()) { - UserName = _testUserName, - Name = _testUserGivenName, + UserName = TestUserName, + Name = TestUserGivenName, Email = "bob@umbraco.test", - SecurityStamp = _testUserSecurityStamp, - Culture = _testUserCulture + SecurityStamp = TestUserSecurityStamp, + Culture = TestUserCulture }; - _mockUserManager = new Mock>(new Mock>().Object, - null, null, null, null, null, null, null, null); + _mockUserManager = GetMockedUserManager(); _mockUserManager.Setup(x => x.GetUserIdAsync(_testUser)).ReturnsAsync(_testUser.Id.ToString); _mockUserManager.Setup(x => x.GetUserNameAsync(_testUser)).ReturnsAsync(_testUser.UserName); _mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false); @@ -159,10 +149,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice _mockUserManager.Setup(x => x.SupportsUserRole).Returns(false); } - private BackOfficeClaimsPrincipalFactory CreateSut() - { - return new BackOfficeClaimsPrincipalFactory(_mockUserManager.Object, + private BackOfficeClaimsPrincipalFactory CreateSut() => new BackOfficeClaimsPrincipalFactory( + _mockUserManager.Object, new OptionsWrapper(new BackOfficeIdentityOptions())); - } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs index e1a8ff9c58..3270a003f5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Moq; @@ -28,8 +28,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security runtime, Mock.Of(), globalSettings, - Mock.Of(), - Mock.Of()); + Mock.Of()); var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); @@ -47,8 +46,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security runtime, Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"), globalSettings, - Mock.Of(), - Mock.Of()); + Mock.Of()); var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); @@ -67,8 +65,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security runtime, Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), globalSettings, - Mock.Of(), - GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + Mock.Of()); + + GenerateAuthPaths(out var remainingTimeoutSecondsPath, out var isAuthPath); var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{remainingTimeoutSecondsPath}")); Assert.IsTrue(result); @@ -89,8 +88,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security runtime, Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), globalSettings, - Mock.Of(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"), - GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + Mock.Of(x => x.IsAvailable && x.Get(Constants.Security.ForceReAuthFlag) == "not null")); var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); Assert.IsTrue(result); @@ -108,8 +106,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security runtime, Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), globalSettings, - Mock.Of(), - GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + Mock.Of()); var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); Assert.IsFalse(result); @@ -119,7 +116,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security Assert.IsFalse(result); } - private LinkGenerator GetMockLinkGenerator(out string remainingTimeoutSecondsPath, out string isAuthPath) + private void GenerateAuthPaths(out string remainingTimeoutSecondsPath, out string isAuthPath) { var controllerName = ControllerExtensions.GetControllerName(); @@ -129,24 +126,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security // this is on the same controller but is considered a back office request var aPath = isAuthPath = $"/umbraco/{Constants.Web.Mvc.BackOfficePathSegment}/{Constants.Web.Mvc.BackOfficeApiArea}/{controllerName}/{nameof(AuthenticationController.IsAuthenticated)}".ToLower(); - var linkGenerator = new Mock(); - linkGenerator.Setup(x => x.GetPathByAddress( - //It.IsAny(), - It.IsAny(), - //It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())).Returns((RouteValuesAddress address, RouteValueDictionary routeVals1, PathString path, FragmentString fragment, LinkOptions options) => - { - if (routeVals1["action"].ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds)) - return rPath; - if (routeVals1["action"].ToString() == nameof(AuthenticationController.IsAuthenticated).ToLower()) - return aPath; - return null; - }); - - return linkGenerator.Object; } } } diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 413a54a28b..74953b19be 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Filters; @@ -36,7 +36,7 @@ namespace Umbraco.Extensions .AddUserStore() .AddUserManager() .AddSignInManager() - .AddClaimsPrincipalFactory>(); + .AddClaimsPrincipalFactory(); // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance services.ConfigureOptions(); diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs index 464f2a38aa..2906d9d87a 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs @@ -67,8 +67,6 @@ namespace Umbraco.Web.Common.Security PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); } - #region What we do not currently support - // We don't support an IUserClaimStore and don't need to (at least currently) public override bool SupportsUserClaim => false; @@ -83,8 +81,6 @@ namespace Umbraco.Web.Common.Security // We haven't needed to support this yet, though might be necessary for 2FA public override bool SupportsUserPhoneNumber => false; - #endregion - /// /// Replace the underlying options property with our own strongly typed version /// @@ -97,14 +93,18 @@ namespace Umbraco.Web.Common.Security /// /// Used to validate a user's session /// - /// - /// - /// + /// The user id + /// The sesion id + /// True if the sesion is valid, else false public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { var userSessionStore = Store as IUserSessionStore; - //if this is not set, for backwards compat (which would be super rare), we'll just approve it - if (userSessionStore == null) return true; + + // if this is not set, for backwards compat (which would be super rare), we'll just approve it + if (userSessionStore == null) + { + return true; + } return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); } @@ -112,12 +112,9 @@ namespace Umbraco.Web.Common.Security /// /// This will determine which password hasher to use based on what is defined in config /// - /// - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) - { - // we can use the user aware password hasher (which will be the default and preferred way) - return new PasswordHasher(); - } + /// The + /// An + protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); /// /// Gets/sets the default back office user password checker @@ -129,10 +126,14 @@ namespace Umbraco.Web.Common.Security /// /// Helper method to generate a password for a user based on the current password validator /// - /// + /// The generated password public string GeneratePassword() { - if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + if (_passwordGenerator == null) + { + _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + } + var password = _passwordGenerator.GeneratePassword(); return password; } @@ -160,14 +161,10 @@ namespace Umbraco.Web.Common.Security return await base.IsLockedOutAsync(user); } - #region Overrides for password logic - /// /// Logic used to validate a username and password /// - /// - /// - /// + /// /// /// By default this uses the standard ASP.Net Identity approach which is: /// * Get password store @@ -186,55 +183,61 @@ namespace Umbraco.Web.Common.Security { if (BackOfficeUserPasswordChecker != null) { - var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); + BackOfficeUserPasswordCheckerResult result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); if (user.HasIdentity == false) { return false; } - //if the result indicates to not fallback to the default, then return true if the credentials are valid + // if the result indicates to not fallback to the default, then return true if the credentials are valid if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) { return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; } } - //we cannot proceed if the user passed in does not have an identity + // we cannot proceed if the user passed in does not have an identity if (user.HasIdentity == false) + { return false; + } - //use the default behavior + // use the default behavior return await base.CheckPasswordAsync(user, password); } /// /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event /// - /// - /// - /// - /// + /// The userId + /// The reset password token + /// The new password to set it to + /// The /// /// We use this because in the back office the only way an admin can change another user's password without first knowing their password /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset /// public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) { - var user = await base.FindByIdAsync(userId.ToString()); - if (user == null) throw new InvalidOperationException("Could not find user"); + T user = await FindByIdAsync(userId.ToString()); + if (user == null) + { + throw new InvalidOperationException("Could not find user"); + } - var result = await base.ResetPasswordAsync(user, token, newPassword); + IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); } + return result; } public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) { - var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); + IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); @@ -245,20 +248,14 @@ namespace Umbraco.Web.Common.Security /// /// Override to determine how to hash the password /// - /// - /// - /// - /// - /// - /// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used) - /// + /// protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) { user.LastPasswordChangeDateUtc = DateTime.UtcNow; if (validatePassword) { - var validate = await ValidatePasswordAsync(user, newPassword); + IdentityResult validate = await ValidatePasswordAsync(user, newPassword); if (!validate.Succeeded) { return validate; @@ -266,7 +263,10 @@ namespace Umbraco.Web.Common.Security } var passwordStore = Store as IUserPasswordStore; - if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + if (passwordStore == null) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + } var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); @@ -277,41 +277,44 @@ namespace Umbraco.Web.Common.Security /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// - /// private async Task UpdateSecurityStampInternal(T user) { - if (SupportsUserSecurityStamp == false) return; + if (SupportsUserSecurityStamp == false) + { + return; + } + await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); } /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// private IUserSecurityStampStore GetSecurityStore() { var store = Store as IUserSecurityStampStore; - if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + if (store == null) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + } + return store; } /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - /// - private static string NewSecurityStamp() - { - return Guid.NewGuid().ToString(); - } - - #endregion + private static string NewSecurityStamp() => Guid.NewGuid().ToString(); + /// public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) { - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var result = await base.SetLockoutEndDateAsync(user, lockoutEnd); + IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); // The way we unlock is by setting the lockoutEnd date to the current datetime if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) @@ -321,25 +324,33 @@ namespace Umbraco.Web.Common.Security else { RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - //Resets the login attempt fails back to 0 when unlock is clicked + + // Resets the login attempt fails back to 0 when unlock is clicked await ResetAccessFailedCountAsync(user); } return result; } + /// public override async Task ResetAccessFailedCountAsync(T user) { - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } var lockoutStore = (IUserLockoutStore)Store; var accessFailedCount = await GetAccessFailedCountAsync(user); if (accessFailedCount == 0) + { return IdentityResult.Success; + } await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); - //raise the event now that it's reset + + // raise the event now that it's reset RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id); return await UpdateAsync(user); } @@ -347,33 +358,33 @@ namespace Umbraco.Web.Common.Security /// /// Overrides the Microsoft ASP.NET user management method /// - /// - /// - /// returns a Async Task - /// - /// - /// Doesn't set fail attempts back to 0 - /// + /// public override async Task AccessFailedAsync(T user) { - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } var lockoutStore = Store as IUserLockoutStore; - if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); + if (lockoutStore == null) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); + } var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); if (count >= Options.Lockout.MaxFailedAccessAttempts) { - await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), - CancellationToken.None); - //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 - //here we are persisting the value for the back office + await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None); + + // NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 + // here we are persisting the value for the back office } - var result = await UpdateAsync(user); + IdentityResult result = await UpdateAsync(user); - //Slightly confusing: this will return a Success if we successfully update the AccessFailed count + // Slightly confusing: this will return a Success if we successfully update the AccessFailed count if (result.Succeeded) { RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); @@ -384,16 +395,18 @@ namespace Umbraco.Web.Common.Security private int GetCurrentUserId(IPrincipal currentUser) { - var umbIdentity = currentUser?.GetUmbracoIdentity(); + UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserId; return currentUserId; } + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername) { var currentUserId = GetCurrentUserId(currentUser); var ip = IpResolver.GetCurrentRequestIpAddress(); return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, int affectedUserId, string affectedUsername) { var currentUserId = currentUser.Id; diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 4a9ebcaf46..590edf397a 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -38,8 +38,21 @@ namespace Umbraco.Web.BackOffice.Security private readonly IUserService _userService; private readonly IIpResolver _ipResolver; private readonly ISystemClock _systemClock; - private readonly LinkGenerator _linkGenerator; + /// + /// Initializes a new instance of the class. + /// + /// The + /// The + /// The options + /// The options + /// The + /// The + /// The + /// The + /// The + /// The + /// The public ConfigureBackOfficeCookieOptions( IServiceProvider serviceProvider, IUmbracoContextAccessor umbracoContextAccessor, @@ -51,8 +64,7 @@ namespace Umbraco.Web.BackOffice.Security IRequestCache requestCache, IUserService userService, IIpResolver ipResolver, - ISystemClock systemClock, - LinkGenerator linkGenerator) + ISystemClock systemClock) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -65,15 +77,20 @@ namespace Umbraco.Web.BackOffice.Security _userService = userService; _ipResolver = ipResolver; _systemClock = systemClock; - _linkGenerator = linkGenerator; } + /// public void Configure(string name, CookieAuthenticationOptions options) { - if (name != Constants.Security.BackOfficeAuthenticationType) return; + if (name != Constants.Security.BackOfficeAuthenticationType) + { + return; + } + Configure(options); } + /// public void Configure(CookieAuthenticationOptions options) { options.SlidingExpiration = true; @@ -94,21 +111,18 @@ namespace Umbraco.Web.BackOffice.Security // NOTE: This is borrowed directly from aspnetcore source // Note: the purpose for the data protector must remain fixed for interop to work. - var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", Constants.Security.BackOfficeAuthenticationType, "v2"); + IDataProtector dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", Constants.Security.BackOfficeAuthenticationType, "v2"); var ticketDataFormat = new TicketDataFormat(dataProtector); options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOutInMinutes, ticketDataFormat); - //Custom cookie manager so we can filter requests + // Custom cookie manager so we can filter requests options.CookieManager = new BackOfficeCookieManager( _umbracoContextAccessor, _runtimeState, _hostingEnvironment, _globalSettings, - _requestCache, - _linkGenerator); - // _explicitPaths); TODO: Implement this once we do OAuth somehow - + _requestCache); // _explicitPaths); TODO: Implement this once we do OAuth somehow options.Events = new CookieAuthenticationEvents { @@ -119,22 +133,22 @@ namespace Umbraco.Web.BackOffice.Security // It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else // our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because // the defaults work fine with our setup. - OnValidatePrincipal = async ctx => { // We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this) - var securityStampValidator = ctx.HttpContext.RequestServices.GetRequiredService(); - // Same goes for the signinmanager - var signInManager = ctx.HttpContext.RequestServices.GetRequiredService(); + BackOfficeSecurityStampValidator securityStampValidator = ctx.HttpContext.RequestServices.GetRequiredService(); - var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); + // Same goes for the signinmanager + IBackOfficeSignInManager signInManager = ctx.HttpContext.RequestServices.GetRequiredService(); + + UmbracoBackOfficeIdentity backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); if (backOfficeIdentity == null) { ctx.RejectPrincipal(); await signInManager.SignOutAsync(); } - //ensure the thread culture is set + // ensure the thread culture is set backOfficeIdentity.EnsureCulture(); await EnsureValidSessionId(ctx); @@ -154,19 +168,19 @@ namespace Umbraco.Web.BackOffice.Security OnSigningIn = ctx => { // occurs when sign in is successful but before the ticket is written to the outbound cookie - - var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); + UmbracoBackOfficeIdentity backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); if (backOfficeIdentity != null) { - //generate a session id and assign it - //create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one - var session = _runtimeState.Level == RuntimeLevel.Run + // generate a session id and assign it + // create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one + Guid session = _runtimeState.Level == RuntimeLevel.Run ? _userService.CreateLoginSession(backOfficeIdentity.Id, _ipResolver.GetCurrentRequestIpAddress()) : Guid.NewGuid(); - //add our session claim + // add our session claim backOfficeIdentity.AddClaim(new Claim(Constants.Security.SessionIdClaimType, session.ToString(), ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); - //since it is a cookie-based authentication add that claim + + // since it is a cookie-based authentication add that claim backOfficeIdentity.AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); } @@ -177,18 +191,18 @@ namespace Umbraco.Web.BackOffice.Security // occurs when sign in is successful and after the ticket is written to the outbound cookie // When we are signed in with the cookie, assign the principal to the current HttpContext - ctx.HttpContext.User = ctx.Principal; + ctx.HttpContext.User = ctx.Principal; return Task.CompletedTask; }, OnSigningOut = ctx => { - //Clear the user's session on sign out + // Clear the user's session on sign out if (ctx.HttpContext?.User?.Identity != null) { var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity; var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType); - if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out var guidSession)) + if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out Guid guidSession)) { _userService.ClearLoginSession(guidSession); } @@ -225,10 +239,13 @@ namespace Umbraco.Web.BackOffice.Security /// private async Task EnsureValidSessionId(CookieValidatePrincipalContext context) { - if (_runtimeState.Level != RuntimeLevel.Run) return; - - using var scope = _serviceProvider.CreateScope(); - var validator = scope.ServiceProvider.GetRequiredService(); + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + using IServiceScope scope = _serviceProvider.CreateScope(); + BackOfficeSessionIdValidator validator = scope.ServiceProvider.GetRequiredService(); await validator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } @@ -236,21 +253,24 @@ namespace Umbraco.Web.BackOffice.Security /// Ensures the ticket is renewed if the is set to true /// and the current request is for the get user seconds endpoint /// - /// + /// The private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context) { - if (!_securitySettings.KeepUserLoggedIn) return; + if (!_securitySettings.KeepUserLoggedIn) + { + return; + } - var currentUtc = _systemClock.UtcNow; - var issuedUtc = context.Properties.IssuedUtc; - var expiresUtc = context.Properties.ExpiresUtc; + DateTimeOffset currentUtc = _systemClock.UtcNow; + DateTimeOffset? issuedUtc = context.Properties.IssuedUtc; + DateTimeOffset? expiresUtc = context.Properties.ExpiresUtc; if (expiresUtc.HasValue && issuedUtc.HasValue) { - var timeElapsed = currentUtc.Subtract(issuedUtc.Value); - var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); + TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); - //if it's time to renew, then do it + // if it's time to renew, then do it if (timeRemaining < timeElapsed) { context.ShouldRenew = true; From de03dae46f9edcee7016163c20fd07b266faf026 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 3 Dec 2020 23:49:32 +1100 Subject: [PATCH 02/14] Moving namespaces, cleaning up some stuff on the underlying base identity classes --- .../Models/Identity/IIdentityUser.cs | 26 ++ .../Models/Identity/IIdentityUserLogin.cs | 15 +- .../Models/Identity/IdentityUser.cs | 71 ++-- .../Models/Identity/IdentityUserClaim.cs | 26 +- .../Models/Identity/IdentityUserLogin.cs | 2 +- .../Models/Identity/IdentityUserRole.cs | 19 +- .../Security/AuthenticationExtensions.cs | 1 - .../BackOfficeIdentityUser.cs | 51 +-- .../BackOfficeUserPasswordCheckerResult.cs | 2 +- .../ClaimsPrincipalExtensions.cs | 4 +- .../IBackOfficeUserPasswordChecker.cs | 4 +- .../IdentityAuditEventArgs.cs | 6 +- .../IdentityMapDefinition.cs | 6 +- .../UmbracoBackOfficeIdentity.cs | 38 +- .../BackOfficeClaimsPrincipalFactory.cs | 11 +- .../BackOffice/BackOfficeIdentityBuilder.cs | 1 + .../BackOffice/BackOfficeUserStore.cs | 5 +- .../BackOffice/BackOfficeUserValidator.cs | 1 + .../BackOffice/IBackOfficeUserManager.cs | 323 +---------------- .../BackOffice/IUmbracoUserManager.cs | 326 ++++++++++++++++++ .../CoreMappingProfiles.cs | 2 +- .../Security/SignOutAuditEventArgs.cs | 4 +- .../Security/UserInviteEventArgs.cs | 1 + .../TestServerTest/TestAuthHandler.cs | 2 +- ...kOfficeServiceCollectionExtensionsTests.cs | 1 + .../AutoFixture/AutoMoqDataAttribute.cs | 2 +- .../BackOfficeClaimsPrincipalFactoryTests.cs | 5 +- .../UmbracoBackOfficeIdentityTests.cs | 2 +- .../ClaimsPrincipalExtensionsTests.cs | 2 +- .../Controllers/UsersControllerUnitTests.cs | 1 + .../Security/BackOfficeAntiforgeryTests.cs | 28 +- .../OwinDataProtectorTokenProviderTests.cs | 2 +- .../AuthenticateEverythingMiddleware.cs | 4 +- .../TestControllerActivatorBase.cs | 1 - .../Extensions/IdentityBuilderExtensions.cs | 2 +- .../CheckIfUserTicketDataIsStaleAttribute.cs | 2 +- .../Security/BackOfficePasswordHasher.cs | 1 - .../Security/BackOfficeSecureDataFormat.cs | 2 +- .../BackOfficeSecurityStampValidator.cs | 2 +- .../Security/BackOfficeSignInManager.cs | 1 - .../Security/BackOfficeUserManager.cs | 1 + .../Security/BackOfficeUserManagerAuditer.cs | 1 + .../ConfigureBackOfficeCookieOptions.cs | 1 - .../Security/ExternalSignInAutoLinkOptions.cs | 2 +- .../Security/IBackOfficeSignInManager.cs | 2 +- .../Extensions/HttpContextExtensions.cs | 2 +- ...eDirectoryBackOfficeUserPasswordChecker.cs | 3 +- .../Security/AuthenticationExtensions.cs | 2 +- .../Security/BackOfficeSignInManager.cs | 1 + .../Security/FixWindowsAuthMiddlware.cs | 2 +- .../IBackOfficeUserPasswordChecker.cs | 2 +- .../OwinDataProtectorTokenProvider.cs | 2 +- .../Security/UmbracoSecureDataFormat.cs | 2 +- 53 files changed, 550 insertions(+), 478 deletions(-) create mode 100644 src/Umbraco.Core/Models/Identity/IIdentityUser.cs rename src/Umbraco.Core/{BackOffice => Security}/BackOfficeIdentityUser.cs (90%) rename src/Umbraco.Core/{BackOffice => Security}/BackOfficeUserPasswordCheckerResult.cs (87%) rename src/Umbraco.Core/{BackOffice => Security}/ClaimsPrincipalExtensions.cs (98%) rename src/Umbraco.Core/{BackOffice => Security}/IBackOfficeUserPasswordChecker.cs (93%) rename src/Umbraco.Core/{BackOffice => Security}/IdentityAuditEventArgs.cs (96%) rename src/Umbraco.Core/{BackOffice => Security}/IdentityMapDefinition.cs (97%) rename src/Umbraco.Core/{BackOffice => Security}/UmbracoBackOfficeIdentity.cs (85%) create mode 100644 src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUser.cs b/src/Umbraco.Core/Models/Identity/IIdentityUser.cs new file mode 100644 index 0000000000..fa7f52c710 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IIdentityUser.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Identity +{ + public interface IIdentityUser + { + int AccessFailedCount { get; set; } + //ICollection Claims { get; } + string Email { get; set; } + bool EmailConfirmed { get; set; } + TKey Id { get; set; } + DateTime? LastLoginDateUtc { get; set; } + DateTime? LastPasswordChangeDateUtc { get; set; } + bool LockoutEnabled { get; set; } + DateTime? LockoutEndDateUtc { get; set; } + //ICollection Logins { get; } + string PasswordHash { get; set; } + string PhoneNumber { get; set; } + bool PhoneNumberConfirmed { get; set; } + //ICollection Roles { get; } + string SecurityStamp { get; set; } + bool TwoFactorEnabled { get; set; } + string UserName { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs index cbe5b47b38..62c92de16d 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -1,27 +1,30 @@ -using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models.Identity { - + /// + /// An external login provider linked to a user + /// + /// The PK type for the user public interface IIdentityUserLogin : IEntity, IRememberBeingDirty { /// - /// The login provider for the login (i.e. Facebook, Google) + /// Gets or sets the login provider for the login (i.e. Facebook, Google) /// string LoginProvider { get; set; } /// - /// Key representing the login for the provider + /// Gets or sets key representing the login for the provider /// string ProviderKey { get; set; } /// - /// User Id for the user who owns this login + /// Gets or sets user Id for the user who owns this login /// int UserId { get; set; } /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// Gets or sets any arbitrary data for the user and external provider - like user tokens returned from the provider /// string UserData { get; set; } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index 093e42c1e7..dd3841d2c8 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -1,26 +1,30 @@ -using System; +using System; using System.Collections.Generic; namespace Umbraco.Core.Models.Identity { /// - /// Default IUser implementation + /// Abstract class for use in Umbraco Identity /// - /// + /// The type of user login + /// The type of user role + /// The type of user claims /// - /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want - /// references to that so we will create our own here + /// This class was originally borrowed from the EF implementation in Identity prior to netcore. + /// The new IdentityUser in netcore does not have properties such as Claims, Roles and Logins and those are instead + /// by default managed with their default user store backed by EF which utilizes EF's change tracking to track these values + /// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of + /// claims, roles and logins directly on the user model. /// - public class IdentityUser + public abstract class IdentityUser where TLogin : IIdentityUserLogin - //NOTE: Making our role id a string - where TRole : IdentityUserRole - where TClaim : IdentityUserClaim + where TRole : IdentityUserRole + where TClaim : IdentityUserClaim { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public IdentityUser() + protected IdentityUser() { Claims = new List(); Roles = new List(); @@ -28,87 +32,96 @@ namespace Umbraco.Core.Models.Identity } /// - /// Last login date + /// Gets or sets last login date /// public virtual DateTime? LastLoginDateUtc { get; set; } /// - /// Email + /// Gets or sets email /// public virtual string Email { get; set; } /// - /// True if the email is confirmed, default is false + /// Gets or sets a value indicating whether the email is confirmed, default is false /// public virtual bool EmailConfirmed { get; set; } /// - /// The salted/hashed form of the user password + /// Gets or sets the salted/hashed form of the user password /// public virtual string PasswordHash { get; set; } /// - /// A random value that should change whenever a users credentials have changed (password changed, login removed) + /// Gets or sets a random value that should change whenever a users credentials have changed (password changed, login removed) /// public virtual string SecurityStamp { get; set; } /// - /// PhoneNumber for the user + /// Gets or sets a phone Number for the user /// + /// + /// This is unused until we or an end-user requires this value for 2FA + /// public virtual string PhoneNumber { get; set; } /// - /// True if the phone number is confirmed, default is false + /// Gets or sets a value indicating whether true if the phone number is confirmed, default is false /// + /// + /// This is unused until we or an end-user requires this value for 2FA + /// public virtual bool PhoneNumberConfirmed { get; set; } /// - /// Is two factor enabled for the user + /// Gets or sets a value indicating whether is two factor enabled for the user /// + /// + /// This is unused until we or an end-user requires this value for 2FA + /// public virtual bool TwoFactorEnabled { get; set; } /// - /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// Gets or sets dateTime in UTC when lockout ends, any time in the past is considered not locked out. /// public virtual DateTime? LockoutEndDateUtc { get; set; } /// - /// DateTime in UTC when the password was last changed. + /// Gets or sets dateTime in UTC when the password was last changed. /// public virtual DateTime? LastPasswordChangeDateUtc { get; set; } /// - /// Is lockout enabled for this user + /// Gets or sets a value indicating whether is lockout enabled for this user /// public virtual bool LockoutEnabled { get; set; } /// - /// Used to record failures for the purposes of lockout + /// Gets or sets the value to record failures for the purposes of lockout /// public virtual int AccessFailedCount { get; set; } /// - /// Navigation property for user roles + /// Gets the user roles collection /// public virtual ICollection Roles { get; } /// - /// Navigation property for user claims + /// Gets navigation the user claims collection /// public virtual ICollection Claims { get; } /// - /// Navigation property for user logins + /// Gets the user logins collection /// public virtual ICollection Logins { get; } /// - /// User ID (Primary Key) + /// Gets or sets user ID (Primary Key) /// - public virtual TKey Id { get; set; } + public virtual int Id { get; set; } /// - /// User name + /// Gets or sets user name /// public virtual string UserName { get; set; } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs index e117d2fd13..2524463284 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs @@ -1,37 +1,27 @@ -namespace Umbraco.Core.Models.Identity +namespace Umbraco.Core.Models.Identity { /// /// EntityType that represents one specific user claim - /// /// - /// - /// - /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want - /// references to that so we will create our own here - /// - public class IdentityUserClaim + public class IdentityUserClaim { /// - /// Primary key - /// + /// Gets or sets primary key /// - public virtual int Id { get; set; } + public virtual string Id { get; set; } // TODO: Not used /// - /// User Id for the user who owns this login - /// + /// Gets or sets user Id for the user who owns this login /// - public virtual TKey UserId { get; set; } + public virtual string UserId { get; set; } /// - /// Claim type - /// + /// Gets or sets claim type /// public virtual string ClaimType { get; set; } /// - /// Claim value - /// + /// Gets or sets claim value /// public virtual string ClaimValue { get; set; } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index c13b28461d..18e8d4694b 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models.Identity diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs index ba9e87e46c..39ed65112d 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs @@ -1,26 +1,19 @@ -namespace Umbraco.Core.Models.Identity +namespace Umbraco.Core.Models.Identity { /// /// EntityType that represents a user belonging to a role - /// /// /// - /// - /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want - /// references to that so we will create our own here - /// - public class IdentityUserRole + public class IdentityUserRole { /// - /// UserId for the user that is in the role - /// + /// Gets or sets userId for the user that is in the role /// - public virtual TKey UserId { get; set; } + public virtual string UserId { get; set; } /// - /// RoleId for the role - /// + /// Gets or sets roleId for the role /// - public virtual TKey RoleId { get; set; } + public virtual string RoleId { get; set; } } } diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index edc11bcac2..607c4748cc 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Security.Principal; using System.Text; using System.Threading; -using Umbraco.Core.BackOffice; namespace Umbraco.Core.Security { diff --git a/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs similarity index 90% rename from src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs rename to src/Umbraco.Core/Security/BackOfficeIdentityUser.cs index 027e7c0904..e8e036b51b 100644 --- a/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -10,14 +10,13 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { - public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty + public class BackOfficeIdentityUser : IdentityUser, IRememberBeingDirty { private string _email; private string _userName; private int _id; - private bool _hasIdentity; private DateTime? _lastLoginDateUtc; private bool _emailConfirmed; private string _name; @@ -36,23 +35,32 @@ namespace Umbraco.Core.BackOffice /// /// Used to construct a new instance without an identity /// + /// /// /// This is allowed to be null (but would need to be filled in if trying to persist this instance) /// /// public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string username, string email, string culture, string name = null) { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + } + + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + } var user = new BackOfficeIdentityUser(globalSettings, Array.Empty()); user.DisableChangeTracking(); user._userName = username; user._email = email; - //we are setting minvalue here because the default is "0" which is the id of the admin user - //which we cannot allow because the admin user will always exist + + // we are setting minvalue here because the default is "0" which is the id of the admin user + // which we cannot allow because the admin user will always exist user._id = int.MinValue; - user._hasIdentity = false; + user.HasIdentity = false; user._culture = culture; user._name = name; user.EnableChangeTracking(); @@ -67,7 +75,7 @@ namespace Umbraco.Core.BackOffice _culture = globalSettings.DefaultUILanguage; // must initialize before setting groups - _roles = new ObservableCollection>(); + _roles = new ObservableCollection(); _roles.CollectionChanged += _roles_CollectionChanged; // use the property setters - they do more than just setting a field @@ -75,7 +83,7 @@ namespace Umbraco.Core.BackOffice } /// - /// Creates an existing user with the specified groups + /// Initializes a new instance of the class. /// /// /// @@ -90,7 +98,7 @@ namespace Umbraco.Core.BackOffice /// /// Returns true if an Id has been set on this object this will be false if the object is new and not persisted to the database /// - public bool HasIdentity => _hasIdentity; + public bool HasIdentity { get; private set; } public int[] CalculatedMediaStartNodeIds { get; set; } public int[] CalculatedContentStartNodeIds { get; set; } @@ -101,7 +109,7 @@ namespace Umbraco.Core.BackOffice set { _id = value; - _hasIdentity = true; + HasIdentity = true; } } @@ -192,7 +200,8 @@ namespace Umbraco.Core.BackOffice get => _startContentIds; set { - if (value == null) value = new int[0]; + if (value == null) + value = new int[0]; _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer); } } @@ -205,7 +214,8 @@ namespace Umbraco.Core.BackOffice get => _startMediaIds; set { - if (value == null) value = new int[0]; + if (value == null) + value = new int[0]; _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer); } } @@ -237,7 +247,7 @@ namespace Umbraco.Core.BackOffice //now clear all roles and re-add them _roles.CollectionChanged -= _roles_CollectionChanged; _roles.Clear(); - foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole + foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole { RoleId = x.Alias, UserId = Id.ToString() @@ -288,7 +298,8 @@ namespace Umbraco.Core.BackOffice get { // return if it exists - if (_logins != null) return _logins; + if (_logins != null) + return _logins; _logins = new ObservableCollection(); @@ -318,7 +329,7 @@ namespace Umbraco.Core.BackOffice _beingDirty.OnPropertyChanged(nameof(Roles)); } - private readonly ObservableCollection> _roles; + private readonly ObservableCollection _roles; /// /// helper method to easily add a role without having to deal with IdentityUserRole{T} @@ -329,7 +340,7 @@ namespace Umbraco.Core.BackOffice /// public void AddRole(string role) { - Roles.Add(new IdentityUserRole + Roles.Add(new IdentityUserRole { UserId = Id.ToString(), RoleId = role @@ -339,7 +350,7 @@ namespace Umbraco.Core.BackOffice /// /// Override Roles because the value of these are the user's group aliases /// - public override ICollection> Roles => _roles; + public override ICollection Roles => _roles; /// /// Used to set a lazy call back to populate the user's Login list diff --git a/src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs similarity index 87% rename from src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs rename to src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs index 7936fab682..c640c85d0c 100644 --- a/src/Umbraco.Core/BackOffice/BackOfficeUserPasswordCheckerResult.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// The result returned from the IBackOfficeUserPasswordChecker diff --git a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs similarity index 98% rename from src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs rename to src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs index 7cbca0428a..395465cfb7 100644 --- a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs similarity index 93% rename from src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs rename to src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs index 5874337f4a..45f5ea44e2 100644 --- a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily diff --git a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs similarity index 96% rename from src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs rename to src/Umbraco.Core/Security/IdentityAuditEventArgs.cs index 1d51c45074..454d651944 100644 --- a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs @@ -1,7 +1,7 @@ -using System; +using System; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// @@ -58,7 +58,7 @@ namespace Umbraco.Core.BackOffice DateTimeUtc = DateTime.UtcNow; Action = action; IpAddress = ipAddress; - Comment = comment; + Comment = comment; PerformingUser = performingUser; AffectedUsername = affectedUsername; AffectedUser = affectedUser; diff --git a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Core/Security/IdentityMapDefinition.cs similarity index 97% rename from src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs rename to src/Umbraco.Core/Security/IdentityMapDefinition.cs index 61fdf82d19..26a5d11f6e 100644 --- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/Security/IdentityMapDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; @@ -7,7 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { public class IdentityMapDefinition : IMapDefinition { @@ -65,7 +65,7 @@ namespace Umbraco.Core.BackOffice target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; - target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?) null; + target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; // this was in AutoMapper but does not have a setter anyways //target.AllowedSections = source.AllowedSections.ToArray(), diff --git a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs similarity index 85% rename from src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs rename to src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 9a60c5d64f..3430814f83 100644 --- a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// @@ -15,13 +15,12 @@ namespace Umbraco.Core.BackOffice // TODO: Ideally we remove this class and only deal with ClaimsIdentity as a best practice. All things relevant to our own // identity are part of claims. This class would essentially become extension methods on a ClaimsIdentity for resolving // values from it. - public static bool FromClaimsIdentity(ClaimsIdentity identity, out UmbracoBackOfficeIdentity backOfficeIdentity) { - //validate that all claims exist + // validate that all claims exist foreach (var t in RequiredBackOfficeIdentityClaimTypes) { - //if the identity doesn't have the claim, or the claim value is null + // if the identity doesn't have the claim, or the claim value is null if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace())) { backOfficeIdentity = null; @@ -59,11 +58,16 @@ namespace Umbraco.Core.BackOffice string securityStamp, IEnumerable allowedApps, IEnumerable roles) : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true { - if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); + if (allowedApps == null) + throw new ArgumentNullException(nameof(allowedApps)); + if (string.IsNullOrWhiteSpace(username)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(realName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); + if (string.IsNullOrWhiteSpace(culture)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(securityStamp)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles); } @@ -88,10 +92,14 @@ namespace Umbraco.Core.BackOffice string securityStamp, IEnumerable allowedApps, IEnumerable roles) : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType) { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); + if (string.IsNullOrWhiteSpace(username)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(realName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); + if (string.IsNullOrWhiteSpace(culture)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(securityStamp)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles); } @@ -205,7 +213,7 @@ namespace Umbraco.Core.BackOffice public string SecurityStamp => this.FindFirstValue(Constants.Security.SecurityStampClaimType); - public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); + public string[] Roles => FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); /// /// Overridden to remove any temporary claims that shouldn't be copied diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs index 22ea4423d2..380ed452d0 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using Umbraco.Core.Security; namespace Umbraco.Core.BackOffice { @@ -36,11 +37,13 @@ namespace Umbraco.Core.BackOffice ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); + // TODO: How to flow claims then? This is most likely built into aspnetcore now and this is not the way + // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - foreach (Models.Identity.IdentityUserClaim claim in user.Claims) - { - baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); - } + //foreach (Models.Identity.IdentityUserClaim claim in user.Claims) + //{ + // baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); + //} // TODO: We want to remove UmbracoBackOfficeIdentity and only rely on ClaimsIdentity, once // that is done then we'll create a ClaimsIdentity with all of the requirements here instead diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs index 5bae03cad6..90c2823122 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs @@ -3,6 +3,7 @@ using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Infrastructure.BackOffice { diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index b271f5aa41..e297eca86d 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -13,6 +13,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Scoping; +using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Core.BackOffice @@ -23,7 +24,7 @@ namespace Umbraco.Core.BackOffice IUserLoginStore, IUserRoleStore, IUserSecurityStampStore, - IUserLockoutStore, + IUserLockoutStore, IUserSessionStore // TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs index 131bd08ac9..b7cbb7555d 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Security; namespace Umbraco.Core.BackOffice { diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs index c026c256f5..be4bd194f9 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs @@ -1,324 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Models.Membership; -using Umbraco.Web.Models.ContentEditing; +using Umbraco.Core.Security; namespace Umbraco.Core.BackOffice { - public interface IBackOfficeUserManager : IBackOfficeUserManager + /// + /// The user manager for the back office + /// + public interface IBackOfficeUserManager : IUmbracoUserManager { - - } - public interface IBackOfficeUserManager: IDisposable - where TUser : BackOfficeIdentityUser - { - Task GetUserIdAsync(TUser user); - - Task GetUserAsync(ClaimsPrincipal principal); - - string GetUserId(ClaimsPrincipal principal); - - Task> GetLoginsAsync(TUser user); - - Task DeleteAsync(TUser user); - - Task FindByLoginAsync(string loginProvider, string providerKey); - - /// - /// Finds and returns a user, if any, who has the specified . - /// - /// The user ID to search for. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - Task FindByIdAsync(string userId); - - /// - /// Generates a password reset token for the specified , using - /// the configured password reset token provider. - /// - /// The user to generate a password reset token for. - /// The that represents the asynchronous operation, - /// containing a password reset token for the specified . - Task GeneratePasswordResetTokenAsync(TUser user); - - /// - /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event - /// - /// - /// - /// - /// - /// - /// We use this because in the back office the only way an admin can change another user's password without first knowing their password - /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset - /// - Task ChangePasswordWithResetAsync(int userId, string token, string newPassword); - - /// - /// Validates that an email confirmation token matches the specified . - /// - /// The user to validate the token against. - /// The email confirmation token to validate. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task ConfirmEmailAsync(TUser user, string token); - - /// - /// Gets the user, if any, associated with the normalized value of the specified email address. - /// Note: Its recommended that identityOptions.User.RequireUniqueEmail be set to true when using this method, otherwise - /// the store may throw if there are users with duplicate emails. - /// - /// The email address to return the user for. - /// - /// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address. - /// - Task FindByEmailAsync(string email); - - /// - /// Resets the 's password to the specified after - /// validating the given password reset . - /// - /// The user whose password should be reset. - /// The password reset token to verify. - /// The new password to set if reset token verification succeeds. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task ResetPasswordAsync(TUser user, string token, string newPassword); - - /// - /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date - /// - /// - /// - /// - /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values - /// - Task IsLockedOutAsync(TUser user); - - /// - /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. - /// - /// The user whose lockout date should be set. - /// The after which the 's lockout should end. - /// The that represents the asynchronous operation, containing the of the operation. - Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd); - - /// - /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise - /// false. - /// - /// The user whose email confirmation status should be returned. - /// - /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified - /// has been confirmed or not. - /// - Task IsEmailConfirmedAsync(TUser user); - - /// - /// Updates the specified in the backing store. - /// - /// The user to update. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task UpdateAsync(TUser user); - - /// - /// Returns a flag indicating whether the specified is valid for - /// the given and . - /// - /// The user to validate the token against. - /// The token provider used to generate the token. - /// The purpose the token should be generated for. - /// The token to validate - /// - /// The that represents the asynchronous operation, returning true if the - /// is valid, otherwise false. - /// - Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, - string token); - - /// - /// Adds the to the specified only if the user - /// does not already have a password. - /// - /// The user whose password should be set. - /// The password to set. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task AddPasswordAsync(TUser user, string password); - - - /// - /// Returns a flag indicating whether the given is valid for the - /// specified . - /// - /// The user whose password should be validated. - /// The password to validate - /// The that represents the asynchronous operation, containing true if - /// the specified matches the one store for the , - /// otherwise false. - Task CheckPasswordAsync(TUser user, string password); - - /// - /// Changes a user's password after confirming the specified is correct, - /// as an asynchronous operation. - /// - /// The user whose password should be set. - /// The current password to validate before changing. - /// The new password to set for the specified . - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task ChangePasswordAsync(TUser user, string currentPassword, - string newPassword); - - /// - /// Used to validate a user's session - /// - /// - /// - /// - Task ValidateSessionIdAsync(string userId, string sessionId); - - /// - /// Creates the specified in the backing store with no password, - /// as an asynchronous operation. - /// - /// The user to create. - /// - /// The that represents the asynchronous operation, containing the - /// of the operation. - /// - Task CreateAsync(TUser user); - - /// - /// Helper method to generate a password for a user based on the current password validator - /// - /// - string GeneratePassword(); - - - /// - /// Generates an email confirmation token for the specified user. - /// - /// The user to generate an email confirmation token for. - /// - /// The that represents the asynchronous operation, an email confirmation token. - /// - Task GenerateEmailConfirmationTokenAsync(TUser user); - - /// - /// Finds and returns a user, if any, who has the specified user name. - /// - /// The user name to search for. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - Task FindByNameAsync(string userName); - - /// - /// Increments the access failed count for the user as an asynchronous operation. - /// If the failed access account is greater than or equal to the configured maximum number of attempts, - /// the user will be locked out for the configured lockout time span. - /// - /// The user whose failed access count to increment. - /// The that represents the asynchronous operation, containing the of the operation. - Task AccessFailedAsync(TUser user); - - /// - /// Returns a flag indicating whether the specified has two factor authentication enabled or not, - /// as an asynchronous operation. - /// - /// The user whose two factor authentication enabled status should be retrieved. - /// - /// The that represents the asynchronous operation, true if the specified - /// has two factor authentication enabled, otherwise false. - /// - Task GetTwoFactorEnabledAsync(TUser user); - - /// - /// Gets a list of valid two factor token providers for the specified , - /// as an asynchronous operation. - /// - /// The user the whose two factor authentication providers will be returned. - /// - /// The that represents result of the asynchronous operation, a list of two - /// factor authentication providers for the specified user. - /// - Task> GetValidTwoFactorProvidersAsync(TUser user); - - /// - /// Verifies the specified two factor authentication against the . - /// - /// The user the token is supposed to be for. - /// The provider which will verify the token. - /// The token to verify. - /// - /// The that represents result of the asynchronous operation, true if the token is valid, - /// otherwise false. - /// - Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token); - - /// - /// Adds an external Microsoft.AspNetCore.Identity.UserLoginInfo to the specified user. - /// - /// The user to add the login to. - /// The external Microsoft.AspNetCore.Identity.UserLoginInfo to add to the specified user. - /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation. - Task AddLoginAsync(TUser user, UserLoginInfo login); - - /// - /// Attempts to remove the provided external login information from the specified user. and returns a flag indicating whether the removal succeed or not. - /// - /// The user to remove the login information from. - /// The login provide whose information should be removed. - /// The key given by the external login provider for the specified user. - /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation. - Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey); - - Task ResetAccessFailedCountAsync(TUser user); - - Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider); - - /// - /// Gets the email address for the specified user. - /// - /// The user whose email should be returned. - /// The task object containing the results of the asynchronous operation, the email address for the specified user. - Task GetEmailAsync(TUser user); - - /// - /// Gets the telephone number, if any, for the specified user. - /// - /// The user whose telephone number should be retrieved. - /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the user's telephone number, if any. - /// - /// A user can only support a phone number if the BackOfficeUserStore is replaced with another that implements IUserPhoneNumberStore - /// - Task GetPhoneNumberAsync(TUser user); - - // TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers, - // let's see if there's a way to avoid that and only have these called within signinmanager and usermanager - // which means we can remove these from the interface (things like invite seems like they cannot be moved) - void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); - void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); - SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); - UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser); - bool HasSendingUserInviteEventHandler { get; } - } } diff --git a/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs new file mode 100644 index 0000000000..8f8e0ffc50 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Core.BackOffice +{ + + /// + /// A user manager for Umbraco (either back office users or front-end members) + /// + /// The type of user + public interface IUmbracoUserManager : IDisposable + where TUser : BackOfficeIdentityUser + { + Task GetUserIdAsync(TUser user); + + Task GetUserAsync(ClaimsPrincipal principal); + + string GetUserId(ClaimsPrincipal principal); + + Task> GetLoginsAsync(TUser user); + + Task DeleteAsync(TUser user); + + Task FindByLoginAsync(string loginProvider, string providerKey); + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + Task FindByIdAsync(string userId); + + /// + /// Generates a password reset token for the specified , using + /// the configured password reset token provider. + /// + /// The user to generate a password reset token for. + /// The that represents the asynchronous operation, + /// containing a password reset token for the specified . + Task GeneratePasswordResetTokenAsync(TUser user); + + /// + /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event + /// + /// + /// + /// + /// + /// + /// We use this because in the back office the only way an admin can change another user's password without first knowing their password + /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset + /// + Task ChangePasswordWithResetAsync(int userId, string token, string newPassword); + + /// + /// Validates that an email confirmation token matches the specified . + /// + /// The user to validate the token against. + /// The email confirmation token to validate. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ConfirmEmailAsync(TUser user, string token); + + /// + /// Gets the user, if any, associated with the normalized value of the specified email address. + /// Note: Its recommended that identityOptions.User.RequireUniqueEmail be set to true when using this method, otherwise + /// the store may throw if there are users with duplicate emails. + /// + /// The email address to return the user for. + /// + /// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address. + /// + Task FindByEmailAsync(string email); + + /// + /// Resets the 's password to the specified after + /// validating the given password reset . + /// + /// The user whose password should be reset. + /// The password reset token to verify. + /// The new password to set if reset token verification succeeds. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ResetPasswordAsync(TUser user, string token, string newPassword); + + /// + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date + /// + /// + /// + /// + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values + /// + Task IsLockedOutAsync(TUser user); + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The that represents the asynchronous operation, containing the of the operation. + Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd); + + /// + /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise + /// false. + /// + /// The user whose email confirmation status should be returned. + /// + /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified + /// has been confirmed or not. + /// + Task IsEmailConfirmedAsync(TUser user); + + /// + /// Updates the specified in the backing store. + /// + /// The user to update. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task UpdateAsync(TUser user); + + /// + /// Returns a flag indicating whether the specified is valid for + /// the given and . + /// + /// The user to validate the token against. + /// The token provider used to generate the token. + /// The purpose the token should be generated for. + /// The token to validate + /// + /// The that represents the asynchronous operation, returning true if the + /// is valid, otherwise false. + /// + Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, + string token); + + /// + /// Adds the to the specified only if the user + /// does not already have a password. + /// + /// The user whose password should be set. + /// The password to set. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task AddPasswordAsync(TUser user, string password); + + + /// + /// Returns a flag indicating whether the given is valid for the + /// specified . + /// + /// The user whose password should be validated. + /// The password to validate + /// The that represents the asynchronous operation, containing true if + /// the specified matches the one store for the , + /// otherwise false. + Task CheckPasswordAsync(TUser user, string password); + + /// + /// Changes a user's password after confirming the specified is correct, + /// as an asynchronous operation. + /// + /// The user whose password should be set. + /// The current password to validate before changing. + /// The new password to set for the specified . + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task ChangePasswordAsync(TUser user, string currentPassword, + string newPassword); + + /// + /// Used to validate a user's session + /// + /// + /// + /// + Task ValidateSessionIdAsync(string userId, string sessionId); + + /// + /// Creates the specified in the backing store with no password, + /// as an asynchronous operation. + /// + /// The user to create. + /// + /// The that represents the asynchronous operation, containing the + /// of the operation. + /// + Task CreateAsync(TUser user); + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// + string GeneratePassword(); + + + /// + /// Generates an email confirmation token for the specified user. + /// + /// The user to generate an email confirmation token for. + /// + /// The that represents the asynchronous operation, an email confirmation token. + /// + Task GenerateEmailConfirmationTokenAsync(TUser user); + + /// + /// Finds and returns a user, if any, who has the specified user name. + /// + /// The user name to search for. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + Task FindByNameAsync(string userName); + + /// + /// Increments the access failed count for the user as an asynchronous operation. + /// If the failed access account is greater than or equal to the configured maximum number of attempts, + /// the user will be locked out for the configured lockout time span. + /// + /// The user whose failed access count to increment. + /// The that represents the asynchronous operation, containing the of the operation. + Task AccessFailedAsync(TUser user); + + /// + /// Returns a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be retrieved. + /// + /// The that represents the asynchronous operation, true if the specified + /// has two factor authentication enabled, otherwise false. + /// + Task GetTwoFactorEnabledAsync(TUser user); + + /// + /// Gets a list of valid two factor token providers for the specified , + /// as an asynchronous operation. + /// + /// The user the whose two factor authentication providers will be returned. + /// + /// The that represents result of the asynchronous operation, a list of two + /// factor authentication providers for the specified user. + /// + Task> GetValidTwoFactorProvidersAsync(TUser user); + + /// + /// Verifies the specified two factor authentication against the . + /// + /// The user the token is supposed to be for. + /// The provider which will verify the token. + /// The token to verify. + /// + /// The that represents result of the asynchronous operation, true if the token is valid, + /// otherwise false. + /// + Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token); + + /// + /// Adds an external Microsoft.AspNetCore.Identity.UserLoginInfo to the specified user. + /// + /// The user to add the login to. + /// The external Microsoft.AspNetCore.Identity.UserLoginInfo to add to the specified user. + /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation. + Task AddLoginAsync(TUser user, UserLoginInfo login); + + /// + /// Attempts to remove the provided external login information from the specified user. and returns a flag indicating whether the removal succeed or not. + /// + /// The user to remove the login information from. + /// The login provide whose information should be removed. + /// The key given by the external login provider for the specified user. + /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation. + Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey); + + Task ResetAccessFailedCountAsync(TUser user); + + Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider); + + /// + /// Gets the email address for the specified user. + /// + /// The user whose email should be returned. + /// The task object containing the results of the asynchronous operation, the email address for the specified user. + Task GetEmailAsync(TUser user); + + /// + /// Gets the telephone number, if any, for the specified user. + /// + /// The user whose telephone number should be retrieved. + /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the user's telephone number, if any. + /// + /// A user can only support a phone number if the BackOfficeUserStore is replaced with another that implements IUserPhoneNumberStore + /// + Task GetPhoneNumberAsync(TUser user); + + // TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers, + // let's see if there's a way to avoid that and only have these called within signinmanager and usermanager + // which means we can remove these from the interface (things like invite seems like they cannot be moved) + void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); + void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); + SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser); + bool HasSendingUserInviteEventHandler { get; } + + } +} diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs index 04f715c7c0..ed45f24a96 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.BackOffice; using Umbraco.Core.Builder; using Umbraco.Core.Mapping; +using Umbraco.Core.Security; using Umbraco.Web.Models.Mapping; namespace Umbraco.Core.Composing.CompositionExtensions diff --git a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index 961c2e6137..34bd9c9a42 100644 --- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.BackOffice +using Umbraco.Core.Security; + +namespace Umbraco.Core.BackOffice { /// diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 4e980b7bb1..2aefb47c14 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -1,4 +1,5 @@ using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Core.BackOffice diff --git a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs index b9acd9529c..ab5821c81c 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Common.Security; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 26c3f7875c..b6a86344a2 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Extensions; using Umbraco.Core.BackOffice; using Umbraco.Tests.Integration.Testing; +using Umbraco.Core.Security; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs index 78d5d5554c..365dca780c 100644 --- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs +++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs @@ -10,10 +10,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Moq; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; +using Umbraco.Core.Security; using Umbraco.Tests.Common.Builders; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Routing; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index 5291c1b12e..9d8edbc75e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -10,6 +10,7 @@ using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Extensions; namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice @@ -98,7 +99,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice const string expectedClaimType = ClaimTypes.Role; const string expectedClaimValue = "b87309fb-4caf-48dc-b45a-2b752d051508"; - _testUser.Roles.Add(new global::Umbraco.Core.Models.Identity.IdentityUserRole{RoleId = expectedClaimValue}); + _testUser.Roles.Add(new global::Umbraco.Core.Models.Identity.IdentityUserRole{RoleId = expectedClaimValue}); _mockUserManager.Setup(x => x.SupportsUserRole).Returns(true); _mockUserManager.Setup(x => x.GetRolesAsync(_testUser)).ReturnsAsync(new[] {expectedClaimValue}); @@ -115,7 +116,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice const string expectedClaimType = "custom"; const string expectedClaimValue = "val"; - _testUser.Claims.Add(new global::Umbraco.Core.Models.Identity.IdentityUserClaim {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); + _testUser.Claims.Add(new global::Umbraco.Core.Models.Identity.IdentityUserClaim {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); _mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true); _mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync( new List {new Claim(expectedClaimType, expectedClaimValue)}); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 9e9d29a123..8dcaafafcb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Security.Claims; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs index a078456f8f..30706b1b67 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Security.Claims; using Umbraco.Extensions; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs index b04a5ff158..6ecda57cc6 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; using Umbraco.Tests.UnitTests.AutoFixture; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Exceptions; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs index d93bc01b4e..ccebe17b09 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs @@ -1,19 +1,15 @@ -using Microsoft.AspNetCore.Antiforgery; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -using Moq; using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; using Umbraco.Web.BackOffice.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Security @@ -25,8 +21,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Security { var httpContext = new DefaultHttpContext() { - User = new ClaimsPrincipal(new UmbracoBackOfficeIdentity(-1, "test", "test", Enumerable.Empty(), Enumerable.Empty(), "en-US", - Guid.NewGuid().ToString(), Enumerable.Empty(), Enumerable.Empty())) + User = new ClaimsPrincipal(new UmbracoBackOfficeIdentity( + -1, + "test", + "test", + Enumerable.Empty(), + Enumerable.Empty(), + "en-US", + Guid.NewGuid().ToString(), + Enumerable.Empty(), + Enumerable.Empty())) }; httpContext.Request.IsHttps = true; return httpContext; diff --git a/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs index 66965ca632..c44844fd66 100644 --- a/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs +++ b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs @@ -6,10 +6,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Owin.Security.DataProtection; using Moq; using NUnit.Framework; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Tests.Common.Builders; using Umbraco.Web.Security; diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs index 48ffdbcdec..3673bdf333 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; using Owin; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Tests.TestHelpers.ControllerTesting { diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 23f7e09f5d..f993ee5b6a 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -6,7 +6,6 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using Moq; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs index ddf46a24a7..e6385e6bf9 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index 9cfaae6980..a770a01e4d 100644 --- a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -7,12 +7,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.BackOffice.Security; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs index 6c61e7bb35..65f1a7f5bc 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficePasswordHasher.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Identity; -using Umbraco.Core.BackOffice; using Umbraco.Core.Security; using Umbraco.Core; using Umbraco.Core.Models.Membership; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs index 91b982b5f6..377801a0b7 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authentication; using System; using System.Security.Claims; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.BackOffice.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs index f12b6279bb..abd0af1353 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecurityStampValidator.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; using Umbraco.Web.Common.Security; namespace Umbraco.Web.BackOffice.Security diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index e17067daa0..6d1c348d7f 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs index 2906d9d87a..cc9b9410a6 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs @@ -65,6 +65,7 @@ namespace Umbraco.Web.Common.Security IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); _httpContextAccessor = httpContextAccessor; PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + } // We don't support an IUserClaimStore and don't need to (at least currently) diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index 019eed7e39..5f0757ea9c 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -6,6 +6,7 @@ using Umbraco.Core.BackOffice; using Umbraco.Core.Compose; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Web.Common.Security diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 590edf397a..9f90395ff3 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; diff --git a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs index 8d9f57945b..8636d9e62d 100644 --- a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Identity; using System; using System.Runtime.Serialization; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Security; using SecurityConstants = Umbraco.Core.Constants.Security; namespace Umbraco.Web.BackOffice.Security diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs index ce87484b2c..669ca21239 100644 --- a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.Common.Security { diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index 15d3d04c0b..f484ddac18 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -5,7 +5,7 @@ using System.Security.Claims; using System.Security.Principal; using System.Text; using Microsoft.AspNetCore.Http.Features; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 82c9cb8496..8071af2f5d 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.DirectoryServices.AccountManagement; using System.Threading.Tasks; using Microsoft.Extensions.Options; @@ -9,6 +9,7 @@ using Umbraco.Core.Configuration.Models; namespace Umbraco.Web.Security { // TODO: This relies on an assembly that is not .NET Standard (at least not at the time of implementation) :( + // TODO: This could be ported now, see https://stackoverflow.com/questions/37330705/working-with-directoryservices-in-asp-net-core public class ActiveDirectoryBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker { private readonly IOptions _activeDirectorySettings; diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs index de5abf8a6b..aa0cd6aca2 100644 --- a/src/Umbraco.Web/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs @@ -12,8 +12,8 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Web.Composing; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index e5ba931b0b..010c2d4d33 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -10,6 +10,7 @@ using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs index 9e26964091..3338344e73 100644 --- a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs +++ b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs @@ -4,7 +4,7 @@ using System.Security.Principal; using System.Threading.Tasks; using Microsoft.Owin; using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs index 2fae308eb0..7bd67e608a 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs index 72e12b8621..429014dea8 100644 --- a/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs +++ b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Owin.Security.DataProtection; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs b/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs index 73c1c3fd55..d1b0c54279 100644 --- a/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs +++ b/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Owin.Security; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { From 90b7ee3f377436989c3d4b07124557cfcc820972 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 3 Dec 2020 23:55:10 +1100 Subject: [PATCH 03/14] oops broke build --- .../Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 8071af2f5d..901e7bf81b 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { From c51ed88d56aecb3de4fabdb4b8b54f7df5138d9c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 00:20:48 +1100 Subject: [PATCH 04/14] Adds notes for external login service/repo, changes IdentityUserLogin user id to string for now so it can be shared with members/users --- .../Models/Identity/IIdentityUserLogin.cs | 2 +- .../Models/Identity/IdentityUserLogin.cs | 6 +- .../Models/Identity/IdentityUserRole.cs | 2 +- .../Security/BackOfficeIdentityUser.cs | 8 +-- .../BackOffice/BackOfficeUserStore.cs | 24 ++++---- .../BackOffice/IUmbracoUserManager.cs | 56 ++++++++++++++++--- .../Persistence/Dtos/UserDto.cs | 4 +- .../Factories/ExternalLoginFactory.cs | 6 +- .../Implement/ExternalLoginRepository.cs | 4 +- .../Implement/ExternalLoginService.cs | 5 +- .../Services/ExternalLoginServiceTests.cs | 8 +-- 11 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs index 62c92de16d..05703a1b2c 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets user Id for the user who owns this login /// - int UserId { get; set; } + string UserId { get; set; } // TODO: This should be able to be used by both users and members /// /// Gets or sets any arbitrary data for the user and external provider - like user tokens returned from the provider diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index 18e8d4694b..1ae19da128 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -9,14 +9,14 @@ namespace Umbraco.Core.Models.Identity /// public class IdentityUserLogin : EntityBase, IIdentityUserLogin { - public IdentityUserLogin(string loginProvider, string providerKey, int userId) + public IdentityUserLogin(string loginProvider, string providerKey, string userId) { LoginProvider = loginProvider; ProviderKey = providerKey; UserId = userId; } - public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate) + public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) { Id = id; LoginProvider = loginProvider; @@ -32,7 +32,7 @@ namespace Umbraco.Core.Models.Identity public string ProviderKey { get; set; } /// - public int UserId { get; set; } + public string UserId { get; set; } /// public string UserData { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs index 39ed65112d..8a0b6b891d 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets userId for the user that is in the role /// - public virtual string UserId { get; set; } + public virtual int UserId { get; set; } /// /// Gets or sets roleId for the role diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs index e8e036b51b..07811f39e1 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs @@ -239,18 +239,18 @@ namespace Umbraco.Core.Security get => _groups; set { - //so they recalculate + // so they recalculate _allowedSections = null; _groups = value; - //now clear all roles and re-add them + // now clear all roles and re-add them _roles.CollectionChanged -= _roles_CollectionChanged; _roles.Clear(); foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole { RoleId = x.Alias, - UserId = Id.ToString() + UserId = Id })) { _roles.Add(identityUserRole); @@ -342,7 +342,7 @@ namespace Umbraco.Core.Security { Roles.Add(new IdentityUserRole { - UserId = Id.ToString(), + UserId = Id, RoleId = role }); } diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index e297eca86d..8e980e7598 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -417,7 +417,7 @@ namespace Umbraco.Core.BackOffice if (login == null) throw new ArgumentNullException(nameof(login)); var logins = user.Logins; - var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); + var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString()); var userLogin = instance; logins.Add(userLogin); @@ -462,25 +462,29 @@ namespace Umbraco.Core.BackOffice /// /// 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(loginProvider, providerKey).ToArray(); + // get all logins associated with the login id + IIdentityUserLogin[] result = _externalLoginService.Find(loginProvider, providerKey).ToArray(); if (result.Any()) { - //return the first user that matches the result + // return the first user that matches the result BackOfficeIdentityUser output = null; - foreach (var l in result) + foreach (IIdentityUserLogin l in result) { - var user = _userService.GetUserById(l.UserId); - if (user != null) + // TODO: This won't be necessary once we add GUID support for users and make the external login + // table uses GUIDs without referential integrity + if (int.TryParse(l.UserId, out int userId)) { - output = _mapper.Map(user); - break; + IUser user = _userService.GetUserById(userId); + if (user != null) + { + output = _mapper.Map(user); + break; + } } } diff --git a/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs index 8f8e0ffc50..803f64e0e6 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs @@ -18,16 +18,47 @@ namespace Umbraco.Core.BackOffice public interface IUmbracoUserManager : IDisposable where TUser : BackOfficeIdentityUser { + /// + /// Gets the user id of a user + /// + /// The user + /// A representing the result of the asynchronous operation. Task GetUserIdAsync(TUser user); + /// + /// Get the from a + /// + /// The + /// A representing the result of the asynchronous operation. Task GetUserAsync(ClaimsPrincipal principal); + /// + /// Get the user id from the + /// + /// the + /// Returns the user id from the string GetUserId(ClaimsPrincipal principal); + /// + /// Gets the external logins for the user + /// + /// + /// A representing the result of the asynchronous operation. Task> GetLoginsAsync(TUser user); + /// + /// Deletes a user + /// + /// + /// A representing the result of the asynchronous operation. Task DeleteAsync(TUser user); + /// + /// Finds a user by the external login provider + /// + /// + /// + /// A representing the result of the asynchronous operation. Task FindByLoginAsync(string loginProvider, string providerKey); /// @@ -147,8 +178,7 @@ namespace Umbraco.Core.BackOffice /// The that represents the asynchronous operation, returning true if the /// is valid, otherwise false. /// - Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, - string token); + Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, string token); /// /// Adds the to the specified only if the user @@ -185,15 +215,14 @@ namespace Umbraco.Core.BackOffice /// The that represents the asynchronous operation, containing the /// of the operation. /// - Task ChangePasswordAsync(TUser user, string currentPassword, - string newPassword); + Task ChangePasswordAsync(TUser user, string currentPassword, string newPassword); /// /// Used to validate a user's session /// /// /// - /// + /// Returns true if the session is valid, otherwise false Task ValidateSessionIdAsync(string userId, string sessionId); /// @@ -208,12 +237,11 @@ namespace Umbraco.Core.BackOffice Task CreateAsync(TUser user); /// - /// Helper method to generate a password for a user based on the current password validator + /// Generate a password for a user based on the current password validator /// - /// + /// A generated password string GeneratePassword(); - /// /// Generates an email confirmation token for the specified user. /// @@ -292,8 +320,19 @@ namespace Umbraco.Core.BackOffice /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the Microsoft.AspNetCore.Identity.IdentityResult of the operation. Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey); + /// + /// Resets the access failed count for the user + /// + /// + /// A representing the result of the asynchronous operation. Task ResetAccessFailedCountAsync(TUser user); + /// + /// Generates a two factor token for the user + /// + /// + /// + /// A representing the result of the asynchronous operation. Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider); /// @@ -316,6 +355,7 @@ namespace Umbraco.Core.BackOffice // TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers, // let's see if there's a way to avoid that and only have these called within signinmanager and usermanager // which means we can remove these from the interface (things like invite seems like they cannot be moved) + // TODO: When we change to not having the crappy static events this will need to be revisited void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 028b760ba5..46bec34a49 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -19,6 +19,8 @@ namespace Umbraco.Core.Persistence.Dtos UserStartNodeDtos = new HashSet(); } + // TODO: We need to add a GUID for users and track external logins with that instead of the INT + [Column("id")] [PrimaryKeyColumn(Name = "PK_user")] public int Id { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 74d2fe7ff0..aa4b20aa40 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Core.Models.Identity; using Umbraco.Core.Persistence.Dtos; @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories { public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) { - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate) + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId.ToString(), dto.CreateDate) { UserData = dto.UserData }; @@ -26,7 +26,7 @@ namespace Umbraco.Core.Persistence.Factories CreateDate = entity.CreateDate, LoginProvider = entity.LoginProvider, ProviderKey = entity.ProviderKey, - UserId = entity.UserId, + UserId = int.Parse(entity.UserId), // TODO: This is temp until we change the ext logins to use GUIDs UserData = entity.UserData }; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 33fd3af7fc..c3ed111ffb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -13,6 +13,8 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement { + // TODO: We should update this to support both users and members. It means we would remove referential integrity from users + // and the user/member key would be a GUID (we also need to add a GUID to users) internal class ExternalLoginRepository : NPocoRepositoryBase, IExternalLoginRepository { public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs index fabbfea1d4..5edbe77cdb 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -25,7 +25,8 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _externalLoginRepository.Get(Query().Where(x => x.UserId == userId)) + var asString = userId.ToString(); // TODO: This is temp until we update the external service to support guids for both users and members + return _externalLoginRepository.Get(Query().Where(x => x.UserId == asString)) .ToList(); } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index 429e1953f7..192971f405 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using NUnit.Framework; @@ -93,7 +93,7 @@ namespace Umbraco.Tests.Services var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); UserService.Save(user); - var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) + var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id.ToString()) { UserData = "hello" }; @@ -112,7 +112,7 @@ namespace Umbraco.Tests.Services var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); UserService.Save(user); - var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) + var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id.ToString()) { UserData = "hello" }; @@ -218,7 +218,7 @@ namespace Umbraco.Tests.Services var logins = ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); logins.RemoveAt(0); // remove the first one - logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id)); // add a new one + logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id.ToString())); // add a new one // save new list ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); From 8e9dfad381129f3604d2eb304d38a9add09d80e8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 00:54:28 +1100 Subject: [PATCH 05/14] Simplifying IdentityUser --- .../Models/Identity/IdentityUser.cs | 224 ++++++++++-- .../Security/BackOfficeIdentityUser.cs | 328 ++---------------- .../BackOffice/BackOfficeUserStore.cs | 12 +- 3 files changed, 240 insertions(+), 324 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index dd3841d2c8..516bd60c49 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models.Identity { /// - /// Abstract class for use in Umbraco Identity + /// Abstract class for use in Umbraco Identity for users and members /// - /// The type of user login - /// The type of user role - /// The type of user claims /// /// This class was originally borrowed from the EF implementation in Identity prior to netcore. /// The new IdentityUser in netcore does not have properties such as Claims, Roles and Logins and those are instead @@ -16,40 +17,79 @@ namespace Umbraco.Core.Models.Identity /// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of /// claims, roles and logins directly on the user model. /// - public abstract class IdentityUser - where TLogin : IIdentityUserLogin - where TRole : IdentityUserRole - where TClaim : IdentityUserClaim + public abstract class IdentityUser : IRememberBeingDirty { + private int _id; + private string _email; + private string _userName; + private DateTime? _lastLoginDateUtc; + private bool _emailConfirmed; + private int _accessFailedCount; + private string _passwordHash; + private DateTime? _lastPasswordChangeDateUtc; + private ObservableCollection _logins; + private Lazy> _getLogins; + private ObservableCollection _roles; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected IdentityUser() + public IdentityUser() { - Claims = new List(); - Roles = new List(); - Logins = new List(); + // must initialize before setting groups + _roles = new ObservableCollection(); + _roles.CollectionChanged += Roles_CollectionChanged; + Claims = new List(); + } + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + BeingDirty.PropertyChanged += value; + } + + remove + { + BeingDirty.PropertyChanged -= value; + } } /// /// Gets or sets last login date /// - public virtual DateTime? LastLoginDateUtc { get; set; } + public DateTime? LastLoginDateUtc + { + get => _lastLoginDateUtc; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc)); + } /// /// Gets or sets email /// - public virtual string Email { get; set; } + public string Email + { + get => _email; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); + } /// /// Gets or sets a value indicating whether the email is confirmed, default is false /// - public virtual bool EmailConfirmed { get; set; } + public bool EmailConfirmed + { + get => _emailConfirmed; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); + } /// /// Gets or sets the salted/hashed form of the user password /// - public virtual string PasswordHash { get; set; } + public string PasswordHash + { + get => _passwordHash; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); + } /// /// Gets or sets a random value that should change whenever a users credentials have changed (password changed, login removed) @@ -88,41 +128,173 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets dateTime in UTC when the password was last changed. /// - public virtual DateTime? LastPasswordChangeDateUtc { get; set; } + public DateTime? LastPasswordChangeDateUtc + { + get => _lastPasswordChangeDateUtc; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc)); + } /// /// Gets or sets a value indicating whether is lockout enabled for this user /// - public virtual bool LockoutEnabled { get; set; } + /// + /// Currently this is always true for users and members + /// + public bool LockoutEnabled + { + get => true; + set { } + } /// /// Gets or sets the value to record failures for the purposes of lockout /// - public virtual int AccessFailedCount { get; set; } + public int AccessFailedCount + { + get => _accessFailedCount; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); + } /// - /// Gets the user roles collection + /// Gets or sets the user roles collection /// - public virtual ICollection Roles { get; } + public ICollection Roles + { + get => _roles; + set + { + _roles.CollectionChanged -= Roles_CollectionChanged; + _roles = new ObservableCollection(value); + _roles.CollectionChanged += Roles_CollectionChanged; + } + } /// /// Gets navigation the user claims collection /// - public virtual ICollection Claims { get; } + public ICollection Claims { get; } /// /// Gets the user logins collection /// - public virtual ICollection Logins { get; } + public ICollection Logins + { + get + { + // return if it exists + if (_logins != null) + { + return _logins; + } + + _logins = new ObservableCollection(); + + // if the callback is there and hasn't been created yet then execute it and populate the logins + if (_getLogins != null && !_getLogins.IsValueCreated) + { + foreach (IIdentityUserLogin l in _getLogins.Value) + { + _logins.Add(l); + } + } + + // now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + + return _logins; + } + } /// /// Gets or sets user ID (Primary Key) /// - public virtual int Id { get; set; } + public int Id + { + get => _id; + set + { + _id = value; + HasIdentity = true; + } + } + + /// + /// Gets or sets a value indicating whether returns an Id has been set on this object this will be false if the object is new and not persisted to the database + /// + public bool HasIdentity { get; protected set; } /// /// Gets or sets user name /// - public virtual string UserName { get; set; } + public string UserName + { + get => _userName; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName)); + } + + /// + /// Gets the for change tracking + /// + protected BeingDirty BeingDirty { get; } = new BeingDirty(); + + /// + public bool IsDirty() => BeingDirty.IsDirty(); + + /// + public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName); + + /// + public IEnumerable GetDirtyProperties() => BeingDirty.GetDirtyProperties(); + + /// + public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties(); + + /// + public bool WasDirty() => BeingDirty.WasDirty(); + + /// + public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName); + + /// + public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties(); + + /// + public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty); + + /// + public IEnumerable GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties(); + + /// + /// Disables change tracking. + /// + public void DisableChangeTracking() => BeingDirty.DisableChangeTracking(); + + /// + /// Enables change tracking. + /// + public void EnableChangeTracking() => BeingDirty.EnableChangeTracking(); + + /// + /// Adds a role + /// + /// The role to add + /// + /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted + /// + public void AddRole(string role) => Roles.Add(new IdentityUserRole + { + UserId = Id, + RoleId = role + }); + + /// + /// Used to set a lazy call back to populate the user's Login list + /// + /// The lazy value + public void SetLoginsCallback(Lazy> callback) => _getLogins = callback ?? throw new ArgumentNullException(nameof(callback)); + + private void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Logins)); + + private void Roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => BeingDirty.OnPropertyChanged(nameof(Roles)); } } diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs index 07811f39e1..051a68a362 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs @@ -12,25 +12,24 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Security { - public class BackOfficeIdentityUser : IdentityUser, IRememberBeingDirty + public class BackOfficeIdentityUser : IdentityUser { - private string _email; - private string _userName; - private int _id; - private DateTime? _lastLoginDateUtc; - private bool _emailConfirmed; private string _name; - private int _accessFailedCount; - private string _passwordHash; private string _passwordConfig; private string _culture; - private ObservableCollection _logins; - private Lazy> _getLogins; private IReadOnlyUserGroup[] _groups; private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; - private DateTime? _lastPasswordChangeDateUtc; + + // Custom comparer for enumerables + private static readonly DelegateEqualityComparer s_groupsComparer = new DelegateEqualityComparer( + (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), + groups => groups.GetHashCode()); + + private static readonly DelegateEqualityComparer s_startIdsComparer = new DelegateEqualityComparer( + (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), + groups => groups.GetHashCode()); /// /// Used to construct a new instance without an identity @@ -54,12 +53,12 @@ namespace Umbraco.Core.Security var user = new BackOfficeIdentityUser(globalSettings, Array.Empty()); user.DisableChangeTracking(); - user._userName = username; - user._email = email; + user.UserName = username; + user.Email = email; // we are setting minvalue here because the default is "0" which is the id of the admin user // which we cannot allow because the admin user will always exist - user._id = int.MinValue; + user.Id = int.MinValue; user.HasIdentity = false; user._culture = culture; user._name = name; @@ -74,10 +73,6 @@ namespace Umbraco.Core.Security _allowedSections = Array.Empty(); _culture = globalSettings.DefaultUILanguage; - // must initialize before setting groups - _roles = new ObservableCollection(); - _roles.CollectionChanged += _roles_CollectionChanged; - // use the property setters - they do more than just setting a field Groups = groups; } @@ -95,105 +90,28 @@ namespace Umbraco.Core.Security Id = userId; } - /// - /// Returns true if an Id has been set on this object this will be false if the object is new and not persisted to the database - /// - public bool HasIdentity { get; private set; } - public int[] CalculatedMediaStartNodeIds { get; set; } public int[] CalculatedContentStartNodeIds { get; set; } - public override int Id - { - get => _id; - set - { - _id = value; - HasIdentity = true; - } - } - /// - /// Override Email so we can track changes to it - /// - public override string Email - { - get => _email; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); - } - - /// - /// Override UserName so we can track changes to it - /// - public override string UserName - { - get => _userName; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName)); - } - - /// - /// LastPasswordChangeDateUtc so we can track changes to it - /// - public override DateTime? LastPasswordChangeDateUtc - { - get { return _lastPasswordChangeDateUtc; } - set { _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc)); } - } - - /// - /// Override LastLoginDateUtc so we can track changes to it - /// - public override DateTime? LastLoginDateUtc - { - get => _lastLoginDateUtc; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc)); - } - - /// - /// Override EmailConfirmed so we can track changes to it - /// - public override bool EmailConfirmed - { - get => _emailConfirmed; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); - } - - /// - /// Gets/sets the user's real name + /// Gets or sets the user's real name /// public string Name { get => _name; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); } - /// - /// Override AccessFailedCount so we can track changes to it - /// - public override int AccessFailedCount - { - get => _accessFailedCount; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); - } - - /// - /// Override PasswordHash so we can track changes to it - /// - public override string PasswordHash - { - get => _passwordHash; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); - } public string PasswordConfig { get => _passwordConfig; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); } /// - /// Content start nodes assigned to the User (not ones assigned to the user's groups) + /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups) /// public int[] StartContentIds { @@ -201,13 +119,16 @@ namespace Umbraco.Core.Security set { if (value == null) + { value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer); + } + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), s_startIdsComparer); } } /// - /// Media start nodes assigned to the User (not ones assigned to the user's groups) + /// Gets or sets media start nodes assigned to the User (not ones assigned to the user's groups) /// public int[] StartMediaIds { @@ -215,23 +136,23 @@ namespace Umbraco.Core.Security set { if (value == null) + { value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer); + } + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), s_startIdsComparer); } } /// - /// This is a readonly list of the user's allowed sections which are based on it's user groups + /// Gets a readonly list of the user's allowed sections which are based on it's user groups /// - public string[] AllowedSections - { - get { return _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); } - } + public string[] AllowedSections => _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); public string Culture { get => _culture; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); } public IReadOnlyUserGroup[] Groups @@ -244,34 +165,23 @@ namespace Umbraco.Core.Security _groups = value; - // now clear all roles and re-add them - _roles.CollectionChanged -= _roles_CollectionChanged; - _roles.Clear(); - foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole + var roles = new List(); + foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole { RoleId = x.Alias, UserId = Id })) { - _roles.Add(identityUserRole); + roles.Add(identityUserRole); } - _roles.CollectionChanged += _roles_CollectionChanged; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), GroupsComparer); + // now reset the collection + Roles = roles; + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer); } } - /// - /// Lockout is always enabled - /// - public override bool LockoutEnabled - { - get { return true; } - set - { - //do nothing - } - } /// /// Based on the user's lockout end date, this will determine if they are locked out @@ -290,171 +200,5 @@ namespace Umbraco.Core.Security /// public bool IsApproved { get; set; } - /// - /// Overridden to make the retrieval lazy - /// - public override ICollection Logins - { - get - { - // return if it exists - if (_logins != null) - return _logins; - - _logins = new ObservableCollection(); - - // if the callback is there and hasn't been created yet then execute it and populate the logins - if (_getLogins != null && !_getLogins.IsValueCreated) - { - foreach (var l in _getLogins.Value) - { - _logins.Add(l); - } - } - - //now assign events - _logins.CollectionChanged += Logins_CollectionChanged; - - return _logins; - } - } - - void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _beingDirty.OnPropertyChanged(nameof(Logins)); - } - - private void _roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _beingDirty.OnPropertyChanged(nameof(Roles)); - } - - private readonly ObservableCollection _roles; - - /// - /// helper method to easily add a role without having to deal with IdentityUserRole{T} - /// - /// - /// - /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted - /// - public void AddRole(string role) - { - Roles.Add(new IdentityUserRole - { - UserId = Id, - RoleId = role - }); - } - - /// - /// Override Roles because the value of these are the user's group aliases - /// - public override ICollection Roles => _roles; - - /// - /// Used to set a lazy call back to populate the user's Login list - /// - /// - public void SetLoginsCallback(Lazy> callback) - { - _getLogins = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - #region BeingDirty - - private readonly BeingDirty _beingDirty = new BeingDirty(); - - /// - public bool IsDirty() - { - return _beingDirty.IsDirty(); - } - - /// - public bool IsPropertyDirty(string propName) - { - return _beingDirty.IsPropertyDirty(propName); - } - - /// - public IEnumerable GetDirtyProperties() - { - return _beingDirty.GetDirtyProperties(); - } - - /// - public void ResetDirtyProperties() - { - _beingDirty.ResetDirtyProperties(); - } - - /// - public bool WasDirty() - { - return _beingDirty.WasDirty(); - } - - /// - public bool WasPropertyDirty(string propertyName) - { - return _beingDirty.WasPropertyDirty(propertyName); - } - - /// - public void ResetWereDirtyProperties() - { - _beingDirty.ResetWereDirtyProperties(); - } - - /// - public void ResetDirtyProperties(bool rememberDirty) - { - _beingDirty.ResetDirtyProperties(rememberDirty); - } - - /// - public IEnumerable GetWereDirtyProperties() - => _beingDirty.GetWereDirtyProperties(); - - /// - /// Disables change tracking. - /// - public void DisableChangeTracking() - { - _beingDirty.DisableChangeTracking(); - } - - /// - /// Enables change tracking. - /// - public void EnableChangeTracking() - { - _beingDirty.EnableChangeTracking(); - } - - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _beingDirty.PropertyChanged += value; - } - remove - { - _beingDirty.PropertyChanged -= value; - } - } - - #endregion - - //Custom comparer for enumerables - private static readonly DelegateEqualityComparer GroupsComparer = new DelegateEqualityComparer( - (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), - groups => groups.GetHashCode()); - - private static readonly DelegateEqualityComparer StartIdsComparer = new DelegateEqualityComparer( - (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), - groups => groups.GetHashCode()); - } } diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index 8e980e7598..234d6eae66 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -870,28 +870,28 @@ namespace Umbraco.Core.BackOffice { anythingChanged = true; - //clear out the current groups (need to ToArray since we are modifying the iterator) + // clear out the current groups (need to ToArray since we are modifying the iterator) user.ClearGroups(); - //go lookup all these groups + // go lookup all these groups var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); - //use all of the ones assigned and add them + // use all of the ones assigned and add them foreach (var group in groups) { user.AddGroup(group); } - //re-assign + // re-assign identityUser.Groups = groups; } } - //we should re-set the calculated start nodes + // we should re-set the calculated start nodes identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); - //reset all changes + // reset all changes identityUser.ResetDirtyProperties(false); return anythingChanged; From 35af86c3d3ecc2c3ba76edfa4cc7208caadfc510 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 01:38:36 +1100 Subject: [PATCH 06/14] Splits user manager into a base class that can be reused changes base class of IdentityUser to UmbracoIdentityUser --- linting/codeanalysis.ruleset | 2 + ...IdentityUser.cs => UmbracoIdentityUser.cs} | 6 +- .../Security/BackOfficeIdentityUser.cs | 20 +- .../BackOffice/BackOfficeUserStore.cs | 128 +------- .../Security/BackOfficeUserManager.cs | 303 +++--------------- .../Security/UmbracoUserManager.cs | 241 ++++++++++++++ 6 files changed, 309 insertions(+), 391 deletions(-) rename src/Umbraco.Core/Models/Identity/{IdentityUser.cs => UmbracoIdentityUser.cs} (98%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Security/BackOfficeUserManager.cs (56%) create mode 100644 src/Umbraco.Web.Common/Security/UmbracoUserManager.cs diff --git a/linting/codeanalysis.ruleset b/linting/codeanalysis.ruleset index 4fde2bef8d..57c9fb7d60 100644 --- a/linting/codeanalysis.ruleset +++ b/linting/codeanalysis.ruleset @@ -11,6 +11,8 @@ + + \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs similarity index 98% rename from src/Umbraco.Core/Models/Identity/IdentityUser.cs rename to src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs index 516bd60c49..ffa549ab47 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Models.Identity /// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of /// claims, roles and logins directly on the user model. /// - public abstract class IdentityUser : IRememberBeingDirty + public abstract class UmbracoIdentityUser : IRememberBeingDirty { private int _id; private string _email; @@ -32,9 +32,9 @@ namespace Umbraco.Core.Models.Identity private ObservableCollection _roles; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public IdentityUser() + public UmbracoIdentityUser() { // must initialize before setting groups _roles = new ObservableCollection(); diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs index 051a68a362..4de1ae4d0f 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; @@ -12,7 +8,10 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Security { - public class BackOfficeIdentityUser : IdentityUser + /// + /// The identity user used for the back office + /// + public class BackOfficeIdentityUser : UmbracoIdentityUser { private string _name; private string _passwordConfig; @@ -34,11 +33,7 @@ namespace Umbraco.Core.Security /// /// Used to construct a new instance without an identity /// - /// - /// /// This is allowed to be null (but would need to be filled in if trying to persist this instance) - /// - /// public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string username, string email, string culture, string name = null) { if (string.IsNullOrWhiteSpace(username)) @@ -80,9 +75,6 @@ namespace Umbraco.Core.Security /// /// Initializes a new instance of the class. /// - /// - /// - /// public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups) : this(globalSettings, groups.ToArray()) { @@ -184,7 +176,7 @@ namespace Umbraco.Core.Security /// - /// Based on the user's lockout end date, this will determine if they are locked out + /// Gets a value indicating whether the user is locked out based on the user's lockout end date /// public bool IsLockedOut { @@ -196,7 +188,7 @@ namespace Umbraco.Core.Security } /// - /// This is a 1:1 mapping with IUser.IsApproved + /// Gets or sets a value indicating the IUser IsApproved /// public bool IsApproved { get; set; } diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index 234d6eae66..e74690b76f 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -18,6 +17,8 @@ using Umbraco.Core.Services; namespace Umbraco.Core.BackOffice { + // TODO: Make this into a base class that can be re-used + public class BackOfficeUserStore : DisposableObjectSlim, IUserPasswordStore, IUserEmailStore, @@ -28,11 +29,11 @@ namespace Umbraco.Core.BackOffice IUserSessionStore // TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco - //IUserTwoFactorStore, + // IUserTwoFactorStore, // TODO: This would require additional columns/tables for now people will need to implement this on their own - //IUserPhoneNumberStore, + // IUserPhoneNumberStore, // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation - //IQueryableUserStore + // IQueryableUserStore { private readonly IScopeProvider _scopeProvider; private readonly IUserService _userService; @@ -42,15 +43,16 @@ namespace Umbraco.Core.BackOffice private readonly UmbracoMapper _mapper; private bool _disposed = false; + /// + /// Initializes a new instance of the class. + /// public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper) { _scopeProvider = scopeProvider; - _userService = userService; + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService; - _externalLoginService = externalLoginService; + _externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService)); _globalSettings = globalSettings.Value; - if (userService == null) throw new ArgumentNullException("userService"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); _mapper = mapper; _userService = userService; _externalLoginService = externalLoginService; @@ -59,10 +61,7 @@ namespace Umbraco.Core.BackOffice /// /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// - protected override void DisposeResources() - { - _disposed = true; - } + protected override void DisposeResources() => _disposed = true; public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) { @@ -105,9 +104,6 @@ namespace Umbraco.Core.BackOffice /// /// Insert a new user /// - /// - /// - /// public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -158,9 +154,6 @@ namespace Umbraco.Core.BackOffice /// /// Update a user /// - /// - /// - /// public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -206,8 +199,6 @@ namespace Umbraco.Core.BackOffice /// /// Delete a user /// - /// - /// public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -227,9 +218,6 @@ namespace Umbraco.Core.BackOffice /// /// Finds a user /// - /// - /// - /// public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -244,9 +232,6 @@ namespace Umbraco.Core.BackOffice /// /// Find a user by name /// - /// - /// - /// public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -265,9 +250,6 @@ namespace Umbraco.Core.BackOffice /// /// Set the user password hash /// - /// - /// - /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -278,6 +260,7 @@ namespace Umbraco.Core.BackOffice user.PasswordHash = passwordHash; user.PasswordConfig = null; // Clear this so that it's reset at the repository level + user.LastPasswordChangeDateUtc = DateTime.UtcNow; return Task.CompletedTask; } @@ -285,9 +268,6 @@ namespace Umbraco.Core.BackOffice /// /// Get the user password hash /// - /// - /// - /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -300,9 +280,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns true if a user has a password set /// - /// - /// - /// public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -315,9 +292,6 @@ namespace Umbraco.Core.BackOffice /// /// Set the user email /// - /// - /// - /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -333,9 +307,6 @@ namespace Umbraco.Core.BackOffice /// /// Get the user email /// - /// - /// - /// public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -348,9 +319,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns true if the user email is confirmed /// - /// - /// - /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -363,9 +331,6 @@ namespace Umbraco.Core.BackOffice /// /// Sets whether the user email is confirmed /// - /// - /// - /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -377,9 +342,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns the user associated with this email /// - /// - /// - /// public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -393,22 +355,14 @@ namespace Umbraco.Core.BackOffice } public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetEmailAsync(user, cancellationToken); - } + => GetEmailAsync(user, cancellationToken); public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - return SetEmailAsync(user, normalizedEmail, cancellationToken); - } + => 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(); @@ -427,11 +381,6 @@ namespace Umbraco.Core.BackOffice /// /// 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(); @@ -447,9 +396,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns the linked accounts for this user /// - /// - /// - /// public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -498,10 +444,6 @@ namespace Umbraco.Core.BackOffice /// /// Adds a user to a role (user group) /// - /// - /// - /// - /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -523,10 +465,6 @@ namespace Umbraco.Core.BackOffice /// /// Removes the role (user group) for the user /// - /// - /// - /// - /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -548,9 +486,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns the roles (user groups) for this user /// - /// - /// - /// public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -562,10 +497,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns true if a user is in the role /// - /// - /// - /// - /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -597,10 +528,6 @@ namespace Umbraco.Core.BackOffice /// /// Set the security stamp for the user /// - /// - /// - /// - /// public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -614,9 +541,6 @@ namespace Umbraco.Core.BackOffice /// /// Get the user security stamp /// - /// - /// - /// public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -644,10 +568,7 @@ namespace Umbraco.Core.BackOffice /// /// 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)) @@ -664,9 +585,6 @@ namespace Umbraco.Core.BackOffice /// /// 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 /// @@ -683,9 +601,6 @@ namespace Umbraco.Core.BackOffice /// /// Used to record when an attempt to access the user has failed /// - /// - /// - /// public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -699,9 +614,6 @@ namespace Umbraco.Core.BackOffice /// /// 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(); @@ -716,9 +628,6 @@ namespace Umbraco.Core.BackOffice /// 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(); @@ -730,9 +639,6 @@ namespace Umbraco.Core.BackOffice /// /// Returns true /// - /// - /// - /// public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); @@ -744,9 +650,6 @@ namespace Umbraco.Core.BackOffice /// /// 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(); @@ -904,8 +807,7 @@ namespace Umbraco.Core.BackOffice public Task ValidateSessionIdAsync(string userId, string sessionId) { - Guid guidSessionId; - if (Guid.TryParse(sessionId, out guidSessionId)) + if (Guid.TryParse(sessionId, out Guid guidSessionId)) { return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs similarity index 56% rename from src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs rename to src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index cc9b9410a6..9f77cdb7d4 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -11,6 +11,7 @@ using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; @@ -20,9 +21,10 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.Security { - - public class BackOfficeUserManager : BackOfficeUserManager, IBackOfficeUserManager + public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager { + private readonly IHttpContextAccessor _httpContextAccessor; + public BackOfficeUserManager( IIpResolver ipResolver, IUserStore store, @@ -36,135 +38,11 @@ namespace Umbraco.Web.Common.Security IHttpContextAccessor httpContextAccessor, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, httpContextAccessor, logger, passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) { - } - } - - public class BackOfficeUserManager : UserManager - where T : BackOfficeIdentityUser - { - private PasswordGenerator _passwordGenerator; - private readonly IHttpContextAccessor _httpContextAccessor; - - public BackOfficeUserManager( - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - BackOfficeLookupNormalizer keyNormalizer, - BackOfficeIdentityErrorDescriber errors, - IServiceProvider services, - IHttpContextAccessor httpContextAccessor, - ILogger> logger, - IOptions passwordConfiguration) - : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) - { - IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); _httpContextAccessor = httpContextAccessor; - PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - } - // We don't support an IUserClaimStore and don't need to (at least currently) - public override bool SupportsUserClaim => false; - - // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository - public override bool SupportsQueryableUsers => false; - - /// - /// Developers will need to override this to support custom 2 factor auth - /// - public override bool SupportsUserTwoFactor => false; - - // We haven't needed to support this yet, though might be necessary for 2FA - public override bool SupportsUserPhoneNumber => false; - - /// - /// Replace the underlying options property with our own strongly typed version - /// - public new BackOfficeIdentityOptions Options - { - get => (BackOfficeIdentityOptions)base.Options; - set => base.Options = value; - } - - /// - /// Used to validate a user's session - /// - /// The user id - /// The sesion id - /// True if the sesion is valid, else false - public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) - { - var userSessionStore = Store as IUserSessionStore; - - // if this is not set, for backwards compat (which would be super rare), we'll just approve it - if (userSessionStore == null) - { - return true; - } - - return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); - } - - /// - /// This will determine which password hasher to use based on what is defined in config - /// - /// The - /// An - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); - - /// - /// Gets/sets the default back office user password checker - /// - public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - public IPasswordConfiguration PasswordConfiguration { get; protected set; } - public IIpResolver IpResolver { get; } - - /// - /// Helper method to generate a password for a user based on the current password validator - /// - /// The generated password - public string GeneratePassword() - { - if (_passwordGenerator == null) - { - _passwordGenerator = new PasswordGenerator(PasswordConfiguration); - } - - var password = _passwordGenerator.GeneratePassword(); - return password; - } - - /// - /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date - /// - /// The user - /// True if the user is locked out, else false - /// - /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values - /// - public override async Task IsLockedOutAsync(T user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (user.IsApproved == false) - { - return true; - } - - return await base.IsLockedOutAsync(user); - } - - /// - /// Logic used to validate a username and password - /// /// /// /// By default this uses the standard ASP.Net Identity approach which is: @@ -180,7 +58,7 @@ namespace Umbraco.Web.Common.Security /// We've allowed this check to be overridden with a simple callback so that developers don't actually /// have to implement/override this class. /// - public override async Task CheckPasswordAsync(T user, string password) + public override async Task CheckPasswordAsync(BackOfficeIdentityUser user, string password) { if (BackOfficeUserPasswordChecker != null) { @@ -198,36 +76,49 @@ namespace Umbraco.Web.Common.Security } } - // we cannot proceed if the user passed in does not have an identity - if (user.HasIdentity == false) - { - return false; - } - // use the default behavior return await base.CheckPasswordAsync(user, password); } /// - /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date /// - /// The userId - /// The reset password token - /// The new password to set it to - /// The + /// The user + /// True if the user is locked out, else false /// - /// We use this because in the back office the only way an admin can change another user's password without first knowing their password - /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values /// - public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + public override async Task IsLockedOutAsync(BackOfficeIdentityUser user) { - T user = await FindByIdAsync(userId.ToString()); if (user == null) { - throw new InvalidOperationException("Could not find user"); + throw new ArgumentNullException(nameof(user)); } - IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword); + if (user.IsApproved == false) + { + return true; + } + + return await base.IsLockedOutAsync(user); + } + + public override async Task AccessFailedAsync(BackOfficeIdentityUser user) + { + IdentityResult result = await base.AccessFailedAsync(user); + + // Slightly confusing: this will return a Success if we successfully update the AccessFailed count + if (result.Succeeded) + { + RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + } + + return result; + } + + public override async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + { + IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); @@ -236,79 +127,19 @@ namespace Umbraco.Web.Common.Security return result; } - public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) + public override async Task ChangePasswordAsync(BackOfficeIdentityUser user, string currentPassword, string newPassword) { IdentityResult result = await base.ChangePasswordAsync(user, currentPassword, newPassword); if (result.Succeeded) { RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); } + return result; } - /// - /// Override to determine how to hash the password - /// /// - protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) - { - user.LastPasswordChangeDateUtc = DateTime.UtcNow; - - if (validatePassword) - { - IdentityResult validate = await ValidatePasswordAsync(user, newPassword); - if (!validate.Succeeded) - { - return validate; - } - } - - var passwordStore = Store as IUserPasswordStore; - if (passwordStore == null) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); - } - - var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; - await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); - await UpdateSecurityStampInternal(user); - return IdentityResult.Success; - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - private async Task UpdateSecurityStampInternal(T user) - { - if (SupportsUserSecurityStamp == false) - { - return; - } - - await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - private IUserSecurityStampStore GetSecurityStore() - { - var store = Store as IUserSecurityStampStore; - if (store == null) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); - } - - return store; - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - private static string NewSecurityStamp() => Guid.NewGuid().ToString(); - - /// - public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) + public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd) { if (user == null) { @@ -320,7 +151,7 @@ namespace Umbraco.Web.Common.Security // The way we unlock is by setting the lockoutEnd date to the current datetime if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) { - RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); } else { @@ -334,62 +165,12 @@ namespace Umbraco.Web.Common.Security } /// - public override async Task ResetAccessFailedCountAsync(T user) + public override async Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var lockoutStore = (IUserLockoutStore)Store; - var accessFailedCount = await GetAccessFailedCountAsync(user); - - if (accessFailedCount == 0) - { - return IdentityResult.Success; - } - - await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); + IdentityResult result = await base.ResetAccessFailedCountAsync(user); // raise the event now that it's reset RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id); - return await UpdateAsync(user); - } - - /// - /// Overrides the Microsoft ASP.NET user management method - /// - /// - public override async Task AccessFailedAsync(T user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var lockoutStore = Store as IUserLockoutStore; - if (lockoutStore == null) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); - } - - var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); - - if (count >= Options.Lockout.MaxFailedAccessAttempts) - { - await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None); - - // NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 - // here we are persisting the value for the back office - } - - IdentityResult result = await UpdateAsync(user); - - // Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Succeeded) - { - RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } return result; } diff --git a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs new file mode 100644 index 0000000000..a555cca4be --- /dev/null +++ b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; +using Umbraco.Net; + + +namespace Umbraco.Web.Common.Security +{ + + /// + /// Abstract class for Umbraco User Managers for back office users or front-end members + /// + /// The type of user + public abstract class UmbracoUserManager : UserManager + where T : UmbracoIdentityUser + { + private PasswordGenerator _passwordGenerator; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoUserManager( + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + BackOfficeLookupNormalizer keyNormalizer, + BackOfficeIdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger, + IOptions passwordConfiguration) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); + PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + } + + // We don't support an IUserClaimStore and don't need to (at least currently) + public override bool SupportsUserClaim => false; + + // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository + public override bool SupportsQueryableUsers => false; + + /// + /// Developers will need to override this to support custom 2 factor auth + /// + public override bool SupportsUserTwoFactor => false; + + // We haven't needed to support this yet, though might be necessary for 2FA + public override bool SupportsUserPhoneNumber => false; + + /// + /// Used to validate a user's session + /// + /// The user id + /// The sesion id + /// True if the sesion is valid, else false + public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) + { + var userSessionStore = Store as IUserSessionStore; + + // if this is not set, for backwards compat (which would be super rare), we'll just approve it + if (userSessionStore == null) + { + return true; + } + + return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); + } + + /// + /// This will determine which password hasher to use based on what is defined in config + /// + /// The + /// An + protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); + + /// + /// Gets or sets the default back office user password checker + /// + public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } + + public IPasswordConfiguration PasswordConfiguration { get; protected set; } + + public IIpResolver IpResolver { get; } + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// The generated password + public string GeneratePassword() + { + if (_passwordGenerator == null) + { + _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + } + + var password = _passwordGenerator.GeneratePassword(); + return password; + } + + /// + public override async Task CheckPasswordAsync(T user, string password) + { + // we cannot proceed if the user passed in does not have an identity + if (user.HasIdentity == false) + { + return false; + } + + // use the default behavior + return await base.CheckPasswordAsync(user, password); + } + + /// + /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event + /// + /// The userId + /// The reset password token + /// The new password to set it to + /// The + /// + /// We use this because in the back office the only way an admin can change another user's password without first knowing their password + /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset + /// + public virtual async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + { + T user = await FindByIdAsync(userId.ToString()); + if (user == null) + { + throw new InvalidOperationException("Could not find user"); + } + + IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword); + return result; + } + + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + private IUserSecurityStampStore GetSecurityStore() + { + var store = Store as IUserSecurityStampStore; + if (store == null) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + } + + return store; + } + + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + private static string NewSecurityStamp() => Guid.NewGuid().ToString(); + + /// + public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IdentityResult result = await base.SetLockoutEndDateAsync(user, lockoutEnd); + + // The way we unlock is by setting the lockoutEnd date to the current datetime + if (!result.Succeeded || lockoutEnd < DateTimeOffset.UtcNow) + { + // Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(user); + } + + return result; + } + + /// + public override async Task ResetAccessFailedCountAsync(T user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var lockoutStore = (IUserLockoutStore)Store; + var accessFailedCount = await GetAccessFailedCountAsync(user); + + if (accessFailedCount == 0) + { + return IdentityResult.Success; + } + + await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); + + return await UpdateAsync(user); + } + + /// + /// Overrides the Microsoft ASP.NET user management method + /// + /// + public override async Task AccessFailedAsync(T user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var lockoutStore = Store as IUserLockoutStore; + if (lockoutStore == null) + { + throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); + } + + var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); + + if (count >= Options.Lockout.MaxFailedAccessAttempts) + { + await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), CancellationToken.None); + + // NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 + // here we are persisting the value for the back office + } + + IdentityResult result = await UpdateAsync(user); + return result; + } + + } +} From aeec18d80828a665a01a4983d44b1a0c89aa0984 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 01:50:30 +1100 Subject: [PATCH 07/14] removes the interface --- .../Models/Identity/IIdentityUser.cs | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/Umbraco.Core/Models/Identity/IIdentityUser.cs diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUser.cs b/src/Umbraco.Core/Models/Identity/IIdentityUser.cs deleted file mode 100644 index fa7f52c710..0000000000 --- a/src/Umbraco.Core/Models/Identity/IIdentityUser.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Models.Identity -{ - public interface IIdentityUser - { - int AccessFailedCount { get; set; } - //ICollection Claims { get; } - string Email { get; set; } - bool EmailConfirmed { get; set; } - TKey Id { get; set; } - DateTime? LastLoginDateUtc { get; set; } - DateTime? LastPasswordChangeDateUtc { get; set; } - bool LockoutEnabled { get; set; } - DateTime? LockoutEndDateUtc { get; set; } - //ICollection Logins { get; } - string PasswordHash { get; set; } - string PhoneNumber { get; set; } - bool PhoneNumberConfirmed { get; set; } - //ICollection Roles { get; } - string SecurityStamp { get; set; } - bool TwoFactorEnabled { get; set; } - string UserName { get; set; } - } -} From 86d231f5de05a55adefff362b25fd29dabcd4ffe Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 02:21:21 +1100 Subject: [PATCH 08/14] removes remaining back office things from underlying UmbracoUserManager moves files --- .../IBackOfficeUserPasswordChecker.cs | 3 - .../Install/InstallSteps/NewInstallStep.cs | 5 +- .../BackOfficeClaimsPrincipalFactory.cs | 2 +- .../BackOfficeIdentityBuilder.cs | 24 ++++---- .../BackOfficeIdentityErrorDescriber.cs | 5 +- .../BackOfficeIdentityOptions.cs | 4 +- .../BackOfficeLookupNormalizer.cs | 6 +- .../BackOfficeUserStore.cs | 2 +- .../BackOfficeUserValidator.cs | 4 +- .../IBackOfficeUserManager.cs | 2 +- .../IUmbracoUserManager.cs | 2 +- .../IUserSessionStore.cs | 2 +- .../IdentityExtensions.cs | 0 .../Security/SignOutAuditEventArgs.cs | 4 +- .../Security/UserInviteEventArgs.cs | 6 +- ...kOfficeServiceCollectionExtensionsTests.cs | 5 +- .../BackOfficeClaimsPrincipalFactoryTests.cs | 1 - .../BackOffice/NopLookupNormalizerTests.cs | 4 +- .../BackOfficeLookupNormalizerTests.cs | 4 +- .../Controllers/UsersControllerUnitTests.cs | 3 - .../Controllers/AuthenticationController.cs | 8 +-- .../Controllers/BackOfficeController.cs | 21 ++++--- .../Controllers/CurrentUserController.cs | 10 ++-- .../Controllers/UsersController.cs | 18 +++--- .../BackOfficeApplicationBuilderExtensions.cs | 1 - .../BackOfficeServiceCollectionExtensions.cs | 5 -- .../Extensions/WebMappingProfiles.cs | 5 +- .../Security/BackOfficeSessionIdValidator.cs | 4 +- .../Security/BackOfficeUserManagerAuditer.cs | 4 +- .../ConfigureBackOfficeIdentityOptions.cs | 4 +- .../Security/PasswordChanger.cs | 4 +- .../Security/BackOfficeUserManager.cs | 11 ++-- .../Security/UmbracoUserManager.cs | 59 +++++++++---------- ...eDirectoryBackOfficeUserPasswordChecker.cs | 2 - .../BackOfficeCookieAuthenticationProvider.cs | 46 --------------- .../Security/BackOfficeSignInManager.cs | 47 --------------- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- 37 files changed, 111 insertions(+), 230 deletions(-) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeClaimsPrincipalFactory.cs (99%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeIdentityBuilder.cs (69%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeIdentityErrorDescriber.cs (55%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeIdentityOptions.cs (72%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeLookupNormalizer.cs (75%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeUserStore.cs (99%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/BackOfficeUserValidator.cs (90%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/IBackOfficeUserManager.cs (86%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/IUmbracoUserManager.cs (99%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/IUserSessionStore.cs (92%) rename src/Umbraco.Infrastructure/{BackOffice => Security}/IdentityExtensions.cs (100%) delete mode 100644 src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs delete mode 100644 src/Umbraco.Web/Security/BackOfficeSignInManager.cs diff --git a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs index 45f5ea44e2..fdf1f1fcf2 100644 --- a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs @@ -11,9 +11,6 @@ namespace Umbraco.Core.Security /// /// Checks a password for a user /// - /// - /// - /// /// /// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally. /// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in. diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 96e4a9ae34..80570ae5de 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.Net.Http; using System.Text; @@ -6,10 +6,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.Install.Models; diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs similarity index 99% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 380ed452d0..8a6680d2bf 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Core.Security; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// A diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs similarity index 69% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs index 90c2823122..c9f8d35ada 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -1,19 +1,25 @@ -using System; +using System; using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Security; -namespace Umbraco.Infrastructure.BackOffice +namespace Umbraco.Core.Security { public class BackOfficeIdentityBuilder : IdentityBuilder { - public BackOfficeIdentityBuilder(IServiceCollection services) : base(typeof(BackOfficeIdentityUser), services) + /// + /// Initializes a new instance of the class. + /// + public BackOfficeIdentityBuilder(IServiceCollection services) + : base(typeof(BackOfficeIdentityUser), services) { } - public BackOfficeIdentityBuilder(Type role, IServiceCollection services) : base(typeof(BackOfficeIdentityUser), role, services) + /// + /// Initializes a new instance of the class. + /// + public BackOfficeIdentityBuilder(Type role, IServiceCollection services) + : base(typeof(BackOfficeIdentityUser), role, services) { } @@ -29,10 +35,8 @@ namespace Umbraco.Infrastructure.BackOffice { throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}"); } - Services.Configure(options => - { - options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider); - }); + + Services.Configure(options => options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider)); Services.AddTransient(provider); return this; } diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs similarity index 55% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs index 012ac5650f..6d36e489b8 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityErrorDescriber.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// Umbraco back office specific /// public class BackOfficeIdentityErrorDescriber : IdentityErrorDescriber { + // TODO: Override all the methods in order to provide our own translated error messages } } diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs similarity index 72% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs index 2f729072a6..77849c4d0c 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityOptions.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityOptions.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// Identity options specifically for the back office identity implementation diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs b/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs similarity index 75% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs index cc9249d462..957e36d1d0 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizer.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeLookupNormalizer.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// @@ -8,6 +8,8 @@ namespace Umbraco.Core.BackOffice /// public class BackOfficeLookupNormalizer : ILookupNormalizer { + // TODO: Do we need this? + public string NormalizeName(string name) => name; public string NormalizeEmail(string email) => email; diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs similarity index 99% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index e74690b76f..4b4383c402 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -15,7 +15,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Security; using Umbraco.Core.Services; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { // TODO: Make this into a base class that can be re-used diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs similarity index 90% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs index b7cbb7555d..8b2c8932a7 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core.Security; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { public class BackOfficeUserValidator : UserValidator where T : BackOfficeIdentityUser diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs similarity index 86% rename from src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs rename to src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs index be4bd194f9..4235195bb1 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs @@ -1,6 +1,6 @@ using Umbraco.Core.Security; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// The user manager for the back office diff --git a/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs similarity index 99% rename from src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs rename to src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 803f64e0e6..c50b012dae 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// diff --git a/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs similarity index 92% rename from src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs rename to src/Umbraco.Infrastructure/Security/IUserSessionStore.cs index 69d5408cf7..06b7c2f165 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs +++ b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// /// An IUserStore interface part to implement if the store supports validating user session Ids diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/IdentityExtensions.cs similarity index 100% rename from src/Umbraco.Infrastructure/BackOffice/IdentityExtensions.cs rename to src/Umbraco.Infrastructure/Security/IdentityExtensions.cs diff --git a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index 34bd9c9a42..2e5997b603 100644 --- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -1,6 +1,6 @@ -using Umbraco.Core.Security; +using Umbraco.Core.Security; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { /// diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 2aefb47c14..811092a2c9 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -1,8 +1,8 @@ -using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { public class UserInviteEventArgs : IdentityAuditEventArgs { @@ -25,7 +25,7 @@ namespace Umbraco.Core.BackOffice /// /// The local user that has been created that is pending the invite - /// + /// public IUser User { get; } /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index b6a86344a2..bf198d9819 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -2,10 +2,9 @@ using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using Umbraco.Extensions; -using Umbraco.Core.BackOffice; -using Umbraco.Tests.Integration.Testing; using Umbraco.Core.Security; +using Umbraco.Extensions; +using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index 9d8edbc75e..f85c15b3bf 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs index 1447b7f97e..02ff01ff3b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs @@ -1,6 +1,6 @@ -using System; +using System; using NUnit.Framework; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs index 3feb458fe8..8172a712d8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs @@ -1,6 +1,6 @@ -using System; +using System; using NUnit.Framework; -using Umbraco.Core.BackOffice; +using Umbraco.Core.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs index 6ecda57cc6..4f4db85e5e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/UsersControllerUnitTests.cs @@ -1,9 +1,6 @@ -using System.Threading; using AutoFixture.NUnit3; -using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; -using Umbraco.Core.BackOffice; using Umbraco.Core.Security; using Umbraco.Tests.UnitTests.AutoFixture; using Umbraco.Web.BackOffice.Controllers; diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index efe28763f1..f7e10d77af 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -26,6 +25,7 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; @@ -33,8 +33,6 @@ using Umbraco.Web.Common.Security; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; -using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 1ce0831502..89b121b575 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -1,15 +1,19 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; @@ -22,21 +26,16 @@ using Umbraco.Core.WebAssets; using Umbraco.Extensions; using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Security; +using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.Security; using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; -using Microsoft.AspNetCore.Identity; -using System.Security.Claims; -using Microsoft.AspNetCore.Http; -using Umbraco.Web.BackOffice.Security; -using Umbraco.Web.Common.ActionsResults; -using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.Common.Authorization; -using Microsoft.AspNetCore.Authentication; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 7c984e901e..d156551c26 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -1,15 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; @@ -23,12 +23,10 @@ using Umbraco.Extensions; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Common.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; -using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 38bf69721a..5052f5146e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,13 +6,13 @@ using System.Net; using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; @@ -26,23 +26,21 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Web.Models; -using Umbraco.Web.Models.ContentEditing; using Umbraco.Extensions; +using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.BackOffice.Security; -using Umbraco.Web.BackOffice.ActionResults; +using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; +using Umbraco.Web.Models; +using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; using IUser = Umbraco.Core.Models.Membership.IUser; using Task = System.Threading.Tasks.Task; -using Umbraco.Net; -using Umbraco.Web.Common.ActionsResults; -using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index a097ead4a1..6ff42a5737 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -2,7 +2,6 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using SixLabors.ImageSharp.Web.DependencyInjection; -using Umbraco.Core.BackOffice; using Umbraco.Web.BackOffice.Middleware; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.Common.Security; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 74953b19be..9ad448a603 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -1,18 +1,13 @@ -using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Security; using Umbraco.Core.Serialization; -using Umbraco.Infrastructure.BackOffice; using Umbraco.Net; using Umbraco.Web.Actions; using Umbraco.Web.BackOffice.Authorization; -using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Authorization; diff --git a/src/Umbraco.Web.BackOffice/Extensions/WebMappingProfiles.cs b/src/Umbraco.Web.BackOffice/Extensions/WebMappingProfiles.cs index 600ff101fe..6df63a1655 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/WebMappingProfiles.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/WebMappingProfiles.cs @@ -1,8 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core; -using Umbraco.Core.BackOffice; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Builder; -using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Mapping; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs index b5974c870a..1ccb94e988 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSessionIdValidator.cs @@ -1,4 +1,4 @@ - + using System; using System.Security.Claims; using System.Threading.Tasks; @@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; +using Umbraco.Core.Security; using Umbraco.Extensions; namespace Umbraco.Web.BackOffice.Security diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index 5f0757ea9c..ef6d278554 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -1,8 +1,6 @@ -using Microsoft.Extensions.Options; using System; -using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Compose; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Membership; diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs index 31b5de2e43..989c852350 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Security; namespace Umbraco.Web.BackOffice.Security { diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 1a4298cd6b..dd92801d59 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Web.Models; using IUser = Umbraco.Core.Models.Membership.IUser; diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 9f77cdb7d4..230faeff28 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; using System.Security.Principal; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; @@ -21,7 +17,7 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.Security { - public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager + public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager { private readonly IHttpContextAccessor _httpContextAccessor; @@ -43,6 +39,11 @@ namespace Umbraco.Web.Common.Security _httpContextAccessor = httpContextAccessor; } + /// + /// Gets or sets the default back office user password checker + /// + public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } // TODO: This isn't a good way to set this, it needs to be injected + /// /// /// By default this uses the standard ASP.Net Identity approach which is: diff --git a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs index 675262fd7a..a9f7b0ae74 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Identity; @@ -18,27 +17,29 @@ namespace Umbraco.Web.Common.Security /// /// Abstract class for Umbraco User Managers for back office users or front-end members /// - /// The type of user - public abstract class UmbracoUserManager : UserManager - where T : UmbracoIdentityUser + /// The type of user + /// /// The type password config + public abstract class UmbracoUserManager : UserManager + where TUser : UmbracoIdentityUser + where TPasswordConfig: class, IPasswordConfiguration, new() { private PasswordGenerator _passwordGenerator; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public UmbracoUserManager( IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - BackOfficeLookupNormalizer keyNormalizer, - BackOfficeIdentityErrorDescriber errors, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, IServiceProvider services, - ILogger> logger, - IOptions passwordConfiguration) + ILogger> logger, + IOptions passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); @@ -67,9 +68,10 @@ namespace Umbraco.Web.Common.Security /// True if the sesion is valid, else false public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) { - var userSessionStore = Store as IUserSessionStore; + var userSessionStore = Store as IUserSessionStore; // if this is not set, for backwards compat (which would be super rare), we'll just approve it + // TODO: This should be removed after members supports this if (userSessionStore == null) { return true; @@ -83,14 +85,9 @@ namespace Umbraco.Web.Common.Security /// /// The /// An - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); + protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); - /// - /// Gets or sets the default back office user password checker - /// - public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - - public IPasswordConfiguration PasswordConfiguration { get; protected set; } + public IPasswordConfiguration PasswordConfiguration { get; } public IIpResolver IpResolver { get; } @@ -110,7 +107,7 @@ namespace Umbraco.Web.Common.Security } /// - public override async Task CheckPasswordAsync(T user, string password) + public override async Task CheckPasswordAsync(TUser user, string password) { // we cannot proceed if the user passed in does not have an identity if (user.HasIdentity == false) @@ -135,7 +132,7 @@ namespace Umbraco.Web.Common.Security /// public virtual async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) { - T user = await FindByIdAsync(userId.ToString()); + TUser user = await FindByIdAsync(userId.ToString()); if (user == null) { throw new InvalidOperationException("Could not find user"); @@ -148,9 +145,9 @@ namespace Umbraco.Web.Common.Security /// /// This is copied from the underlying .NET base class since they decided to not expose it /// - private IUserSecurityStampStore GetSecurityStore() + private IUserSecurityStampStore GetSecurityStore() { - var store = Store as IUserSecurityStampStore; + var store = Store as IUserSecurityStampStore; if (store == null) { throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); @@ -165,7 +162,7 @@ namespace Umbraco.Web.Common.Security private static string NewSecurityStamp() => Guid.NewGuid().ToString(); /// - public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) + public override async Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd) { if (user == null) { @@ -185,14 +182,14 @@ namespace Umbraco.Web.Common.Security } /// - public override async Task ResetAccessFailedCountAsync(T user) + public override async Task ResetAccessFailedCountAsync(TUser user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var lockoutStore = (IUserLockoutStore)Store; + var lockoutStore = (IUserLockoutStore)Store; var accessFailedCount = await GetAccessFailedCountAsync(user); if (accessFailedCount == 0) @@ -209,14 +206,14 @@ namespace Umbraco.Web.Common.Security /// Overrides the Microsoft ASP.NET user management method /// /// - public override async Task AccessFailedAsync(T user) + public override async Task AccessFailedAsync(TUser user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var lockoutStore = Store as IUserLockoutStore; + var lockoutStore = Store as IUserLockoutStore; if (lockoutStore == null) { throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 901e7bf81b..46b6540d73 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -2,8 +2,6 @@ using System; using System.DirectoryServices.AccountManagement; using System.Threading.Tasks; using Microsoft.Extensions.Options; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; diff --git a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs deleted file mode 100644 index 6ce61c90d6..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.Owin; -using Microsoft.Owin.Security.Cookies; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Hosting; -using Umbraco.Core.Security; - -namespace Umbraco.Web.Security -{ - // TODO: Migrate this logic to cookie events in ConfigureUmbracoBackOfficeCookieOptions - - public class BackOfficeCookieAuthenticationProvider : CookieAuthenticationProvider - { - private readonly IUserService _userService; - private readonly IRuntimeState _runtimeState; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly SecuritySettings _securitySettings; - - public BackOfficeCookieAuthenticationProvider(IUserService userService, IRuntimeState runtimeState, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IOptions securitySettings) - { - _userService = userService; - _runtimeState = runtimeState; - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; - _securitySettings = securitySettings.Value; - } - - - public override void ResponseSignOut(CookieResponseSignOutContext context) - { - - } - - - - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs deleted file mode 100644 index 010c2d4d33..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Diagnostics; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Microsoft.Owin; -using Microsoft.Owin.Logging; -using Microsoft.Owin.Security; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Security; - -namespace Umbraco.Web.Security -{ - // TODO: This has been migrated to netcore - public class BackOfficeSignInManager : IDisposable - { - private readonly IBackOfficeUserManager _userManager; - private readonly IUserClaimsPrincipalFactory _claimsPrincipalFactory; - private readonly IAuthenticationManager _authenticationManager; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; - private readonly IOwinRequest _request; - - public BackOfficeSignInManager( - IBackOfficeUserManager userManager, - IUserClaimsPrincipalFactory claimsPrincipalFactory, - IAuthenticationManager authenticationManager, - ILogger logger, - GlobalSettings globalSettings, - IOwinRequest request) - { - _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - _claimsPrincipalFactory = claimsPrincipalFactory ?? throw new ArgumentNullException(nameof(claimsPrincipalFactory)); - _authenticationManager = authenticationManager ?? throw new ArgumentNullException(nameof(authenticationManager)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); - _request = request ?? throw new ArgumentNullException(nameof(request)); - } - - public void Dispose() - { - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b1ddf26b05..50d379102d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -159,7 +159,6 @@ - @@ -181,7 +180,6 @@ - @@ -306,4 +304,4 @@ - + \ No newline at end of file From 4dea624e23cfef3b1d95d47c35b9d2539df76530 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 02:28:11 +1100 Subject: [PATCH 09/14] remove code --- src/Umbraco.Web.Common/Security/UmbracoUserManager.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs index a9f7b0ae74..68b9011aa4 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs @@ -156,11 +156,6 @@ namespace Umbraco.Web.Common.Security return store; } - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - private static string NewSecurityStamp() => Guid.NewGuid().ToString(); - /// public override async Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd) { From 5172b0e58a7c56ac0ba3ff00d47e1212f14f5415 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 12:44:27 +1100 Subject: [PATCH 10/14] Updates user manager, user store and identity user to use the aspnetcore base classes instead of copies of our own, uses string ids for user and roles to simplify everything and to allow for sharing between members --- src/Umbraco.Core/Constants-Security.cs | 6 +- .../Models/Identity/ExternalLogin.cs | 5 +- .../Models/Identity/IExternalLogin.cs | 13 +- .../Models/Identity/IdentityUserClaim.cs | 28 - .../Models/Identity/IdentityUserLogin.cs | 6 + .../Models/Identity/IdentityUserRole.cs | 19 - .../Security/IdentityAuditEventArgs.cs | 10 +- .../Security/UmbracoBackOfficeIdentity.cs | 8 +- .../CoreMappingProfiles.cs | 3 +- .../Security/BackOfficeIdentityUser.cs | 33 +- .../Security/BackOfficeUserStore.cs | 757 ++++++++---------- .../IBackOfficeUserPasswordChecker.cs | 0 .../Security/IUmbracoUserManager.cs | 24 +- .../Security/IUserSessionStore.cs | 8 +- .../Security/IdentityMapDefinition.cs | 2 +- .../Security/SignOutAuditEventArgs.cs | 2 +- .../Security}/UmbracoIdentityUser.cs | 76 +- .../Security/UmbracoUserManager.cs | 32 +- .../Security/UserInviteEventArgs.cs | 4 +- ...kOfficeServiceCollectionExtensionsTests.cs | 1 + .../BackOfficeClaimsPrincipalFactoryTests.cs | 4 +- .../UmbracoBackOfficeIdentityTests.cs | 6 +- .../ClaimsPrincipalExtensionsTests.cs | 4 +- .../Security/BackOfficeAntiforgeryTests.cs | 2 +- .../AuthenticateEverythingMiddleware.cs | 5 +- .../Controllers/AuthenticationController.cs | 12 +- .../Controllers/BackOfficeController.cs | 2 +- .../Security/BackOfficeUserManagerAuditer.cs | 89 +- .../Security/PasswordChanger.cs | 2 +- .../Security/BackOfficeUserManager.cs | 30 +- 30 files changed, 541 insertions(+), 652 deletions(-) delete mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs delete mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserRole.cs rename src/{Umbraco.Core => Umbraco.Infrastructure}/Security/BackOfficeIdentityUser.cs (85%) rename src/{Umbraco.Core => Umbraco.Infrastructure}/Security/IBackOfficeUserPasswordChecker.cs (100%) rename src/{Umbraco.Core => Umbraco.Infrastructure}/Security/IdentityMapDefinition.cs (96%) rename src/{Umbraco.Core/Models/Identity => Umbraco.Infrastructure/Security}/UmbracoIdentityUser.cs (81%) rename src/{Umbraco.Web.Common => Umbraco.Infrastructure}/Security/UmbracoUserManager.cs (90%) diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 24b8b20731..9a4936d42d 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core +namespace Umbraco.Core { public static partial class Constants { @@ -11,6 +11,8 @@ /// public const int SuperUserId = -1; + public const string SuperUserIdAsString = "-1"; + /// /// The id for the 'unknown' user. /// @@ -22,7 +24,7 @@ /// /// The name of the 'unknown' user. /// - public const string UnknownUserName = "SYTEM"; + public const string UnknownUserName = "SYSTEM"; public const string AdminGroupAlias = "admin"; public const string EditorGroupAlias = "editor"; diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs index 6e4abf2906..a5de9da0cb 100644 --- a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs +++ b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs @@ -1,10 +1,13 @@ -using System; +using System; namespace Umbraco.Core.Models.Identity { /// public class ExternalLogin : IExternalLogin { + /// + /// Initializes a new instance of the class. + /// public ExternalLogin(string loginProvider, string providerKey, string userData = null) { LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs index 68f66a5cee..2718802324 100644 --- a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs @@ -1,12 +1,23 @@ -namespace Umbraco.Core.Models.Identity +namespace Umbraco.Core.Models.Identity { /// /// Used to persist external login data for a user /// public interface IExternalLogin { + /// + /// Gets the login provider + /// string LoginProvider { get; } + + /// + /// Gets the provider key + /// string ProviderKey { get; } + + /// + /// Gets the user data + /// string UserData { get; } } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs deleted file mode 100644 index 2524463284..0000000000 --- a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.Core.Models.Identity -{ - /// - /// EntityType that represents one specific user claim - /// - public class IdentityUserClaim - { - /// - /// Gets or sets primary key - /// - public virtual string Id { get; set; } // TODO: Not used - - /// - /// Gets or sets user Id for the user who owns this login - /// - public virtual string UserId { get; set; } - - /// - /// Gets or sets claim type - /// - public virtual string ClaimType { get; set; } - - /// - /// Gets or sets claim value - /// - public virtual string ClaimValue { get; set; } - } -} diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index 1ae19da128..5974822c20 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -9,6 +9,9 @@ namespace Umbraco.Core.Models.Identity /// public class IdentityUserLogin : EntityBase, IIdentityUserLogin { + /// + /// Initializes a new instance of the class. + /// public IdentityUserLogin(string loginProvider, string providerKey, string userId) { LoginProvider = loginProvider; @@ -16,6 +19,9 @@ namespace Umbraco.Core.Models.Identity UserId = userId; } + /// + /// Initializes a new instance of the class. + /// public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) { Id = id; diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs deleted file mode 100644 index 8a0b6b891d..0000000000 --- a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Umbraco.Core.Models.Identity -{ - /// - /// EntityType that represents a user belonging to a role - /// - /// - public class IdentityUserRole - { - /// - /// Gets or sets userId for the user that is in the role - /// - public virtual int UserId { get; set; } - - /// - /// Gets or sets roleId for the role - /// - public virtual string RoleId { get; set; } - } -} diff --git a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs index 454d651944..b9884c8e7d 100644 --- a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs @@ -27,12 +27,12 @@ namespace Umbraco.Core.Security /// /// The user affected by the event raised /// - public int AffectedUser { get; private set; } + public string AffectedUser { get; private set; } /// /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 /// - public int PerformingUser { get; private set; } + public string PerformingUser { get; private set; } /// /// An optional comment about the action being logged @@ -53,7 +53,7 @@ namespace Umbraco.Core.Security /// /// /// - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser, string comment, int affectedUser, string affectedUsername) + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) { DateTimeUtc = DateTime.UtcNow; Action = action; @@ -64,8 +64,8 @@ namespace Umbraco.Core.Security AffectedUser = affectedUser; } - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser, string comment, string affectedUsername) - : this(action, ipAddress, performingUser, comment, -1, affectedUsername) + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) + : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) { } diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 3430814f83..5fd9f23c92 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -53,7 +53,7 @@ namespace Umbraco.Core.Security /// /// /// - public UmbracoBackOfficeIdentity(int userId, string username, string realName, + public UmbracoBackOfficeIdentity(string userId, string username, string realName, IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true @@ -87,7 +87,7 @@ namespace Umbraco.Core.Security /// /// public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity, - int userId, string username, string realName, + string userId, string username, string realName, IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType) @@ -126,13 +126,13 @@ namespace Umbraco.Core.Security /// /// Adds claims based on the ctor data /// - private void AddRequiredClaims(int userId, string username, string realName, + private void AddRequiredClaims(string userId, string username, string realName, IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) { //This is the id that 'identity' uses to check for the user id if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) - AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, Issuer, Issuer, this)); if (HasClaim(x => x.Type == ClaimTypes.Name) == false) AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this)); diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs index ed45f24a96..90701a888e 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Builder; using Umbraco.Core.Mapping; using Umbraco.Core.Security; @@ -19,7 +19,6 @@ namespace Umbraco.Core.Composing.CompositionExtensions builder.Services.AddUnique(); builder.WithCollectionBuilder() - .Add() .Add() .Add() .Add() diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs similarity index 85% rename from src/Umbraco.Core/Security/BackOfficeIdentityUser.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 4de1ae4d0f..e2e8031768 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Identity; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; @@ -16,13 +17,13 @@ namespace Umbraco.Core.Security private string _name; private string _passwordConfig; private string _culture; - private IReadOnlyUserGroup[] _groups; + private IReadOnlyCollection _groups; private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; // Custom comparer for enumerables - private static readonly DelegateEqualityComparer s_groupsComparer = new DelegateEqualityComparer( + private static readonly DelegateEqualityComparer> s_groupsComparer = new DelegateEqualityComparer>( (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), groups => groups.GetHashCode()); @@ -51,9 +52,7 @@ namespace Umbraco.Core.Security user.UserName = username; user.Email = email; - // we are setting minvalue here because the default is "0" which is the id of the admin user - // which we cannot allow because the admin user will always exist - user.Id = int.MinValue; + user.Id = null; user.HasIdentity = false; user._culture = culture; user._name = name; @@ -61,7 +60,7 @@ namespace Umbraco.Core.Security return user; } - private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyUserGroup[] groups) + private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyCollection groups) { _startMediaIds = Array.Empty(); _startContentIds = Array.Empty(); @@ -79,7 +78,7 @@ namespace Umbraco.Core.Security : this(globalSettings, groups.ToArray()) { // use the property setters - they do more than just setting a field - Id = userId; + Id = UserIdToString(userId); } public int[] CalculatedMediaStartNodeIds { get; set; } @@ -141,13 +140,19 @@ namespace Umbraco.Core.Security /// public string[] AllowedSections => _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); + /// + /// Gets or sets the culture + /// public string Culture { get => _culture; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); } - public IReadOnlyUserGroup[] Groups + /// + /// Gets or sets the user groups + /// + public IReadOnlyCollection Groups { get => _groups; set @@ -155,13 +160,13 @@ namespace Umbraco.Core.Security // so they recalculate _allowedSections = null; - _groups = value; + _groups = value.Where(x => x.Alias != null).ToArray(); - var roles = new List(); - foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole + var roles = new List>(); + foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole { RoleId = x.Alias, - UserId = Id + UserId = Id?.ToString() })) { roles.Add(identityUserRole); @@ -174,7 +179,6 @@ namespace Umbraco.Core.Security } } - /// /// Gets a value indicating whether the user is locked out based on the user's lockout end date /// @@ -182,7 +186,7 @@ namespace Umbraco.Core.Security { get { - var isLocked = LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now; + var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; return isLocked; } } @@ -192,5 +196,6 @@ namespace Umbraco.Core.Security /// public bool IsApproved { get; set; } + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 4b4383c402..befa5ebac2 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Linq; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -12,28 +14,16 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Scoping; -using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Core.Security { // TODO: Make this into a base class that can be re-used - public class BackOfficeUserStore : DisposableObjectSlim, - IUserPasswordStore, - IUserEmailStore, - IUserLoginStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserLockoutStore, - IUserSessionStore - - // TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco - // IUserTwoFactorStore, - // 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 + /// + /// The user store for back office users + /// + public class BackOfficeUserStore : UserStoreBase, string, IdentityUserClaim, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { private readonly IScopeProvider _scopeProvider; private readonly IUserService _userService; @@ -41,12 +31,19 @@ namespace Umbraco.Core.Security private readonly IExternalLoginService _externalLoginService; private readonly GlobalSettings _globalSettings; private readonly UmbracoMapper _mapper; - private bool _disposed = false; /// /// Initializes a new instance of the class. /// - public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper) + public BackOfficeUserStore( + IScopeProvider scopeProvider, + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IOptions globalSettings, + UmbracoMapper mapper, + IdentityErrorDescriber describer) + : base(describer) { _scopeProvider = scopeProvider; _userService = userService ?? throw new ArgumentNullException(nameof(userService)); @@ -59,61 +56,32 @@ namespace Umbraco.Core.Security } /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// Not supported in Umbraco /// - protected override void DisposeResources() => _disposed = true; + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override IQueryable Users => throw new NotImplementedException(); - public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + /// + public override Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken); + + /// + public override Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken); + + /// + public override Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + 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 + // 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 + // 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")); @@ -133,15 +101,18 @@ namespace Umbraco.Core.Security _userService.Save(userEntity); - if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); + if (!userEntity.HasIdentity) + { + throw new DataException("Could not create the user, check logs for details"); + } - //re-assign id - user.Id = userEntity.Id; + // re-assign id + user.Id = UserIdToString(userEntity.Id); if (isLoginsPropertyDirty) { _externalLoginService.Save( - user.Id, + userEntity.Id, user.Logins.Select(x => new ExternalLogin( x.LoginProvider, x.ProviderKey, @@ -151,24 +122,25 @@ namespace Umbraco.Core.Security return Task.FromResult(IdentityResult.Success); } - /// - /// Update a user - /// - public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + /// + public override Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var asInt = user.Id.TryConvertTo(); + Attempt asInt = user.Id.TryConvertTo(); if (asInt == false) { throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } - using (var scope = _scopeProvider.CreateScope()) + using (IScope scope = _scopeProvider.CreateScope()) { - var found = _userService.GetUserById(asInt.Result); + IUser found = _userService.GetUserById(asInt.Result); if (found != null) { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. @@ -196,263 +168,233 @@ namespace Umbraco.Core.Security return Task.FromResult(IdentityResult.Success); } - /// - /// Delete a user - /// - public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + /// + public override Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var found = _userService.GetUserById(user.Id); + IUser found = _userService.GetUserById(UserIdToInt(user.Id)); if (found != null) { _userService.Delete(found); } - _externalLoginService.DeleteUserLogins(user.Id); + + _externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); return Task.FromResult(IdentityResult.Success); } - /// - /// Finds a user - /// - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); + + /// + protected override Task FindUserAsync(string userId, CancellationToken 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); + IUser user = _userService.GetUserById(UserIdToInt(userId)); if (user == null) { return null; } - var result = AssignLoginsCallback(_mapper.Map(user)); + return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); + } + + /// + public override async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IUser user = _userService.GetByUsername(userName); + if (user == null) + { + return null; + } + + BackOfficeIdentityUser 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)) + /// + public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default) { - 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)); + await base.SetPasswordHashAsync(user, passwordHash, cancellationToken); - user.PasswordHash = passwordHash; user.PasswordConfig = null; // Clear this so that it's reset at the repository level user.LastPasswordChangeDateUtc = DateTime.UtcNow; - - return Task.CompletedTask; } - /// - /// Get the user password hash - /// - public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + /// + public override async Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + // This checks if it's null + var result = await base.HasPasswordAsync(user, cancellationToken); + if (result) + { + // we also want to check empty + return string.IsNullOrEmpty(user.PasswordHash) == false; + } + + return result; + } + + /// + public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) { 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(nameof(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 + IUser user = _userService.GetByEmail(email); + BackOfficeIdentityUser result = user == null ? null : _mapper.Map(user); return Task.FromResult(AssignLoginsCallback(result)); } - public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + /// + public override Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) => GetEmailAsync(user, cancellationToken); - public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + /// + public override Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) => 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)) + /// + 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)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var logins = user.Logins; + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + + ICollection logins = user.Logins; var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString()); - var userLogin = instance; + IdentityUserLogin 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)) + /// + public override Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (userLogin != null) user.Logins.Remove(userLogin); + IIdentityUserLogin 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)) + /// + 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()); - } - - /// - /// 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 - IIdentityUserLogin[] result = _externalLoginService.Find(loginProvider, providerKey).ToArray(); - if (result.Any()) + if (user == null) { - // return the first user that matches the result - BackOfficeIdentityUser output = null; - foreach (IIdentityUserLogin l in result) - { - // TODO: This won't be necessary once we add GUID support for users and make the external login - // table uses GUIDs without referential integrity - if (int.TryParse(l.UserId, out int userId)) - { - IUser user = _userService.GetUserById(userId); - if (user != null) - { - output = _mapper.Map(user); - break; - } - } - } - - return Task.FromResult(AssignLoginsCallback(output)); + throw new ArgumentNullException(nameof(user)); } - return Task.FromResult(null); + return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); } + /// + protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + BackOfficeIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (user == 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((IdentityUserLogin)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 + }); + } /// /// Adds a user to a role (user group) /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + public override Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) { 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)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + 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)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); if (userRole == null) { @@ -465,15 +407,26 @@ namespace Umbraco.Core.Security /// /// Removes the role (user group) for the user /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + public override Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) { 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)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + 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)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); if (userRole != null) { @@ -486,22 +439,30 @@ namespace Umbraco.Core.Security /// /// Returns the roles (user groups) for this user /// - public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public override Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + 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)) + public override Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); } @@ -511,43 +472,62 @@ namespace Umbraco.Core.Security /// /// Identity Role names are equal to Umbraco UserGroup alias. /// - public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } - var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); + IUserGroup userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); - var users = _userService.GetAllInGroup(userGroup.Id); + IEnumerable users = _userService.GetAllInGroup(userGroup.Id); IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList(); return Task.FromResult(backOfficeIdentityUsers); } - /// - /// Set the security stamp for the user - /// - public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + /// + protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + IUserGroup group = _userService.GetUserGroupByAlias(normalizedRoleName); + if (group == null) + { + return Task.FromResult((IdentityRole)null); + } - user.SecurityStamp = stamp; - return Task.CompletedTask; + return Task.FromResult(new IdentityRole(group.Name) + { + Id = group.Alias + }); } - /// - /// Get the user security stamp - /// - public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + /// + 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; + } + + /// + public override Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); + 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 + // 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); @@ -557,157 +537,65 @@ namespace Umbraco.Core.Security { if (user != null) { - user.SetLoginsCallback(new Lazy>(() => - _externalLoginService.GetAll(user.Id))); + user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); } + return user; } - #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 - + // 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(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) - || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + || (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 - var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); + + // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); user.LastLoginDate = dt; } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) - || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) - || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) + || (user.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false) + || (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value)) { anythingChanged = true; user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed)) - || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) - || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.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?)DateTime.Now : null; } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } + 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.IsLockedOut != identityUser.IsLockedOut) { anythingChanged = true; @@ -715,17 +603,18 @@ namespace Umbraco.Core.Security if (user.IsLockedOut) { - //need to set the last lockout date + // 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) { @@ -740,18 +629,21 @@ namespace Umbraco.Core.Security 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; @@ -800,11 +692,7 @@ namespace Umbraco.Core.Security return anythingChanged; } - private void ThrowIfDisposed() - { - if (_disposed) throw new ObjectDisposedException(GetType().Name); - } - + /// public Task ValidateSessionIdAsync(string userId, string sessionId) { if (Guid.TryParse(sessionId, out Guid guidSessionId)) @@ -817,10 +705,73 @@ namespace Umbraco.Core.Security private static int UserIdToInt(string userId) { - var attempt = userId.TryConvertTo(); - if (attempt.Success) return attempt.Result; + Attempt attempt = userId.TryConvertTo(); + if (attempt.Success) + { + return attempt.Result; + } throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); } + + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task> GetClaimsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task AddClaimsAsync(BackOfficeIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task ReplaceClaimAsync(BackOfficeIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task RemoveClaimsAsync(BackOfficeIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + // TODO: We should support these + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task> FindTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task AddUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override Task RemoveUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); } } diff --git a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs similarity index 100% rename from src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs rename to src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index c50b012dae..4bec4c9c7a 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -42,22 +42,18 @@ namespace Umbraco.Core.Security /// /// Gets the external logins for the user /// - /// /// A representing the result of the asynchronous operation. Task> GetLoginsAsync(TUser user); /// /// Deletes a user /// - /// /// A representing the result of the asynchronous operation. Task DeleteAsync(TUser user); /// /// Finds a user by the external login provider /// - /// - /// /// A representing the result of the asynchronous operation. Task FindByLoginAsync(string loginProvider, string providerKey); @@ -82,15 +78,11 @@ namespace Umbraco.Core.Security /// /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event /// - /// - /// - /// - /// /// /// We use this because in the back office the only way an admin can change another user's password without first knowing their password /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset /// - Task ChangePasswordWithResetAsync(int userId, string token, string newPassword); + Task ChangePasswordWithResetAsync(string userId, string token, string newPassword); /// /// Validates that an email confirmation token matches the specified . @@ -130,8 +122,6 @@ namespace Umbraco.Core.Security /// /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date /// - /// - /// /// /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values /// @@ -192,7 +182,6 @@ namespace Umbraco.Core.Security /// Task AddPasswordAsync(TUser user, string password); - /// /// Returns a flag indicating whether the given is valid for the /// specified . @@ -220,8 +209,6 @@ namespace Umbraco.Core.Security /// /// Used to validate a user's session /// - /// - /// /// Returns true if the session is valid, otherwise false Task ValidateSessionIdAsync(string userId, string sessionId); @@ -323,15 +310,12 @@ namespace Umbraco.Core.Security /// /// Resets the access failed count for the user /// - /// /// A representing the result of the asynchronous operation. Task ResetAccessFailedCountAsync(TUser user); /// /// Generates a two factor token for the user /// - /// - /// /// A representing the result of the asynchronous operation. Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider); @@ -356,9 +340,9 @@ namespace Umbraco.Core.Security // let's see if there's a way to avoid that and only have these called within signinmanager and usermanager // which means we can remove these from the interface (things like invite seems like they cannot be moved) // TODO: When we change to not having the crappy static events this will need to be revisited - void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); - void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); - SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId); + void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId); + SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId); UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser); bool HasSendingUserInviteEventHandler { get; } diff --git a/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs index 06b7c2f165..c68d1f13f9 100644 --- a/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs +++ b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs @@ -1,15 +1,17 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; namespace Umbraco.Core.Security { /// /// An IUserStore interface part to implement if the store supports validating user session Ids /// - /// - public interface IUserSessionStore : IUserStore + /// The user type + public interface IUserSessionStore where TUser : class { + /// + /// Validates a user's session is still valid + /// Task ValidateSessionIdAsync(string userId, string sessionId); } } diff --git a/src/Umbraco.Core/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs similarity index 96% rename from src/Umbraco.Core/Security/IdentityMapDefinition.cs rename to src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 26a5d11f6e..aebb2de5bf 100644 --- a/src/Umbraco.Core/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -65,7 +65,7 @@ namespace Umbraco.Core.Security target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; - target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; + target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; // this was in AutoMapper but does not have a setter anyways //target.AllowedSections = source.AllowedSections.ToArray(), diff --git a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index 2e5997b603..626932640c 100644 --- a/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Security /// public class SignOutAuditEventArgs : IdentityAuditEventArgs { - public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1) + public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, string performingUser = Constants.Security.SuperUserIdAsString, string affectedUser = Constants.Security.SuperUserIdAsString) : base(action, ipAddress, performingUser, comment, affectedUser, null) { } diff --git a/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs similarity index 81% rename from src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs rename to src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs index ffa549ab47..1b888123be 100644 --- a/src/Umbraco.Core/Models/Identity/UmbracoIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -3,23 +3,33 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using Microsoft.AspNetCore.Identity; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models.Identity { + /// /// Abstract class for use in Umbraco Identity for users and members /// /// + /// + /// This uses strings for the ID of the user, claims, roles. This is because aspnetcore identity's base store will + /// not support having an INT user PK and a string role PK with the way they've made the generics. So we will just use + /// string for both which makes things more flexible anyways for users and members and also if/when we transition to + /// GUID support + /// + /// /// This class was originally borrowed from the EF implementation in Identity prior to netcore. /// The new IdentityUser in netcore does not have properties such as Claims, Roles and Logins and those are instead /// by default managed with their default user store backed by EF which utilizes EF's change tracking to track these values /// to a user. We will continue using this approach since it works fine for what we need which does the change tracking of /// claims, roles and logins directly on the user model. + /// /// - public abstract class UmbracoIdentityUser : IRememberBeingDirty + public abstract class UmbracoIdentityUser : IdentityUser, IRememberBeingDirty { - private int _id; + private string _id; private string _email; private string _userName; private DateTime? _lastLoginDateUtc; @@ -29,7 +39,7 @@ namespace Umbraco.Core.Models.Identity private DateTime? _lastPasswordChangeDateUtc; private ObservableCollection _logins; private Lazy> _getLogins; - private ObservableCollection _roles; + private ObservableCollection> _roles; /// /// Initializes a new instance of the class. @@ -37,9 +47,9 @@ namespace Umbraco.Core.Models.Identity public UmbracoIdentityUser() { // must initialize before setting groups - _roles = new ObservableCollection(); + _roles = new ObservableCollection>(); _roles.CollectionChanged += Roles_CollectionChanged; - Claims = new List(); + Claims = new List>(); } public event PropertyChangedEventHandler PropertyChanged @@ -67,7 +77,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets email /// - public string Email + public override string Email { get => _email; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); @@ -76,7 +86,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets a value indicating whether the email is confirmed, default is false /// - public bool EmailConfirmed + public override bool EmailConfirmed { get => _emailConfirmed; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); @@ -85,46 +95,12 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets the salted/hashed form of the user password /// - public string PasswordHash + public override string PasswordHash { get => _passwordHash; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); } - /// - /// Gets or sets a random value that should change whenever a users credentials have changed (password changed, login removed) - /// - public virtual string SecurityStamp { get; set; } - - /// - /// Gets or sets a phone Number for the user - /// - /// - /// This is unused until we or an end-user requires this value for 2FA - /// - public virtual string PhoneNumber { get; set; } - - /// - /// Gets or sets a value indicating whether true if the phone number is confirmed, default is false - /// - /// - /// This is unused until we or an end-user requires this value for 2FA - /// - public virtual bool PhoneNumberConfirmed { get; set; } - - /// - /// Gets or sets a value indicating whether is two factor enabled for the user - /// - /// - /// This is unused until we or an end-user requires this value for 2FA - /// - public virtual bool TwoFactorEnabled { get; set; } - - /// - /// Gets or sets dateTime in UTC when lockout ends, any time in the past is considered not locked out. - /// - public virtual DateTime? LockoutEndDateUtc { get; set; } - /// /// Gets or sets dateTime in UTC when the password was last changed. /// @@ -140,7 +116,7 @@ namespace Umbraco.Core.Models.Identity /// /// Currently this is always true for users and members /// - public bool LockoutEnabled + public override bool LockoutEnabled { get => true; set { } @@ -149,7 +125,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets the value to record failures for the purposes of lockout /// - public int AccessFailedCount + public override int AccessFailedCount { get => _accessFailedCount; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); @@ -158,13 +134,13 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets the user roles collection /// - public ICollection Roles + public ICollection> Roles { get => _roles; set { _roles.CollectionChanged -= Roles_CollectionChanged; - _roles = new ObservableCollection(value); + _roles = new ObservableCollection>(value); _roles.CollectionChanged += Roles_CollectionChanged; } } @@ -172,7 +148,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets navigation the user claims collection /// - public ICollection Claims { get; } + public ICollection> Claims { get; } /// /// Gets the user logins collection @@ -208,7 +184,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets user ID (Primary Key) /// - public int Id + public override string Id { get => _id; set @@ -226,7 +202,7 @@ namespace Umbraco.Core.Models.Identity /// /// Gets or sets user name /// - public string UserName + public override string UserName { get => _userName; set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName)); @@ -281,7 +257,7 @@ namespace Umbraco.Core.Models.Identity /// /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted /// - public void AddRole(string role) => Roles.Add(new IdentityUserRole + public void AddRole(string role) => Roles.Add(new IdentityUserRole { UserId = Id, RoleId = role diff --git a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs similarity index 90% rename from src/Umbraco.Web.Common/Security/UmbracoUserManager.cs rename to src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 68b9011aa4..2ea0bc52b2 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -6,12 +6,10 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; using Umbraco.Net; -namespace Umbraco.Web.Common.Security +namespace Umbraco.Core.Security { /// @@ -21,7 +19,7 @@ namespace Umbraco.Web.Common.Security /// /// The type password config public abstract class UmbracoUserManager : UserManager where TUser : UmbracoIdentityUser - where TPasswordConfig: class, IPasswordConfiguration, new() + where TPasswordConfig : class, IPasswordConfiguration, new() { private PasswordGenerator _passwordGenerator; @@ -87,8 +85,14 @@ namespace Umbraco.Web.Common.Security /// An protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); + /// + /// Gets the password configuration + /// public IPasswordConfiguration PasswordConfiguration { get; } + /// + /// Gets the IP resolver + /// public IIpResolver IpResolver { get; } /// @@ -130,32 +134,18 @@ namespace Umbraco.Web.Common.Security /// We use this because in the back office the only way an admin can change another user's password without first knowing their password /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset /// - public virtual async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + public virtual async Task ChangePasswordWithResetAsync(string userId, string token, string newPassword) { - TUser user = await FindByIdAsync(userId.ToString()); + TUser user = await FindByIdAsync(userId); if (user == null) { throw new InvalidOperationException("Could not find user"); } - IdentityResult result = await base.ResetPasswordAsync(user, token, newPassword); + IdentityResult result = await ResetPasswordAsync(user, token, newPassword); return result; } - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - private IUserSecurityStampStore GetSecurityStore() - { - var store = Store as IUserSecurityStampStore; - if (store == null) - { - throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); - } - - return store; - } - /// public override async Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd) { diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 811092a2c9..80b05497a8 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -6,8 +6,8 @@ namespace Umbraco.Core.Security { public class UserInviteEventArgs : IdentityAuditEventArgs { - public UserInviteEventArgs(string ipAddress, int performingUser, UserInvite invitedUser, IUser localUser, string comment = null) - : base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, localUser.Id, localUser.Name) + public UserInviteEventArgs(string ipAddress, string performingUser, UserInvite invitedUser, IUser localUser, string comment = null) + : base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, string.Intern(localUser.Id.ToString()), localUser.Name) { InvitedUser = invitedUser ?? throw new System.ArgumentNullException(nameof(invitedUser)); User = localUser; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index bf198d9819..d9dee389ee 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Tests.Integration.Testing; +using Umbraco.Web.Common.Security; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index f85c15b3bf..64bdca6437 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -98,7 +98,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice const string expectedClaimType = ClaimTypes.Role; const string expectedClaimValue = "b87309fb-4caf-48dc-b45a-2b752d051508"; - _testUser.Roles.Add(new global::Umbraco.Core.Models.Identity.IdentityUserRole{RoleId = expectedClaimValue}); + _testUser.Roles.Add(new IdentityUserRole { RoleId = expectedClaimValue }); _mockUserManager.Setup(x => x.SupportsUserRole).Returns(true); _mockUserManager.Setup(x => x.GetRolesAsync(_testUser)).ReturnsAsync(new[] {expectedClaimValue}); @@ -115,7 +115,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice const string expectedClaimType = "custom"; const string expectedClaimValue = "val"; - _testUser.Claims.Add(new global::Umbraco.Core.Models.Identity.IdentityUserClaim {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); + _testUser.Claims.Add(new IdentityUserClaim { ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); _mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true); _mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync( new List {new Claim(expectedClaimType, expectedClaimValue)}); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 8dcaafafcb..79a9456643 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Security.Claims; using NUnit.Framework; @@ -103,7 +103,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice }); var identity = new UmbracoBackOfficeIdentity(claimsIdentity, - 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); + "1234", "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); Assert.AreEqual(12, identity.Claims.Count()); Assert.IsNull(identity.Actor); @@ -116,7 +116,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var securityStamp = Guid.NewGuid().ToString(); var identity = new UmbracoBackOfficeIdentity( - 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); + "1234", "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); // this will be filtered out during cloning identity.AddClaim(new Claim(Constants.Security.TicketExpiresClaimType, "test")); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs index 30706b1b67..ad0f292fae 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -15,7 +15,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions [Test] public void Get_Remaining_Ticket_Seconds() { - var backOfficeIdentity = new UmbracoBackOfficeIdentity(-1, "test", "test", + var backOfficeIdentity = new UmbracoBackOfficeIdentity(Constants.Security.SuperUserIdAsString, "test", "test", Enumerable.Empty(), Enumerable.Empty(), "en-US", Guid.NewGuid().ToString(), Enumerable.Empty(), Enumerable.Empty()); var principal = new ClaimsPrincipal(backOfficeIdentity); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs index ccebe17b09..7899ef39c2 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgeryTests.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Security var httpContext = new DefaultHttpContext() { User = new ClaimsPrincipal(new UmbracoBackOfficeIdentity( - -1, + Constants.Security.SuperUserIdAsString, "test", "test", Enumerable.Empty(), diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs index 3673bdf333..e702753236 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs @@ -29,9 +29,10 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting { var securityStamp = Guid.NewGuid().ToString(); var identity = new UmbracoBackOfficeIdentity( - -1, "admin", "Admin", new []{-1}, new[] { -1 }, "en-US", securityStamp, new[] { "content", "media", "members" }, new[] { "admin" }); + Umbraco.Core.Constants.Security.SuperUserIdAsString, "admin", "Admin", new[] { -1 }, new[] { -1 }, "en-US", securityStamp, new[] { "content", "media", "members" }, new[] { "admin" }); - return Task.FromResult(new AuthenticationTicket(identity, + return Task.FromResult(new AuthenticationTicket( + identity, new AuthenticationProperties() { ExpiresUtc = DateTime.Now.AddDays(1) diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index f7e10d77af..36e5c2b6fe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -390,7 +390,7 @@ namespace Umbraco.Web.BackOffice.Controllers await _emailSender.SendAsync(mailMessage); - _userManager.RaiseForgotPasswordRequestedEvent(User, user.Id); + _userManager.RaiseForgotPasswordRequestedEvent(User, user.Id.ToString()); } } @@ -554,7 +554,7 @@ namespace Umbraco.Web.BackOffice.Controllers } } - _userManager.RaiseForgotPasswordChangedSuccessEvent(User, model.UserId); + _userManager.RaiseForgotPasswordChangedSuccessEvent(User, model.UserId.ToString()); return Ok(); } @@ -577,7 +577,7 @@ namespace Umbraco.Web.BackOffice.Controllers _logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress); - var userId = int.Parse(result.Principal.Identity.GetUserId()); + var userId = result.Principal.Identity.GetUserId(); var args = _userManager.RaiseLogoutSuccessEvent(User, userId); if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace()) { @@ -608,10 +608,12 @@ namespace Umbraco.Web.BackOffice.Controllers return userDetail; } - private string ConstructCallbackUrl(int userId, string code) + private string ConstructCallbackUrl(string userId, string code) { // Get an mvc helper to get the url - var action = _linkGenerator.GetPathByAction(nameof(BackOfficeController.ValidatePasswordResetCode), ControllerExtensions.GetControllerName(), + var action = _linkGenerator.GetPathByAction( + nameof(BackOfficeController.ValidatePasswordResetCode), + ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.BackOfficeArea, diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 89b121b575..19fb6aa2df 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -433,7 +433,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (result == Microsoft.AspNetCore.Identity.SignInResult.Success) { - } + } else if (result == Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired) { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index ef6d278554..81be953d22 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -33,29 +33,27 @@ namespace Umbraco.Web.Common.Security { // NOTE: This was migrated as-is from v8 including these missing entries // TODO: See note about static events in BackOfficeUserManager - //BackOfficeUserManager.AccountLocked += ; - //BackOfficeUserManager.AccountUnlocked += ; BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; BackOfficeUserManager.LoginFailed += OnLoginFailed; - //BackOfficeUserManager.LoginRequiresVerification += ; BackOfficeUserManager.LoginSuccess += OnLoginSuccess; BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess; BackOfficeUserManager.PasswordChanged += OnPasswordChanged; BackOfficeUserManager.PasswordReset += OnPasswordReset; - //BackOfficeUserManager.ResetAccessFailedCount += ; } - private IUser GetPerformingUser(int userId) + private IUser GetPerformingUser(string userId) { - var found = userId >= 0 ? _userService.GetUserById(userId) : null; + if (!int.TryParse(userId, out int asInt)) + { + return AuditEventsComponent.UnknownUser(_globalSettings); + } + + IUser found = asInt >= 0 ? _userService.GetUserById(asInt) : null; return found ?? AuditEventsComponent.UnknownUser(_globalSettings); } - private static string FormatEmail(IMembershipUser user) - { - return user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - } + private static string FormatEmail(IMembershipUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; private void OnLoginSuccess(object sender, IdentityAuditEventArgs args) { @@ -65,70 +63,78 @@ namespace Umbraco.Web.Common.Security private void OnLogoutSuccess(object sender, IdentityAuditEventArgs args) { - var performingUser = GetPerformingUser(args.PerformingUser); + IUser performingUser = GetPerformingUser(args.PerformingUser); WriteAudit(performingUser, args.AffectedUser, args.IpAddress, "umbraco/user/sign-in/logout", "logout success"); } - private void OnPasswordReset(object sender, IdentityAuditEventArgs args) - { - WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/reset", "password reset"); - } + private void OnPasswordReset(object sender, IdentityAuditEventArgs args) => WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/reset", "password reset"); - private void OnPasswordChanged(object sender, IdentityAuditEventArgs args) - { - WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/change", "password change"); - } + private void OnPasswordChanged(object sender, IdentityAuditEventArgs args) => WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/change", "password change"); - private void OnLoginFailed(object sender, IdentityAuditEventArgs args) - { - WriteAudit(args.PerformingUser, 0, args.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: ""); - } + private void OnLoginFailed(object sender, IdentityAuditEventArgs args) => WriteAudit(args.PerformingUser, "0", args.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: ""); - private void OnForgotPasswordChange(object sender, IdentityAuditEventArgs args) - { - WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change"); - } + private void OnForgotPasswordChange(object sender, IdentityAuditEventArgs args) => WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change"); - private void OnForgotPasswordRequest(object sender, IdentityAuditEventArgs args) - { - WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/request", "password forgot/request"); - } + private void OnForgotPasswordRequest(object sender, IdentityAuditEventArgs args) => WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/request", "password forgot/request"); - private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) + private void WriteAudit(string performingId, string affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) { - var performingUser = _userService.GetUserById(performingId); + IUser performingUser = null; + if (int.TryParse(performingId, out int asInt)) + { + performingUser = _userService.GetUserById(asInt); + } var performingDetails = performingUser == null ? $"User UNKNOWN:{performingId}" : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; - WriteAudit(performingId, performingDetails, affectedId, ipAddress, eventType, eventDetails, affectedDetails); + if (!int.TryParse(performingId, out int performingIdAsInt)) + { + performingIdAsInt = 0; + } + + if (!int.TryParse(affectedId, out int affectedIdAsInt)) + { + affectedIdAsInt = 0; + } + + WriteAudit(performingIdAsInt, performingDetails, affectedIdAsInt, ipAddress, eventType, eventDetails, affectedDetails); } - private void WriteAudit(IUser performingUser, int affectedId, string ipAddress, string eventType, string eventDetails) + private void WriteAudit(IUser performingUser, string affectedId, string ipAddress, string eventType, string eventDetails) { var performingDetails = performingUser == null ? $"User UNKNOWN" : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; - WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedId, ipAddress, eventType, eventDetails); + if (!int.TryParse(affectedId, out int affectedIdInt)) + { + affectedIdInt = 0; + } + + WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedIdInt, ipAddress, eventType, eventDetails); } private void WriteAudit(int performingId, string performingDetails, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) { if (affectedDetails == null) { - var affectedUser = _userService.GetUserById(affectedId); + IUser affectedUser = _userService.GetUserById(affectedId); affectedDetails = affectedUser == null ? $"User UNKNOWN:{affectedId}" : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}"; } - _auditService.Write(performingId, performingDetails, + _auditService.Write( + performingId, + performingDetails, ipAddress, DateTime.UtcNow, - affectedId, affectedDetails, - eventType, eventDetails); + affectedId, + affectedDetails, + eventType, + eventDetails); } protected virtual void Dispose(bool disposing) @@ -137,12 +143,9 @@ namespace Umbraco.Web.Common.Security { if (disposing) { - //BackOfficeUserManager.AccountLocked -= ; - //BackOfficeUserManager.AccountUnlocked -= ; BackOfficeUserManager.ForgotPasswordRequested -= OnForgotPasswordRequest; BackOfficeUserManager.ForgotPasswordChangedSuccess -= OnForgotPasswordChange; BackOfficeUserManager.LoginFailed -= OnLoginFailed; - //BackOfficeUserManager.LoginRequiresVerification -= ; BackOfficeUserManager.LoginSuccess -= OnLoginSuccess; BackOfficeUserManager.LogoutSuccess -= OnLogoutSuccess; BackOfficeUserManager.PasswordChanged -= OnPasswordChanged; diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index dd92801d59..180f433fab 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.BackOffice.Security //ok, we should be able to reset it var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser); - var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id, resetToken, passwordModel.NewPassword); + var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id.ToString(), resetToken, passwordModel.NewPassword); if (resetResult.Succeeded == false) { diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 230faeff28..081ca6b581 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -117,7 +117,7 @@ namespace Umbraco.Web.Common.Security return result; } - public override async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + public override async Task ChangePasswordWithResetAsync(string userId, string token, string newPassword) { IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); if (result.Succeeded) @@ -176,21 +176,21 @@ namespace Umbraco.Web.Common.Security return result; } - private int GetCurrentUserId(IPrincipal currentUser) + private string GetCurrentUserId(IPrincipal currentUser) { UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); - var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserId; + var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserIdAsString; return currentUserId; } - private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername) + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, string affectedUserId, string affectedUsername) { var currentUserId = GetCurrentUserId(currentUser); var ip = IpResolver.GetCurrentRequestIpAddress(); return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } - private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, int affectedUserId, string affectedUsername) + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, BackOfficeIdentityUser currentUser, string affectedUserId, string affectedUsername) { var currentUserId = currentUser.Id; var ip = IpResolver.GetCurrentRequestIpAddress(); @@ -199,21 +199,21 @@ namespace Umbraco.Web.Common.Security // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring - public void RaiseAccountLockedEvent(IPrincipal currentUser, int userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); + public void RaiseAccountLockedEvent(IPrincipal currentUser, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); - public void RaiseAccountUnlockedEvent(IPrincipal currentUser, int userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); + public void RaiseAccountUnlockedEvent(IPrincipal currentUser, string userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); - public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); - public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); + public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); - public void RaiseLoginFailedEvent(IPrincipal currentUser, int userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); + public void RaiseLoginFailedEvent(IPrincipal currentUser, string userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); - public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, int userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); + public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, string userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); - public void RaiseLoginSuccessEvent(IPrincipal currentUser, int userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); + public void RaiseLoginSuccessEvent(IPrincipal currentUser, string userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); - public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId) + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) { var currentUserId = GetCurrentUserId(currentUser); var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); @@ -221,9 +221,9 @@ namespace Umbraco.Web.Common.Security return args; } - public void RaisePasswordChangedEvent(IPrincipal currentUser, int userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); + public void RaisePasswordChangedEvent(IPrincipal currentUser, string userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); - public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, int userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); + public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, string userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) { From 216b8559dadc729e0935fbaa280692c2923b80a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 12:52:25 +1100 Subject: [PATCH 11/14] cleanup --- .../Security/UmbracoUserManager.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 2ea0bc52b2..6318218669 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -44,19 +44,30 @@ namespace Umbraco.Core.Security PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); } - // We don't support an IUserClaimStore and don't need to (at least currently) - public override bool SupportsUserClaim => false; + /// + public override bool SupportsUserClaim => false; // We don't support an IUserClaimStore and don't need to (at least currently) - // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository - public override bool SupportsQueryableUsers => false; + /// + public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository /// /// Developers will need to override this to support custom 2 factor auth /// + /// public override bool SupportsUserTwoFactor => false; - // We haven't needed to support this yet, though might be necessary for 2FA - public override bool SupportsUserPhoneNumber => false; + /// + public override bool SupportsUserPhoneNumber => false; // We haven't needed to support this yet, though might be necessary for 2FA + + /// + /// Gets the password configuration + /// + public IPasswordConfiguration PasswordConfiguration { get; } + + /// + /// Gets the IP resolver + /// + public IIpResolver IpResolver { get; } /// /// Used to validate a user's session @@ -85,16 +96,6 @@ namespace Umbraco.Core.Security /// An protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) => new PasswordHasher(); - /// - /// Gets the password configuration - /// - public IPasswordConfiguration PasswordConfiguration { get; } - - /// - /// Gets the IP resolver - /// - public IIpResolver IpResolver { get; } - /// /// Helper method to generate a password for a user based on the current password validator /// From 406528f5538c753a9395bc4569578d87436531fb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 12:54:31 +1100 Subject: [PATCH 12/14] flows claims --- .../Security/BackOfficeClaimsPrincipalFactory.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 8a6680d2bf..77f707d812 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -37,13 +37,11 @@ namespace Umbraco.Core.Security ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); - // TODO: How to flow claims then? This is most likely built into aspnetcore now and this is not the way - // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - //foreach (Models.Identity.IdentityUserClaim claim in user.Claims) - //{ - // baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); - //} + foreach (IdentityUserClaim claim in user.Claims) + { + baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); + } // TODO: We want to remove UmbracoBackOfficeIdentity and only rely on ClaimsIdentity, once // that is done then we'll create a ClaimsIdentity with all of the requirements here instead From 10fc3514708b773552e0fadb02f7fdcb935e637d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 13:09:08 +1100 Subject: [PATCH 13/14] merge cleanup --- .../AuthenticationBuilderExtensions.cs | 3 ++- src/Umbraco.Web.UI.NetCore/Startup.cs | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) rename src/Umbraco.Web.BackOffice/{Security => Extensions}/AuthenticationBuilderExtensions.cs (86%) diff --git a/src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/AuthenticationBuilderExtensions.cs similarity index 86% rename from src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs rename to src/Umbraco.Web.BackOffice/Extensions/AuthenticationBuilderExtensions.cs index 9949018d43..8145cb4278 100644 --- a/src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/AuthenticationBuilderExtensions.cs @@ -1,7 +1,8 @@ using System; using Umbraco.Core.DependencyInjection; +using Umbraco.Web.BackOffice.Security; -namespace Umbraco.Web.BackOffice.Security +namespace Umbraco.Extensions { public static class AuthenticationBuilderExtensions { diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d86e6a8776..d496aadfd3 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.UI.NetCore private readonly IConfiguration _config; /// - /// Constructor + /// Initializes a new instance of the class. /// /// The Web Host Environment /// The Configuration @@ -30,15 +30,22 @@ namespace Umbraco.Web.UI.NetCore // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + /// + /// Configures the services + /// public void ConfigureServices(IServiceCollection services) { +#pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) .AddAllBackOfficeComponents() .AddUmbracoWebsite() .Build(); +#pragma warning restore IDE0022 // Use expression body for methods } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// Configures the application + /// public void Configure(IApplicationBuilder app) { if (_env.IsDevelopment()) From 9441f5c5eae2c06db47238d912a3c1507fd529ad Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 4 Dec 2020 15:05:47 +1100 Subject: [PATCH 14/14] fix remaining test --- .../Security/BackOfficeUserStore.cs | 8 ++++---- .../Security/BackOfficeAuthenticationBuilder.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index befa5ebac2..1756e84d76 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -201,26 +201,26 @@ namespace Umbraco.Core.Security IUser user = _userService.GetUserById(UserIdToInt(userId)); if (user == null) { - return null; + return Task.FromResult((BackOfficeIdentityUser)null); } return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); } /// - public override async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); IUser user = _userService.GetByUsername(userName); if (user == null) { - return null; + return Task.FromResult((BackOfficeIdentityUser)null); } BackOfficeIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); - return await Task.FromResult(result); + return Task.FromResult(result); } /// diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs index b3418697e2..7012d5f1dd 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options;