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/BackOffice/BackOfficeIdentityUser.cs b/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs deleted file mode 100644 index 027e7c0904..0000000000 --- a/src/Umbraco.Core/BackOffice/BackOfficeIdentityUser.cs +++ /dev/null @@ -1,449 +0,0 @@ -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; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.Core.BackOffice -{ - public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty - { - private string _email; - private string _userName; - private int _id; - private bool _hasIdentity; - 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; - - /// - /// 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)); - - 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 - user._id = int.MinValue; - user._hasIdentity = false; - user._culture = culture; - user._name = name; - user.EnableChangeTracking(); - return user; - } - - private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyUserGroup[] groups) - { - _startMediaIds = Array.Empty(); - _startContentIds = Array.Empty(); - _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; - } - - /// - /// Creates an existing user with the specified groups - /// - /// - /// - /// - public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups) - : this(globalSettings, groups.ToArray()) - { - // use the property setters - they do more than just setting a field - 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 => _hasIdentity; - - 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 - /// - public string Name - { - get => _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)); - } - - - /// - /// Content start nodes assigned to the User (not ones assigned to the user's groups) - /// - public int[] StartContentIds - { - get => _startContentIds; - set - { - if (value == null) value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer); - } - } - - /// - /// Media start nodes assigned to the User (not ones assigned to the user's groups) - /// - public int[] StartMediaIds - { - get => _startMediaIds; - set - { - if (value == null) value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer); - } - } - - /// - /// This is 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 Culture - { - get => _culture; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); - } - - public IReadOnlyUserGroup[] Groups - { - get => _groups; - set - { - //so they recalculate - _allowedSections = null; - - _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 - { - RoleId = x.Alias, - UserId = Id.ToString() - })) - { - _roles.Add(identityUserRole); - } - _roles.CollectionChanged += _roles_CollectionChanged; - - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), 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 - /// - public bool IsLockedOut - { - get - { - var isLocked = LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// This is a 1:1 mapping with IUser.IsApproved - /// - 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.ToString(), - 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.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/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs index cbe5b47b38..05703a1b2c 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; } + string UserId { get; set; } // TODO: This should be able to be used by both users and members /// - /// 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 deleted file mode 100644 index 093e42c1e7..0000000000 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Models.Identity -{ - /// - /// Default IUser implementation - /// - /// - /// - /// 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 IdentityUser - where TLogin : IIdentityUserLogin - //NOTE: Making our role id a string - where TRole : IdentityUserRole - where TClaim : IdentityUserClaim - { - /// - /// Initializes a new instance of the class. - /// - public IdentityUser() - { - Claims = new List(); - Roles = new List(); - Logins = new List(); - } - - /// - /// Last login date - /// - public virtual DateTime? LastLoginDateUtc { get; set; } - - /// - /// Email - /// - public virtual string Email { get; set; } - - /// - /// True if the email is confirmed, default is false - /// - public virtual bool EmailConfirmed { get; set; } - - /// - /// 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) - /// - public virtual string SecurityStamp { get; set; } - - /// - /// PhoneNumber for the user - /// - public virtual string PhoneNumber { get; set; } - - /// - /// True if the phone number is confirmed, default is false - /// - public virtual bool PhoneNumberConfirmed { get; set; } - - /// - /// Is two factor enabled for the user - /// - public virtual bool TwoFactorEnabled { get; set; } - - /// - /// 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. - /// - public virtual DateTime? LastPasswordChangeDateUtc { get; set; } - - /// - /// Is lockout enabled for this user - /// - public virtual bool LockoutEnabled { get; set; } - - /// - /// Used to record failures for the purposes of lockout - /// - public virtual int AccessFailedCount { get; set; } - - /// - /// Navigation property for user roles - /// - public virtual ICollection Roles { get; } - - /// - /// Navigation property for user claims - /// - public virtual ICollection Claims { get; } - - /// - /// Navigation property for user logins - /// - public virtual ICollection Logins { get; } - - /// - /// User ID (Primary Key) - /// - public virtual TKey Id { get; set; } - - /// - /// 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 deleted file mode 100644 index e117d2fd13..0000000000 --- a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 - { - /// - /// Primary key - /// - /// - public virtual int Id { get; set; } - - /// - /// User Id for the user who owns this login - /// - /// - public virtual TKey UserId { get; set; } - - /// - /// Claim type - /// - /// - public virtual string ClaimType { get; set; } - - /// - /// 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..5974822c20 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 @@ -9,14 +9,20 @@ namespace Umbraco.Core.Models.Identity /// public class IdentityUserLogin : EntityBase, IIdentityUserLogin { - public IdentityUserLogin(string loginProvider, string providerKey, int userId) + /// + /// Initializes a new instance of the class. + /// + 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) + /// + /// Initializes a new instance of the class. + /// + public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) { Id = id; LoginProvider = loginProvider; @@ -32,7 +38,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 deleted file mode 100644 index ba9e87e46c..0000000000 --- a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 - { - /// - /// UserId for the user that is in the role - /// - /// - public virtual TKey UserId { get; set; } - - /// - /// RoleId for the role - /// - /// - public virtual TKey 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/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/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs similarity index 83% rename from src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs rename to src/Umbraco.Core/Security/IdentityAuditEventArgs.cs index 1d51c45074..b9884c8e7d 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 { /// @@ -27,12 +27,12 @@ namespace Umbraco.Core.BackOffice /// /// 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,19 +53,19 @@ namespace Umbraco.Core.BackOffice /// /// /// - 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; IpAddress = ipAddress; - Comment = comment; + Comment = comment; PerformingUser = performingUser; AffectedUsername = affectedUsername; 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/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs similarity index 83% rename from src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs rename to src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 9a60c5d64f..5fd9f23c92 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; @@ -54,16 +53,21 @@ namespace Umbraco.Core.BackOffice /// /// /// - 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 { - 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); } @@ -83,15 +87,19 @@ namespace Umbraco.Core.BackOffice /// /// 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) { - 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); } @@ -118,13 +126,13 @@ namespace Umbraco.Core.BackOffice /// /// 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)); @@ -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/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs deleted file mode 100644 index b271f5aa41..0000000000 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ /dev/null @@ -1,919 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -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; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Scoping; -using Umbraco.Core.Services; - -namespace Umbraco.Core.BackOffice -{ - 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 - { - private readonly IScopeProvider _scopeProvider; - private readonly IUserService _userService; - private readonly IEntityService _entityService; - private readonly IExternalLoginService _externalLoginService; - private readonly GlobalSettings _globalSettings; - private readonly UmbracoMapper _mapper; - private bool _disposed = false; - - public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper) - { - _scopeProvider = scopeProvider; - _userService = userService; - _entityService = entityService; - _externalLoginService = externalLoginService; - _globalSettings = globalSettings.Value; - if (userService == null) throw new ArgumentNullException("userService"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - _mapper = mapper; - _userService = userService; - _externalLoginService = externalLoginService; - } - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { - _disposed = true; - } - - public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.Id.ToString()); - } - - public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.UserName); - } - - public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.UserName = userName; - return Task.CompletedTask; - } - - public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetUserNameAsync(user, cancellationToken); - } - - public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - return SetUserNameAsync(user, normalizedName, cancellationToken); - } - - /// - /// Insert a new user - /// - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - //the password must be 'something' it could be empty if authenticating - // with an external provider so we'll just generate one and prefix it, the - // prefix will help us determine if the password hasn't actually been specified yet. - //this will hash the guid with a salt so should be nicely random - var aspHasher = new PasswordHasher(); - var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + - aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); - - var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) - { - Language = user.Culture ?? _globalSettings.DefaultUILanguage, - StartContentIds = user.StartContentIds ?? new int[] { }, - StartMediaIds = user.StartMediaIds ?? new int[] { }, - IsLockedOut = user.IsLockedOut, - }; - - // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); - - UpdateMemberProperties(userEntity, user); - - _userService.Save(userEntity); - - if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); - - //re-assign id - user.Id = userEntity.Id; - - if (isLoginsPropertyDirty) - { - _externalLoginService.Save( - user.Id, - user.Logins.Select(x => new ExternalLogin( - x.LoginProvider, - x.ProviderKey, - x.UserData))); - } - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Update a user - /// - /// - /// - /// - public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - using (var scope = _scopeProvider.CreateScope()) - { - var found = _userService.GetUserById(asInt.Result); - if (found != null) - { - // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); - - if (UpdateMemberProperties(found, user)) - { - _userService.Save(found); - } - - if (isLoginsPropertyDirty) - { - _externalLoginService.Save( - found.Id, - user.Logins.Select(x => new ExternalLogin( - x.LoginProvider, - x.ProviderKey, - x.UserData))); - } - } - - scope.Complete(); - } - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Delete a user - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var found = _userService.GetUserById(user.Id); - if (found != null) - { - _userService.Delete(found); - } - _externalLoginService.DeleteUserLogins(user.Id); - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Finds a user - /// - /// - /// - /// - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - var user = _userService.GetUserById(UserIdToInt(userId)); - if (user == null) return null; - - return await Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); - } - - /// - /// Find a user by name - /// - /// - /// - /// - public async Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var user = _userService.GetByUsername(userName); - if (user == null) - { - return null; - } - - var result = AssignLoginsCallback(_mapper.Map(user)); - - return await Task.FromResult(result); - } - - /// - /// Set the user password hash - /// - /// - /// - /// - public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); - if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); - - user.PasswordHash = passwordHash; - user.PasswordConfig = null; // Clear this so that it's reset at the repository level - - return Task.CompletedTask; - } - - /// - /// Get the user password hash - /// - /// - /// - /// - public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.PasswordHash); - } - - /// - /// Returns true if a user has a password set - /// - /// - /// - /// - public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); - } - - /// - /// Set the user email - /// - /// - /// - /// - public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(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 - ? null - : _mapper.Map(user); - - return Task.FromResult(AssignLoginsCallback(result)); - } - - public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetEmailAsync(user, cancellationToken); - } - - public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - return SetEmailAsync(user, normalizedEmail, cancellationToken); - } - - /// - /// Adds a user login with the specified provider and key - /// - /// - /// - /// - /// - public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException(nameof(login)); - - var logins = user.Logins; - var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); - var userLogin = instance; - logins.Add(userLogin); - - return Task.CompletedTask; - } - - /// - /// Removes the user login with the specified combination if it exists - /// - /// - /// - /// - /// - /// - public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (userLogin != null) user.Logins.Remove(userLogin); - - return Task.CompletedTask; - } - - /// - /// Returns the linked accounts for this user - /// - /// - /// - /// - public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList) - user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); - } - - /// - /// Returns the user associated with this login - /// - /// - public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - //get all logins associated with the login id - var result = _externalLoginService.Find(loginProvider, providerKey).ToArray(); - if (result.Any()) - { - //return the first user that matches the result - BackOfficeIdentityUser output = null; - foreach (var l in result) - { - var user = _userService.GetUserById(l.UserId); - if (user != null) - { - output = _mapper.Map(user); - break; - } - } - - return Task.FromResult(AssignLoginsCallback(output)); - } - - return Task.FromResult(null); - } - - - /// - /// Adds a user to a role (user group) - /// - /// - /// - /// - /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole == null) - { - user.AddRole(normalizedRoleName); - } - - return Task.CompletedTask; - } - - /// - /// Removes the role (user group) for the user - /// - /// - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (user == null) throw new ArgumentNullException(nameof(user)); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole != null) - { - user.Roles.Remove(userRole); - } - - return Task.CompletedTask; - } - - /// - /// Returns the roles (user groups) for this user - /// - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); - } - - /// - /// Returns true if a user is in the role - /// - /// - /// - /// - /// - public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); - } - - /// - /// Lists all users of a given role. - /// - /// - /// Identity Role names are equal to Umbraco UserGroup alias. - /// - public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - - var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); - - var 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)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.SecurityStamp = stamp; - return Task.CompletedTask; - } - - /// - /// Get the user security stamp - /// - /// - /// - /// - public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password - return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() - ? user.PasswordHash.GenerateHash() - : user.SecurityStamp); - } - - private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) - { - if (user != null) - { - user.SetLoginsCallback(new Lazy>(() => - _externalLoginService.GetAll(user.Id))); - } - return user; - } - - #region IUserLockoutStore - - /// - /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out. - /// - /// - /// - /// - /// - /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status - /// - public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return user.LockoutEndDateUtc.HasValue - ? Task.FromResult(DateTimeOffset.MaxValue) - : Task.FromResult(DateTimeOffset.MinValue); - } - - /// - /// Locks a user out until the specified end date (set to a past date, to unlock a user) - /// - /// - /// - /// - /// - /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status - /// - public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime; - return Task.CompletedTask; - } - - /// - /// Used to record when an attempt to access the user has failed - /// - /// - /// - /// - public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Used to reset the access failed count, typically after the account is successfully accessed - /// - /// - /// - /// - public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.AccessFailedCount = 0; - return Task.CompletedTask; - } - - /// - /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is - /// verified or the account is locked out. - /// - /// - /// - /// - public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Returns true - /// - /// - /// - /// - public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.LockoutEnabled); - } - - /// - /// Doesn't actually perform any function, users can always be locked out - /// - /// - /// - /// - public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEnabled = enabled; - return Task.CompletedTask; - } - #endregion - - private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser) - { - var anythingChanged = false; - - //don't assign anything if nothing has changed as this will trigger the track changes of the model - - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc)) - || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) - || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) - { - anythingChanged = true; - //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime - var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); - user.LastLoginDate = dt; - } - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) - || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) - || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) - { - anythingChanged = true; - user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); - } - if (identityUser.IsPropertyDirty(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)) - { - 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; - user.IsLockedOut = identityUser.IsLockedOut; - - if (user.IsLockedOut) - { - //need to set the last lockout date - user.LastLockoutDate = DateTime.Now; - } - - } - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) - && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Username = identityUser.UserName; - } - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash)) - && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.RawPasswordValue = identityUser.PasswordHash; - user.PasswordConfiguration = identityUser.PasswordConfig; - } - - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture)) - && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Language = identityUser.Culture; - } - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds)) - && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) - { - anythingChanged = true; - user.StartMediaIds = identityUser.StartMediaIds; - } - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds)) - && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) - { - anythingChanged = true; - user.StartContentIds = identityUser.StartContentIds; - } - if (user.SecurityStamp != identityUser.SecurityStamp) - { - anythingChanged = true; - user.SecurityStamp = identityUser.SecurityStamp; - } - - // TODO: Fix this for Groups too - if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) - { - var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); - - var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray(); - var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray(); - - var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray(); - - if (userGroupAliases.ContainsAll(combinedAliases) == false - || combinedAliases.ContainsAll(userGroupAliases) == false) - { - anythingChanged = true; - - //clear out the current groups (need to ToArray since we are modifying the iterator) - user.ClearGroups(); - - //go lookup all these groups - var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); - - //use all of the ones assigned and add them - foreach (var group in groups) - { - user.AddGroup(group); - } - - //re-assign - identityUser.Groups = groups; - } - } - - //we should re-set the calculated start nodes - identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); - identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); - - //reset all changes - identityUser.ResetDirtyProperties(false); - - return anythingChanged; - } - - private void ThrowIfDisposed() - { - if (_disposed) throw new ObjectDisposedException(GetType().Name); - } - - public Task ValidateSessionIdAsync(string userId, string sessionId) - { - Guid guidSessionId; - if (Guid.TryParse(sessionId, out guidSessionId)) - { - return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); - } - - return Task.FromResult(false); - } - - private static int UserIdToInt(string userId) - { - var attempt = userId.TryConvertTo(); - if (attempt.Success) return attempt.Result; - - throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); - } - } -} diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/CoreMappingProfiles.cs index 09a8523cb9..c700938534 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 Microsoft.Extensions.DependencyInjection; using Umbraco.Core.DependencyInjection; using Umbraco.Core.Mapping; +using Umbraco.Core.Security; using Umbraco.Web.Models.Mapping; namespace Umbraco.Core.Composing.CompositionExtensions @@ -19,7 +19,6 @@ namespace Umbraco.Core.Composing.CompositionExtensions builder.Services.AddUnique(); builder.WithCollectionBuilder() - .Add() .Add() .Add() .Add() 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/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/BackOffice/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs similarity index 53% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 568c028e67..77f707d812 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -1,33 +1,50 @@ -using System; +using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using Umbraco.Core.Security; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Security { - 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 (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 +60,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 +69,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.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 5bae03cad6..c9f8d35ada 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -1,18 +1,25 @@ -using System; +using System; using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.BackOffice; -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) { } @@ -28,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/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs new file mode 100644 index 0000000000..e2e8031768 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -0,0 +1,201 @@ +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; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Core.Security +{ + /// + /// The identity user used for the back office + /// + public class BackOfficeIdentityUser : UmbracoIdentityUser + { + private string _name; + private string _passwordConfig; + private string _culture; + private IReadOnlyCollection _groups; + private string[] _allowedSections; + private int[] _startMediaIds; + private int[] _startContentIds; + + // 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 + /// + /// 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)); + } + + var user = new BackOfficeIdentityUser(globalSettings, Array.Empty()); + user.DisableChangeTracking(); + user.UserName = username; + user.Email = email; + + user.Id = null; + user.HasIdentity = false; + user._culture = culture; + user._name = name; + user.EnableChangeTracking(); + return user; + } + + private BackOfficeIdentityUser(GlobalSettings globalSettings, IReadOnlyCollection groups) + { + _startMediaIds = Array.Empty(); + _startContentIds = Array.Empty(); + _allowedSections = Array.Empty(); + _culture = globalSettings.DefaultUILanguage; + + // use the property setters - they do more than just setting a field + Groups = groups; + } + + /// + /// Initializes a new instance of the class. + /// + public BackOfficeIdentityUser(GlobalSettings globalSettings, int userId, IEnumerable groups) + : this(globalSettings, groups.ToArray()) + { + // use the property setters - they do more than just setting a field + Id = UserIdToString(userId); + } + + public int[] CalculatedMediaStartNodeIds { get; set; } + public int[] CalculatedContentStartNodeIds { get; set; } + + /// + /// Gets or sets the user's real name + /// + public string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + + public string PasswordConfig + { + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } + + + /// + /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups) + /// + public int[] StartContentIds + { + get => _startContentIds; + set + { + if (value == null) + { + value = new int[0]; + } + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), s_startIdsComparer); + } + } + + /// + /// Gets or sets media start nodes assigned to the User (not ones assigned to the user's groups) + /// + public int[] StartMediaIds + { + get => _startMediaIds; + set + { + if (value == null) + { + value = new int[0]; + } + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), s_startIdsComparer); + } + } + + /// + /// Gets a readonly list of the user's allowed sections which are based on it's user groups + /// + 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)); + } + + /// + /// Gets or sets the user groups + /// + public IReadOnlyCollection Groups + { + get => _groups; + set + { + // so they recalculate + _allowedSections = null; + + _groups = value.Where(x => x.Alias != null).ToArray(); + + var roles = new List>(); + foreach (IdentityUserRole identityUserRole in _groups.Select(x => new IdentityUserRole + { + RoleId = x.Alias, + UserId = Id?.ToString() + })) + { + roles.Add(identityUserRole); + } + + // now reset the collection + Roles = roles; + + BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer); + } + } + + /// + /// Gets a value indicating whether the user is locked out based on the user's lockout end date + /// + public bool IsLockedOut + { + get + { + var isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// Gets or sets a value indicating the IUser IsApproved + /// + public bool IsApproved { get; set; } + + private static string UserIdToString(int userId) => string.Intern(userId.ToString()); + } +} 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/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs new file mode 100644 index 0000000000..1756e84d76 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -0,0 +1,777 @@ +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; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + // TODO: Make this into a base class that can be re-used + + /// + /// 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; + private readonly IEntityService _entityService; + private readonly IExternalLoginService _externalLoginService; + private readonly GlobalSettings _globalSettings; + private readonly UmbracoMapper _mapper; + + /// + /// Initializes a new instance of the class. + /// + 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)); + _entityService = entityService; + _externalLoginService = externalLoginService ?? throw new ArgumentNullException(nameof(externalLoginService)); + _globalSettings = globalSettings.Value; + _mapper = mapper; + _userService = userService; + _externalLoginService = externalLoginService; + } + + /// + /// Not supported in Umbraco + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override IQueryable Users => throw new NotImplementedException(); + + /// + 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)); + } + + // the password must be 'something' it could be empty if authenticating + // with an external provider so we'll just generate one and prefix it, the + // prefix will help us determine if the password hasn't actually been specified yet. + // this will hash the guid with a salt so should be nicely random + var aspHasher = new PasswordHasher(); + var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + + aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); + + var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) + { + Language = user.Culture ?? _globalSettings.DefaultUILanguage, + StartContentIds = user.StartContentIds ?? new int[] { }, + StartMediaIds = user.StartMediaIds ?? new int[] { }, + IsLockedOut = user.IsLockedOut, + }; + + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); + + UpdateMemberProperties(userEntity, user); + + _userService.Save(userEntity); + + if (!userEntity.HasIdentity) + { + throw new DataException("Could not create the user, check logs for details"); + } + + // re-assign id + user.Id = UserIdToString(userEntity.Id); + + if (isLoginsPropertyDirty) + { + _externalLoginService.Save( + userEntity.Id, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + x.UserData))); + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + Attempt asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + using (IScope scope = _scopeProvider.CreateScope()) + { + IUser found = _userService.GetUserById(asInt.Result); + if (found != null) + { + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins)); + + if (UpdateMemberProperties(found, user)) + { + _userService.Save(found); + } + + if (isLoginsPropertyDirty) + { + _externalLoginService.Save( + found.Id, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + x.UserData))); + } + } + + scope.Complete(); + } + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IUser found = _userService.GetUserById(UserIdToInt(user.Id)); + if (found != null) + { + _userService.Delete(found); + } + + _externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); + + return Task.FromResult(IdentityResult.Success); + } + + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); + + /// + protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + IUser user = _userService.GetUserById(UserIdToInt(userId)); + if (user == null) + { + return Task.FromResult((BackOfficeIdentityUser)null); + } + + return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); + } + + /// + public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + IUser user = _userService.GetByUsername(userName); + if (user == null) + { + return Task.FromResult((BackOfficeIdentityUser)null); + } + + BackOfficeIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); + + return Task.FromResult(result); + } + + /// + public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default) + { + await base.SetPasswordHashAsync(user, passwordHash, cancellationToken); + + user.PasswordConfig = null; // Clear this so that it's reset at the repository level + user.LastPasswordChangeDateUtc = DateTime.UtcNow; + } + + /// + 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(); + IUser user = _userService.GetByEmail(email); + BackOfficeIdentityUser result = user == null + ? null + : _mapper.Map(user); + + return Task.FromResult(AssignLoginsCallback(result)); + } + + /// + public override Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + => GetEmailAsync(user, cancellationToken); + + /// + public override Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + => SetEmailAsync(user, normalizedEmail, 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)); + } + + ICollection logins = user.Logins; + var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id.ToString()); + IdentityUserLogin userLogin = instance; + logins.Add(userLogin); + + return Task.CompletedTask; + } + + /// + public override Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + if (userLogin != null) + { + user.Logins.Remove(userLogin); + } + + return Task.CompletedTask; + } + + /// + public override Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); + } + + /// + 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 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)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole == null) + { + user.AddRole(normalizedRoleName); + } + + return Task.CompletedTask; + } + + /// + /// Removes the role (user group) for the user + /// + public 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)); + } + + IdentityUserRole userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole != null) + { + user.Roles.Remove(userRole); + } + + return Task.CompletedTask; + } + + /// + /// Returns the roles (user groups) for this user + /// + public override Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); + } + + /// + /// Returns true if a user is in the role + /// + public override Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); + } + + /// + /// Lists all users of a given role. + /// + /// + /// Identity Role names are equal to Umbraco UserGroup alias. + /// + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (normalizedRoleName == null) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + IUserGroup userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); + + IEnumerable users = _userService.GetAllInGroup(userGroup.Id); + IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList(); + + return Task.FromResult(backOfficeIdentityUsers); + } + + /// + protected override Task> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + IUserGroup group = _userService.GetUserGroupByAlias(normalizedRoleName); + if (group == null) + { + return Task.FromResult((IdentityRole)null); + } + + return Task.FromResult(new IdentityRole(group.Name) + { + Id = group.Alias + }); + } + + /// + protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken) + { + BackOfficeIdentityUser user = await FindUserAsync(userId, cancellationToken); + if (user == null) + { + return null; + } + + IdentityUserRole found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId)); + return found; + } + + /// + public override Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + // the stamp cannot be null, so if it is currently null then we'll just return a hash of the password + return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() + ? user.PasswordHash.GenerateHash() + : user.SecurityStamp); + } + + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) + { + if (user != null) + { + user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); + } + + return user; + } + + private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser) + { + var anythingChanged = false; + + // don't assign anything if nothing has changed as this will trigger the track changes of the model + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc)) + || (user.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false) + || (identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)) + { + anythingChanged = true; + + // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); + user.LastLoginDate = dt; + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) + || (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 && 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; + user.IsLockedOut = identityUser.IsLockedOut; + + if (user.IsLockedOut) + { + // need to set the last lockout date + user.LastLockoutDate = DateTime.Now; + } + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) + && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Username = identityUser.UserName; + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash)) + && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.RawPasswordValue = identityUser.PasswordHash; + user.PasswordConfiguration = identityUser.PasswordConfig; + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Culture)) + && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Language = identityUser.Culture; + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartMediaIds)) + && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) + { + anythingChanged = true; + user.StartMediaIds = identityUser.StartMediaIds; + } + + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.StartContentIds)) + && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) + { + anythingChanged = true; + user.StartContentIds = identityUser.StartContentIds; + } + + if (user.SecurityStamp != identityUser.SecurityStamp) + { + anythingChanged = true; + user.SecurityStamp = identityUser.SecurityStamp; + } + + // TODO: Fix this for Groups too + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups))) + { + var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray(); + + var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray(); + var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray(); + + var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray(); + + if (userGroupAliases.ContainsAll(combinedAliases) == false + || combinedAliases.ContainsAll(userGroupAliases) == false) + { + anythingChanged = true; + + // clear out the current groups (need to ToArray since we are modifying the iterator) + user.ClearGroups(); + + // go lookup all these groups + var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray(); + + // use all of the ones assigned and add them + foreach (var group in groups) + { + user.AddGroup(group); + } + + // re-assign + identityUser.Groups = groups; + } + } + + // we should re-set the calculated start nodes + identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); + identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); + + // reset all changes + identityUser.ResetDirtyProperties(false); + + return anythingChanged; + } + + /// + public Task ValidateSessionIdAsync(string userId, string sessionId) + { + if (Guid.TryParse(sessionId, out Guid guidSessionId)) + { + return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); + } + + return Task.FromResult(false); + } + + private static int UserIdToInt(string userId) + { + 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.Infrastructure/BackOffice/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs similarity index 87% rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs index 131bd08ac9..8b2c8932a7 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserValidator.cs @@ -1,7 +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/Security/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs new file mode 100644 index 0000000000..4235195bb1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Security; + +namespace Umbraco.Core.Security +{ + /// + /// The user manager for the back office + /// + public interface IBackOfficeUserManager : IUmbracoUserManager + { + } +} diff --git a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs similarity index 83% rename from src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs rename to src/Umbraco.Infrastructure/Security/IBackOfficeUserPasswordChecker.cs index 5874337f4a..fdf1f1fcf2 100644 --- a/src/Umbraco.Core/BackOffice/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Infrastructure/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 @@ -11,9 +11,6 @@ namespace Umbraco.Core.BackOffice /// /// 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/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs similarity index 85% rename from src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs rename to src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index c026c256f5..4bec4c9c7a 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -5,27 +5,56 @@ 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 +namespace Umbraco.Core.Security { - public interface IBackOfficeUserManager : IBackOfficeUserManager - { - } - public interface IBackOfficeUserManager: IDisposable + /// + /// A user manager for Umbraco (either back office users or front-end members) + /// + /// The type of user + 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); /// @@ -49,15 +78,11 @@ namespace Umbraco.Core.BackOffice /// /// 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 . @@ -97,8 +122,6 @@ namespace Umbraco.Core.BackOffice /// /// 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 /// @@ -145,8 +168,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 @@ -160,7 +182,6 @@ namespace Umbraco.Core.BackOffice /// Task AddPasswordAsync(TUser user, string password); - /// /// Returns a flag indicating whether the given is valid for the /// specified . @@ -183,15 +204,12 @@ 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); /// @@ -206,12 +224,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. /// @@ -290,8 +307,16 @@ 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); /// @@ -314,9 +339,10 @@ 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) - void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); - void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); - SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + // TODO: When we change to not having the crappy static events this will need to be revisited + 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/BackOffice/IUserSessionStore.cs b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs similarity index 55% rename from src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs rename to src/Umbraco.Infrastructure/Security/IUserSessionStore.cs index 69d5408cf7..c68d1f13f9 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs +++ b/src/Umbraco.Infrastructure/Security/IUserSessionStore.cs @@ -1,15 +1,17 @@ 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 /// - /// - 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.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.Core/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs similarity index 95% rename from src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs rename to src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 61fdf82d19..aebb2de5bf 100644 --- a/src/Umbraco.Core/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/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.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 961c2e6137..626932640c 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.Security { /// @@ -6,7 +8,7 @@ /// 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.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs new file mode 100644 index 0000000000..1b888123be --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -0,0 +1,276 @@ +using System; +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 : IdentityUser, IRememberBeingDirty + { + private string _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. + /// + public UmbracoIdentityUser() + { + // 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 DateTime? LastLoginDateUtc + { + get => _lastLoginDateUtc; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc)); + } + + /// + /// Gets or sets email + /// + public override 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 override bool EmailConfirmed + { + get => _emailConfirmed; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); + } + + /// + /// Gets or sets the salted/hashed form of the user password + /// + public override string PasswordHash + { + get => _passwordHash; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); + } + + /// + /// Gets or sets dateTime in UTC when the password was last changed. + /// + 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 + /// + /// + /// Currently this is always true for users and members + /// + public override bool LockoutEnabled + { + get => true; + set { } + } + + /// + /// Gets or sets the value to record failures for the purposes of lockout + /// + public override int AccessFailedCount + { + get => _accessFailedCount; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); + } + + /// + /// Gets or sets the user roles collection + /// + 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 ICollection> Claims { get; } + + /// + /// Gets the user logins collection + /// + 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 override string 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 override 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.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs new file mode 100644 index 0000000000..6318218669 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -0,0 +1,223 @@ +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.Configuration; +using Umbraco.Core.Models.Identity; +using Umbraco.Net; + +namespace Umbraco.Core.Security +{ + + /// + /// Abstract class for Umbraco User Managers for back office users or front-end members + /// + /// 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. + /// + public UmbracoUserManager( + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber 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)); + } + + /// + public override bool SupportsUserClaim => false; // We don't support an IUserClaimStore and don't need to (at least currently) + + /// + 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; + + /// + 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 + /// + /// 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 + // TODO: This should be removed after members supports this + 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(); + + /// + /// 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(TUser 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(string userId, string token, string newPassword) + { + TUser user = await FindByIdAsync(userId); + if (user == null) + { + throw new InvalidOperationException("Could not find user"); + } + + IdentityResult result = await ResetPasswordAsync(user, token, newPassword); + return result; + } + + /// + public override async Task SetLockoutEndDateAsync(TUser 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(TUser 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(TUser 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; + } + + } +} diff --git a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 4e980b7bb1..80b05497a8 100644 --- a/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -1,12 +1,13 @@ -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 { - 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; @@ -24,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.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/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.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))); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 450b3a341a..d9dee389ee 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -1,10 +1,11 @@ -using System; +using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Umbraco.Core.Security; using Umbraco.Extensions; -using Umbraco.Core.BackOffice; using Umbraco.Tests.Integration.Testing; +using Umbraco.Web.Common.Security; namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice { @@ -26,7 +27,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/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 13c73dfa96..64bdca6437 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; @@ -7,51 +7,42 @@ using Microsoft.Extensions.Options; 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.Core.Security; 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(); @@ -107,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}); @@ -124,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)}); @@ -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.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.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 9e9d29a123..79a9456643 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -1,9 +1,9 @@ -using System; +using System; 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 { @@ -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 a078456f8f..ad0f292fae 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs @@ -1,11 +1,11 @@ -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Generic; 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 { @@ -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.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 b04a5ff158..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,7 @@ -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; 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..7899ef39c2 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( + Constants.Security.SuperUserIdAsString, + "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.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs index d45887b3c3..342083039f 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; @@ -60,7 +60,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); - GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath); + GenerateAuthPaths(out var remainingTimeoutSecondsPath, out var isAuthPath); + var mgr = new BackOfficeCookieManager( Mock.Of(), runtime, @@ -115,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(); @@ -125,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.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..e702753236 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 { @@ -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.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/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index efe28763f1..36e5c2b6fe 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 { @@ -392,7 +390,7 @@ namespace Umbraco.Web.BackOffice.Controllers await _emailSender.SendAsync(mailMessage); - _userManager.RaiseForgotPasswordRequestedEvent(User, user.Id); + _userManager.RaiseForgotPasswordRequestedEvent(User, user.Id.ToString()); } } @@ -556,7 +554,7 @@ namespace Umbraco.Web.BackOffice.Controllers } } - _userManager.RaiseForgotPasswordChangedSuccessEvent(User, model.UserId); + _userManager.RaiseForgotPasswordChangedSuccessEvent(User, model.UserId.ToString()); return Ok(); } @@ -579,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()) { @@ -610,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 1ce0831502..19fb6aa2df 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 { @@ -434,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/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 c31cd68707..9d7999b9f7 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/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.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 413a54a28b..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; @@ -36,7 +31,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/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/Extensions/WebMappingProfiles.cs b/src/Umbraco.Web.BackOffice/Extensions/WebMappingProfiles.cs index 4cd9cf5823..eeb0903e88 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.DependencyInjection; -using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Mapping; 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/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; 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/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/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 deleted file mode 100644 index 464f2a38aa..0000000000 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs +++ /dev/null @@ -1,487 +0,0 @@ -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.Membership; -using Umbraco.Core.Security; -using Umbraco.Extensions; -using Umbraco.Net; -using Umbraco.Web.Models.ContentEditing; - - -namespace Umbraco.Web.Common.Security -{ - - public class BackOfficeUserManager : BackOfficeUserManager, IBackOfficeUserManager - { - 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(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, httpContextAccessor, 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)); - } - - #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; - - // 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; - - #endregion - - /// - /// 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 - /// - /// - /// - /// - 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 - /// - /// - 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(); - } - - /// - /// 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 - /// - /// - 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); - } - - #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 - /// * Call VerifyPasswordAsync with the password store + user + password - /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password - /// - /// In some cases people want simple custom control over the username/password check, for simplicity - /// sake, developers would like the users to simply validate against an LDAP directory but the user - /// data remains stored inside of Umbraco. - /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. - /// - /// 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) - { - if (BackOfficeUserPasswordChecker != null) - { - var 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 (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; - } - } - - //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 - /// - /// - /// - /// - /// - /// - /// 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"); - - var 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); - if (result.Succeeded) - { - RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } - return result; - } - - /// - /// 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); - 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() - { - return Guid.NewGuid().ToString(); - } - - #endregion - - public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var 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) - { - RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - } - else - { - RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); - //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); - //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 - /// - /// - /// - /// 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)); - - 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 - } - - var 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; - } - - private int GetCurrentUserId(IPrincipal currentUser) - { - var 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; - var ip = IpResolver.GetCurrentRequestIpAddress(); - return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); - } - - // 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 RaiseAccountUnlockedEvent(IPrincipal currentUser, int 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 RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int 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 RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, int 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 SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId) - { - var currentUserId = GetCurrentUserId(currentUser); - var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); - OnLogoutSuccess(args); - return args; - } - - public void RaisePasswordChangedEvent(IPrincipal currentUser, int 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 UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) - { - var currentUserId = GetCurrentUserId(currentUser); - var ip = IpResolver.GetCurrentRequestIpAddress(); - var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); - OnSendingUserInvite(args); - return args; - } - - public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; - - // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot - // have non-static events here because the user manager is a Scoped instance not a singleton - // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely - public static event EventHandler AccountLocked; - public static event EventHandler AccountUnlocked; - public static event EventHandler ForgotPasswordRequested; - public static event EventHandler ForgotPasswordChangedSuccess; - public static event EventHandler LoginFailed; - public static event EventHandler LoginRequiresVerification; - public static event EventHandler LoginSuccess; - public static event EventHandler LogoutSuccess; - public static event EventHandler PasswordChanged; - public static event EventHandler PasswordReset; - public static event EventHandler ResetAccessFailedCount; - - /// - /// Raised when a user is invited - /// - public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it - - protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); - - protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); - - protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); - - protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); - - protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); - - protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); - - protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); - - protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); - - protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); - - protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); - - protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); - - protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); - } -} diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs index 019eed7e39..81be953d22 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs @@ -1,11 +1,10 @@ -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; +using Umbraco.Core.Security; using Umbraco.Core.Services; namespace Umbraco.Web.Common.Security @@ -34,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) { @@ -66,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) @@ -138,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/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index bd816e9382..9f90395ff3 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; @@ -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; @@ -38,8 +37,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 +63,7 @@ namespace Umbraco.Web.BackOffice.Security IRequestCache requestCache, IUserService userService, IIpResolver ipResolver, - ISystemClock systemClock, - LinkGenerator linkGenerator) + ISystemClock systemClock) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -65,15 +76,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,20 +110,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); - // _explicitPaths); TODO: Implement this once we do OAuth somehow - + _requestCache); // _explicitPaths); TODO: Implement this once we do OAuth somehow options.Events = new CookieAuthenticationEvents { @@ -118,22 +132,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); @@ -153,19 +167,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)); } @@ -182,12 +196,12 @@ namespace Umbraco.Web.BackOffice.Security }, 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); } @@ -224,10 +238,13 @@ namespace Umbraco.Web.BackOffice.Security /// private async Task EnsureValidSessionId(CookieValidatePrincipalContext context) { - if (_runtimeState.Level != RuntimeLevel.Run) return; + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } - using var scope = _serviceProvider.CreateScope(); - var validator = scope.ServiceProvider.GetRequiredService(); + using IServiceScope scope = _serviceProvider.CreateScope(); + BackOfficeSessionIdValidator validator = scope.ServiceProvider.GetRequiredService(); await validator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } @@ -235,21 +252,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; 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/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.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 1a4298cd6b..180f433fab 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; @@ -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/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.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs new file mode 100644 index 0000000000..081ca6b581 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +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.Configuration.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Extensions; +using Umbraco.Net; +using Umbraco.Web.Models.ContentEditing; + + +namespace Umbraco.Web.Common.Security +{ + public class BackOfficeUserManager : UmbracoUserManager, IBackOfficeUserManager + { + 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(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration) + { + _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: + /// * Get password store + /// * Call VerifyPasswordAsync with the password store + user + password + /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password + /// + /// In some cases people want simple custom control over the username/password check, for simplicity + /// sake, developers would like the users to simply validate against an LDAP directory but the user + /// data remains stored inside of Umbraco. + /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. + /// + /// 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(BackOfficeIdentityUser user, string password) + { + if (BackOfficeUserPasswordChecker != null) + { + 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 (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) + { + return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; + } + } + + // use the default behavior + return await base.CheckPasswordAsync(user, 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(BackOfficeIdentityUser user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + 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(string userId, string token, string newPassword) + { + IdentityResult result = await base.ChangePasswordWithResetAsync(userId, token, newPassword); + if (result.Succeeded) + { + RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId); + } + + return result; + } + + 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; + } + + /// + public override async Task SetLockoutEndDateAsync(BackOfficeIdentityUser 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) + { + RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + } + else + { + RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id); + + // Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(user); + } + + return result; + } + + /// + public override async Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) + { + IdentityResult result = await base.ResetAccessFailedCountAsync(user); + + // raise the event now that it's reset + RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id); + + return result; + } + + private string GetCurrentUserId(IPrincipal currentUser) + { + UmbracoBackOfficeIdentity umbIdentity = currentUser?.GetUmbracoIdentity(); + var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserIdAsString; + return currentUserId; + } + + 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, string affectedUserId, string affectedUsername) + { + var currentUserId = currentUser.Id; + var ip = IpResolver.GetCurrentRequestIpAddress(); + return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); + } + + // 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, string userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty)); + + public void RaiseAccountUnlockedEvent(IPrincipal currentUser, string userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty)); + + public void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId) => OnForgotPasswordRequested(CreateArgs(AuditEvent.ForgotPasswordRequested, currentUser, userId, string.Empty)); + + public void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId) => OnForgotPasswordChangedSuccess(CreateArgs(AuditEvent.ForgotPasswordChangedSuccess, currentUser, userId, string.Empty)); + + public void RaiseLoginFailedEvent(IPrincipal currentUser, string userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty)); + + public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, string userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); + + public void RaiseLoginSuccessEvent(IPrincipal currentUser, string userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); + + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId) + { + var currentUserId = GetCurrentUserId(currentUser); + var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); + OnLogoutSuccess(args); + return args; + } + + public void RaisePasswordChangedEvent(IPrincipal currentUser, string userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, 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) + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); + var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); + OnSendingUserInvite(args); + return args; + } + + public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; + + // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot + // have non-static events here because the user manager is a Scoped instance not a singleton + // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely + public static event EventHandler AccountLocked; + public static event EventHandler AccountUnlocked; + public static event EventHandler ForgotPasswordRequested; + public static event EventHandler ForgotPasswordChangedSuccess; + public static event EventHandler LoginFailed; + public static event EventHandler LoginRequiresVerification; + public static event EventHandler LoginSuccess; + public static event EventHandler LogoutSuccess; + public static event EventHandler PasswordChanged; + public static event EventHandler PasswordReset; + public static event EventHandler ResetAccessFailedCount; + + /// + /// Raised when a user is invited + /// + public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it + + protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); + + protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); + + protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); + + protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); + + protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) => ForgotPasswordChangedSuccess?.Invoke(this, e); + + protected virtual void OnLoginFailed(IdentityAuditEventArgs e) => LoginFailed?.Invoke(this, e); + + protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) => LoginRequiresVerification?.Invoke(this, e); + + protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); + + protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); + + protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); + + protected virtual void OnPasswordReset(IdentityAuditEventArgs e) => PasswordReset?.Invoke(this, e); + + protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) => ResetAccessFailedCount?.Invoke(this, e); + } +} 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()) diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index 82c9cb8496..46b6540d73 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -1,14 +1,14 @@ -using System; +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; 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/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 e5ba931b0b..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ /dev/null @@ -1,46 +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; - -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/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 { 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