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