diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 516f26774a..60aa666034 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -16,16 +16,6 @@ namespace Umbraco.Cms.Core.Composing { private readonly Assembly _entryPointAssembly; private readonly ILoggerFactory _loggerFactory; - private static readonly string[] UmbracoCoreAssemblyNames = new[] - { - "Umbraco.Core", - "Umbraco.Infrastructure", - "Umbraco.PublishedCache.NuCache", - "Umbraco.Examine.Lucene", - "Umbraco.Web.Common", - "Umbraco.Web.BackOffice", - "Umbraco.Web.Website", - }; public DefaultUmbracoAssemblyProvider(Assembly entryPointAssembly, ILoggerFactory loggerFactory) { @@ -43,7 +33,7 @@ namespace Umbraco.Cms.Core.Composing { get { - var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, UmbracoCoreAssemblyNames, true, _loggerFactory); + var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, Constants.Composing.UmbracoCoreAssemblyNames, true, _loggerFactory); return finder.Find(); } } diff --git a/src/Umbraco.Core/Composing/ReferenceResolver.cs b/src/Umbraco.Core/Composing/ReferenceResolver.cs index 6ecd425ac1..fdc72a183b 100644 --- a/src/Umbraco.Core/Composing/ReferenceResolver.cs +++ b/src/Umbraco.Core/Composing/ReferenceResolver.cs @@ -82,9 +82,8 @@ namespace Umbraco.Cms.Core.Composing assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) continue; - // don't include this item if it's Umbraco - // TODO: We should maybe pass an explicit list of these names in? - if (assemblyName.FullName.StartsWith("Umbraco.") || assemblyName.Name.EndsWith(".Views")) + // don't include this item if it's Umbraco Core + if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x=>assemblyName.FullName.StartsWith(x) || assemblyName.Name.EndsWith(".Views"))) continue; var assembly = Assembly.Load(assemblyName); diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index a92f71ee04..747a74b8d8 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -9,6 +9,17 @@ /// Defines constants for composition. /// public static class Composing - { } + { + public static readonly string[] UmbracoCoreAssemblyNames = new[] + { + "Umbraco.Core", + "Umbraco.Infrastructure", + "Umbraco.PublishedCache.NuCache", + "Umbraco.Examine.Lucene", + "Umbraco.Web.Common", + "Umbraco.Web.BackOffice", + "Umbraco.Web.Website", + }; + } } } diff --git a/src/Umbraco.Core/Dashboards/ContentDashboard.cs b/src/Umbraco.Core/Dashboards/ContentDashboard.cs index b959851cc7..135fe4304d 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboard.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Composing; +using System; +using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Dashboards { @@ -7,21 +8,10 @@ namespace Umbraco.Cms.Core.Dashboards { public string Alias => "contentIntro"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; public string View => "views/dashboard/default/startupdashboardintro.html"; - public IAccessRule[] AccessRules - { - get - { - var rules = new IAccessRule[] - { - new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, - new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} - }; - return rules; - } - } + public IAccessRule[] AccessRules { get; } = Array.Empty(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ce524a09a1..cade1041ac 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -46,6 +46,17 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + + <_Parameter1>Umbraco.Forms.Core + + + <_Parameter1>Umbraco.Forms.Core.Providers + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 9a91893515..3cb7ff1b4a 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -37,7 +37,11 @@ namespace Umbraco.Cms.Core.Cache INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private List _unbinders; @@ -74,12 +78,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => FileService.DeletedStylesheet += FileService_DeletedStylesheet, () => FileService.DeletedStylesheet -= FileService_DeletedStylesheet); - // bind to domain events - Bind(() => DomainService.Saved += DomainService_Saved, - () => DomainService.Saved -= DomainService_Saved); - Bind(() => DomainService.Deleted += DomainService_Deleted, - () => DomainService.Deleted -= DomainService_Deleted); - // bind to content type events Bind(() => ContentTypeService.Changed += ContentTypeService_Changed, () => ContentTypeService.Changed -= ContentTypeService_Changed); @@ -94,12 +92,6 @@ namespace Umbraco.Cms.Core.Cache Bind(() => FileService.DeletedTemplate += FileService_DeletedTemplate, () => FileService.DeletedTemplate -= FileService_DeletedTemplate); - // bind to macro events - Bind(() => MacroService.Saved += MacroService_Saved, - () => MacroService.Saved -= MacroService_Saved); - Bind(() => MacroService.Deleted += MacroService_Deleted, - () => MacroService.Deleted -= MacroService_Deleted); - // bind to media events - handles all media changes Bind(() => MediaService.TreeChanged += MediaService_TreeChanged, () => MediaService.TreeChanged -= MediaService_TreeChanged); @@ -203,16 +195,20 @@ namespace Umbraco.Cms.Core.Cache #region DomainService - private void DomainService_Saved(IDomainService sender, SaveEventArgs e) + public void Handle(DomainSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (IDomain entity in notification.SavedEntities) + { _distributedCache.RefreshDomainCache(entity); + } } - private void DomainService_Deleted(IDomainService sender, DeleteEventArgs e) + public void Handle(DomainDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (IDomain entity in notification.DeletedEntities) + { _distributedCache.RemoveDomainCache(entity); + } } #endregion @@ -340,16 +336,20 @@ namespace Umbraco.Cms.Core.Cache #region MacroService - private void MacroService_Deleted(IMacroService sender, DeleteEventArgs e) + public void Handle(MacroDeletedNotification notification) { - foreach (var entity in e.DeletedEntities) + foreach (IMacro entity in notification.DeletedEntities) + { _distributedCache.RemoveMacroCache(entity); + } } - private void MacroService_Saved(IMacroService sender, SaveEventArgs e) + public void Handle(MacroSavedNotification notification) { - foreach (var entity in e.SavedEntities) + foreach (IMacro entity in notification.SavedEntities) + { _distributedCache.RefreshMacroCache(entity); + } } #endregion diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index 73b2dcc99d..b5211522fc 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -84,7 +84,12 @@ namespace Umbraco.Cms.Core.Compose .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + ; // add notification handlers for auditing builder diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index 6e732cdc0f..1722fb1d2d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -202,8 +202,8 @@ namespace Umbraco.Cms.Core.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.Where(x=>x.Value.HasValues).SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + foreach (var umbracoEntityReference in mediaValues.Where(x=>x.Value.HasValues) + .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs index 2bb9b1ab8d..c86bda44e0 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs @@ -23,20 +23,29 @@ namespace Umbraco.Cms.Core.Security { } - /// - /// - /// Returns a ClaimsIdentity that has the required claims, and allows flowing of claims from external identity - /// - public override async Task CreateAsync(BackOfficeIdentityUser user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + protected virtual string AuthenticationType { get; } = Constants.Security.BackOfficeAuthenticationType; + /// + protected override async Task GenerateClaimsAsync(BackOfficeIdentityUser user) + { + // NOTE: 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 which is not what we want. + // so we override this method to change it. + + // get the base ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user); - baseIdentity.AddRequiredClaims( + // now create a new one with the correct authentication type + var id = new ClaimsIdentity( + AuthenticationType, + Options.ClaimsIdentity.UserNameClaimType, + Options.ClaimsIdentity.RoleClaimType); + + // and merge all others from the base implementation + id.MergeAllClaims(baseIdentity); + + // ensure our required claims are there + id.AddRequiredClaims( user.Id, user.UserName, user.Name, @@ -49,23 +58,9 @@ namespace Umbraco.Cms.Core.Security // now we can flow any custom claims that the actual user has currently // assigned which could be done in the OnExternalLogin callback - baseIdentity.MergeClaimsFromBackOfficeIdentity(user); + id.MergeClaimsFromBackOfficeIdentity(user); - return new ClaimsPrincipal(baseIdentity); - } - - /// - 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. - // also, this is the method that we should be returning our UmbracoBackOfficeIdentity from , not the method above, - // 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 - - ClaimsIdentity identity = await base.GenerateClaimsAsync(user); - - return identity; + return id; } } } diff --git a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs index 1a37376070..d4b61a934d 100644 --- a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs @@ -13,6 +13,15 @@ namespace Umbraco.Extensions // is re-issued and we don't want to merge old values of these. private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source) + { + foreach (Claim claim in source.Claims + .Where(claim => !destination.HasClaim(claim.Type, claim.Value))) + { + destination.AddClaim(new Claim(claim.Type, claim.Value)); + } + } + public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source) { foreach (Claim claim in source.Claims diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index c0b9a19ef1..7e36081e73 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { private const string genericIdentityErrorCode = "IdentityErrorUserStore"; private readonly IMemberService _memberService; @@ -562,7 +562,7 @@ namespace Umbraco.Cms.Core.Security } /// - protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) + protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(roleName)) { @@ -572,10 +572,10 @@ namespace Umbraco.Cms.Core.Security IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == roleName); if (group == null) { - return Task.FromResult((IdentityRole)null); + return Task.FromResult((UmbracoIdentityRole)null); } - return Task.FromResult(new IdentityRole(group.Name) + return Task.FromResult(new UmbracoIdentityRole(group.Name) { //TODO: what should the alias be? Id = group.Id.ToString() diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs index 9d06dcd037..00c4038287 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs @@ -10,6 +10,14 @@ namespace Umbraco.Cms.Core.Models.Identity private string _id; private string _name; + public UmbracoIdentityRole(string roleName) : base(roleName) + { + } + + public UmbracoIdentityRole() + { + } + public event PropertyChangedEventHandler PropertyChanged { add diff --git a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs b/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs index 2b7d964a13..0540c04d64 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DomainService.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Services.Notifications; namespace Umbraco.Cms.Core.Services.Implement { @@ -28,25 +29,24 @@ namespace Umbraco.Cms.Core.Services.Implement public Attempt Delete(IDomain domain) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages eventMessages = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(domain, evtMsgs); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + var deletingNotification = new DomainDeletingNotification(domain, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); + return OperationResult.Attempt.Cancel(eventMessages); } _domainRepository.Delete(domain); scope.Complete(); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); + scope.Notifications.Publish(new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification)); } - return OperationResult.Attempt.Succeed(evtMsgs); + return OperationResult.Attempt.Succeed(eventMessages); } public IDomain GetByName(string name) @@ -83,48 +83,23 @@ namespace Umbraco.Cms.Core.Services.Implement public Attempt Save(IDomain domainEntity) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages eventMessages = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(domainEntity, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + var savingNotification = new DomainSavingNotification(domainEntity, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); + return OperationResult.Attempt.Cancel(eventMessages); } _domainRepository.Save(domainEntity); scope.Complete(); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + scope.Notifications.Publish(new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification)); } - return OperationResult.Attempt.Succeed(evtMsgs); + return OperationResult.Attempt.Succeed(eventMessages); } - - #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - - #endregion } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs index c42d29b3c0..68192218eb 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs @@ -6,13 +6,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Services.Notifications; namespace Umbraco.Cms.Core.Services.Implement { /// /// Represents the Macro Service, which is an easy access to operations involving /// - public class MacroService : RepositoryService, IMacroService + internal class MacroService : RepositoryService, IMacroService { private readonly IMacroRepository _macroRepository; private readonly IAuditRepository _auditRepository; @@ -83,18 +84,19 @@ namespace Umbraco.Cms.Core.Services.Implement /// Optional id of the user deleting the macro public void Delete(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var deleteEventArgs = new DeleteEventArgs(macro); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new MacroDeletingNotification(macro, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _macroRepository.Delete(macro); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); + + scope.Notifications.Publish(new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification)); Audit(AuditType.Delete, userId, -1); scope.Complete(); @@ -108,10 +110,12 @@ namespace Umbraco.Cms.Core.Services.Implement /// Optional Id of the user deleting the macro public void Save(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var saveEventArgs = new SaveEventArgs(macro); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new MacroSavingNotification(macro, eventMessages); + + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -123,8 +127,8 @@ namespace Umbraco.Cms.Core.Services.Implement } _macroRepository.Save(macro); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); + + scope.Notifications.Publish(new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification)); Audit(AuditType.Save, userId, -1); scope.Complete(); @@ -154,27 +158,5 @@ namespace Umbraco.Cms.Core.Services.Implement { _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro")); } - - #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - #endregion } } diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletedNotification.cs new file mode 100644 index 0000000000..3210b5cc24 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletedNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DomainDeletedNotification : DeletedNotification + { + public DomainDeletedNotification(IDomain target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletingNotification.cs new file mode 100644 index 0000000000..b16a35ee14 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DomainDeletingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DomainDeletingNotification : DeletingNotification + { + public DomainDeletingNotification(IDomain target, EventMessages messages) : base(target, messages) + { + } + + public DomainDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DomainSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DomainSavedNotification.cs new file mode 100644 index 0000000000..6cfaa2304c --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DomainSavedNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DomainSavedNotification : SavedNotification + { + public DomainSavedNotification(IDomain target, EventMessages messages) : base(target, messages) + { + } + + public DomainSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/DomainSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/DomainSavingNotification.cs new file mode 100644 index 0000000000..2689082314 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/DomainSavingNotification.cs @@ -0,0 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class DomainSavingNotification : SavingNotification + { + public DomainSavingNotification(IDomain target, EventMessages messages) : base(target, messages) + { + } + + public DomainSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletedNotification.cs new file mode 100644 index 0000000000..56e0ea759b --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MacroDeletedNotification : DeletedNotification + { + public MacroDeletedNotification(IMacro target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletingNotification.cs new file mode 100644 index 0000000000..fbf560cb77 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MacroDeletingNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MacroDeletingNotification : DeletingNotification + { + public MacroDeletingNotification(IMacro target, EventMessages messages) : base(target, messages) + { + } + + public MacroDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MacroSavedNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MacroSavedNotification.cs new file mode 100644 index 0000000000..e93b8e3cec --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MacroSavedNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MacroSavedNotification : SavedNotification + { + public MacroSavedNotification(IMacro target, EventMessages messages) : base(target, messages) + { + } + + public MacroSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Notifications/MacroSavingNotification.cs b/src/Umbraco.Infrastructure/Services/Notifications/MacroSavingNotification.cs new file mode 100644 index 0000000000..689272620c --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Notifications/MacroSavingNotification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Services.Notifications +{ + public class MacroSavingNotification : SavingNotification + { + public MacroSavingNotification(IMacro target, EventMessages messages) : base(target, messages) + { + } + + public MacroSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index e5ee719a05..baa69dddb9 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -101,6 +101,17 @@ <_Parameter1>DynamicProxyGenAssembly2 + + + + <_Parameter1>Umbraco.Forms.Core + + + <_Parameter1>Umbraco.Forms.Core.Providers + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs index 2a14736d57..c1a527e775 100644 --- a/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests.Integration/Cache/DistributedCacheBinderTests.cs @@ -45,9 +45,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, FileService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, FileService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DomainService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, DomainService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, ContentTypeService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, ContentTypeService, new DeleteEventArgs(Enumerable.Empty())), new EventDefinition>(null, MediaTypeService, new SaveEventArgs(Enumerable.Empty())), @@ -59,9 +56,6 @@ namespace Umbraco.Cms.Tests.Integration.Cache new EventDefinition>(null, FileService, new SaveEventArgs(Enumerable.Empty())), new EventDefinition>(null, FileService, new DeleteEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MacroService, new SaveEventArgs(Enumerable.Empty())), - new EventDefinition>(null, MacroService, new DeleteEventArgs(Enumerable.Empty())), - // not managed //new EventDefinition>(null, ContentService, new SaveEventArgs(Enumerable.Empty()), "SavedBlueprint"), //new EventDefinition>(null, ContentService, new DeleteEventArgs(Enumerable.Empty()), "DeletedBlueprint"), diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs similarity index 80% rename from src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index ffa778765c..e3f936bcac 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -10,18 +10,25 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security { [TestFixture] - public class MemberIdentityUserManagerTests + public class MemberManagerTests { - private Mock> _mockMemberStore; + private MemberUserStore _fakeMemberStore; private Mock> _mockIdentityOptions; private Mock> _mockPasswordHasher; + private Mock _mockMemberService; private Mock> _mockUserValidators; private Mock>> _mockPasswordValidators; private Mock _mockNormalizer; @@ -32,9 +39,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public MemberManager CreateSut() { - _mockMemberStore = new Mock>(); - _mockIdentityOptions = new Mock>(); + _mockMemberService = new Mock(); + _fakeMemberStore = new MemberUserStore( + _mockMemberService.Object, + new UmbracoMapper(new MapDefinitionCollection(new List())), + new Mock().Object, + new IdentityErrorDescriber()); + _mockIdentityOptions = new Mock>(); var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); _mockPasswordHasher = new Mock>(); @@ -63,7 +75,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security var userManager = new MemberManager( new Mock().Object, - _mockMemberStore.Object, + _fakeMemberStore, _mockIdentityOptions.Object, _mockPasswordHasher.Object, userValidators, @@ -100,10 +112,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } }; - _mockMemberStore.Setup(x => - x.CreateAsync(fakeUser, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Failed(identityErrors)); - //act IdentityResult identityResult = await sut.CreateAsync(fakeUser); @@ -129,10 +137,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security } }; - _mockMemberStore.Setup(x => - x.CreateAsync(null, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Failed(identityErrors)); - //act var identityResult = new Func>(() => sut.CreateAsync(null)); @@ -141,20 +145,30 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security Assert.That(identityResult, Throws.ArgumentNullException); } - [Test] public async Task GivenICreateANewUser_AndTheUserIsPopulatedCorrectly_ThenIShouldGetASuccessResultAsync() { //arrange MemberManager sut = CreateSut(); - MemberIdentityUser fakeUser = new MemberIdentityUser() + var fakeUser = new MemberIdentityUser(777) { + UserName = "testUser", + Email = "test@test.com", + Name = "Test", + MemberTypeAlias = "Anything", PasswordConfig = "testConfig" }; - CancellationToken fakeCancellationToken = new CancellationToken() { }; - _mockMemberStore.Setup(x => - x.CreateAsync(fakeUser, fakeCancellationToken)) - .ReturnsAsync(IdentityResult.Success); + + var builder = new MemberTypeBuilder(); + MemberType memberType = builder.BuildSimpleMemberType(); + + IMember fakeMember = new Member(memberType) + { + Id = 777 + }; + + _mockMemberService.Setup(x => x.CreateMember(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(fakeMember); + _mockMemberService.Setup(x => x.Save(fakeMember, false)); //act IdentityResult identityResult = await sut.CreateAsync(fakeUser); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs new file mode 100644 index 0000000000..bdcc5ec75b --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security +{ + [TestFixture] + public class MemberSignInManagerTests + { + private Mock>> _mockLogger; + private readonly Mock> _memberManager = MockUserManager(); + + public MemberClaimsPrincipalFactory CreateClaimsFactory(UserManager userMgr) + => new MemberClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions())); + + public MemberSignInManager CreateSut() + { + // This all needs to be setup because internally aspnet resolves a bunch + // of services from the HttpContext.RequestServices. + var serviceProviderFactory = new DefaultServiceProviderFactory(); + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddAuthentication() + .AddCookie(IdentityConstants.ApplicationScheme); + IServiceProvider serviceProvider = serviceProviderFactory.CreateServiceProvider(serviceCollection); + var httpContextFactory = new DefaultHttpContextFactory(serviceProvider); + IFeatureCollection features = new DefaultHttpContext().Features; + features.Set(new HttpConnectionFeature + { + LocalIpAddress = IPAddress.Parse("127.0.0.1") + }); + HttpContext httpContext = httpContextFactory.Create(features); + + _mockLogger = new Mock>>(); + return new MemberSignInManager( + _memberManager.Object, + Mock.Of(x => x.HttpContext == httpContext), + CreateClaimsFactory(_memberManager.Object), + Mock.Of>(), + _mockLogger.Object, + Mock.Of(), + Mock.Of>()); + } + private static Mock> MockUserManager() + { + var store = new Mock>(); + var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null); + return mgr; + } + + [Test] + public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() + { + //arrange + var userId = "bo8w3d32q9b98"; + MemberSignInManager sut = CreateSut(); + var fakeUser = new MemberIdentityUser(777) + { + UserName = "TestUser", + }; + var password = "testPassword"; + var lockoutOnFailure = false; + var isPersistent = true; + + _memberManager.Setup(x => x.GetUserIdAsync(It.IsAny())).ReturnsAsync(userId); + _memberManager.Setup(x => x.GetUserNameAsync(It.IsAny())).ReturnsAsync(fakeUser.UserName); + _memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).ReturnsAsync(fakeUser); + _memberManager.Setup(x => x.CheckPasswordAsync(fakeUser, password)).ReturnsAsync(true); + _memberManager.Setup(x => x.IsEmailConfirmedAsync(fakeUser)).ReturnsAsync(true); + _memberManager.Setup(x => x.IsLockedOutAsync(fakeUser)).ReturnsAsync(false); + + //act + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); + + //assert + Assert.IsTrue(actual.Succeeded); + } + + [Test] + public async Task WhenPasswordSignInAsyncIsCalled_AndTheResultFails_ThenASignInFailedResultShouldBeReturnedAsync() + { + //arrange + MemberSignInManager sut = CreateSut(); + var fakeUser = new MemberIdentityUser(777) + { + UserName = "TestUser", + }; + var password = "testPassword"; + var lockoutOnFailure = false; + var isPersistent = true; + + //act + SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure); + + //assert + Assert.IsFalse(actual.Succeeded); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 42c37684a3..b3de544184 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -71,8 +71,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = _umbracoVersion.SemanticVersion.ToSemanticString(); + var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = _appCaches.RuntimeCache.GetCacheItem(key); diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index 03ebb1aa45..3e921ba0f9 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -17,17 +17,22 @@ namespace Umbraco.Cms.Web.BackOffice.Security { using Constants = Core.Constants; - public class BackOfficeSignInManager : SignInManager, IBackOfficeSignInManager + /// + /// The sign in manager for back office users + /// + public class BackOfficeSignInManager : UmbracoSignInManager, IBackOfficeSignInManager { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - private const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - private const string UmbracoSignInMgrXsrfKey = "XsrfId"; - private readonly BackOfficeUserManager _userManager; private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly GlobalSettings _globalSettings; + protected override string AuthenticationType => Constants.Security.BackOfficeAuthenticationType; + + protected override string ExternalAuthenticationType => Constants.Security.BackOfficeExternalAuthenticationType; + + protected override string TwoFactorAuthenticationType => Constants.Security.BackOfficeTwoFactorAuthenticationType; + + protected override string TwoFactorRememberMeAuthenticationType => Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType; public BackOfficeSignInManager( BackOfficeUserManager userManager, @@ -46,261 +51,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security _globalSettings = globalSettings.Value; } - // TODO: Have a look into RefreshSignInAsync since we might be able to use this new functionality for auto-cookie renewal in our middleware, though - // i suspect it's taken care of already. - - - /// - public override async Task PasswordSignInAsync(BackOfficeIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - var result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - return await HandleSignIn(user, user.UserName, result); - } - - /// - public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) - { - // override to handle logging/events - var user = await UserManager.FindByNameAsync(userName); - if (user == null) - return await HandleSignIn(null, userName, SignInResult.Failed); - return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); - } - - /// - public override async Task GetTwoFactorAuthenticationUserAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // replaced in order to use a custom auth type - - var info = await RetrieveTwoFactorInfoAsync(); - if (info == null) - { - return null; - } - return await UserManager.FindByIdAsync(info.UserId); - } - - /// - public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 - // replaced in order to use a custom auth type and to implement logging/events - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var error = await PreSignInCheck(user); - if (error != null) - { - return error; - } - if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); - return await HandleSignIn(user, user?.UserName, SignInResult.Success); - } - // If the token is incorrect, record the failure which also may cause the user to be locked out - await UserManager.AccessFailedAsync(user); - return await HandleSignIn(user, user?.UserName, SignInResult.Failed); - } - - - /// - public override bool IsSignedIn(ClaimsPrincipal principal) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - return principal?.Identities != null && - principal.Identities.Any(i => i.AuthenticationType == Constants.Security.BackOfficeAuthenticationType); - } - - /// - public override async Task RefreshSignInAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 - // replaced in order to use a custom auth type - - var auth = await Context.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - IList claims = Array.Empty(); - - var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); - var amr = auth?.Principal?.FindFirst("amr"); - - if (authenticationMethod != null || amr != null) - { - claims = new List(); - if (authenticationMethod != null) - { - claims.Add(authenticationMethod); - } - if (amr != null) - { - claims.Add(amr); - } - } - - await SignInWithClaimsAsync(user, auth?.Properties, claims); - } - - /// - public override async Task SignInWithClaimsAsync(BackOfficeIdentityUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) - { - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.BackOfficeAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // we also override to set the current HttpContext principal since this isn't done by default - - var userPrincipal = await CreateUserPrincipalAsync(user); - foreach (var claim in additionalClaims) - userPrincipal.Identities.First().AddClaim(claim); - - // FYI (just for informational purposes): - // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` - // which then resolves the `IAuthenticationSignInHandler` for the current scheme - // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` - // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync - - // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our - // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created - - // Also note, this method gets called when performing 2FA logins - - await Context.SignInAsync(Constants.Security.BackOfficeAuthenticationType, - userPrincipal, - authenticationProperties ?? new AuthenticationProperties()); - } - - /// - public override async Task SignOutAsync() - { - // override to replace IdentityConstants.ApplicationScheme with Constants.Security.BackOfficeAuthenticationType - // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - - await Context.SignOutAsync(Constants.Security.BackOfficeAuthenticationType); - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - } - - - /// - public override async Task IsTwoFactorClientRememberedAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var userId = await UserManager.GetUserIdAsync(user); - var result = await Context.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId); - } - - - /// - public override async Task RememberTwoFactorClientAsync(BackOfficeIdentityUser user) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var principal = await StoreRememberClient(user); - await Context.SignInAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType, - principal, - new AuthenticationProperties { IsPersistent = true }); - } - - - /// - public override Task ForgetTwoFactorClientAsync() - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - return Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - } - - - /// - public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); - if (twoFactorInfo == null || twoFactorInfo.UserId == null) - { - return SignInResult.Failed; - } - var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); - if (user == null) - { - return SignInResult.Failed; - } - - var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); - if (result.Succeeded) - { - await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); - return SignInResult.Success; - } - - // We don't protect against brute force attacks since codes are expected to be random. - return SignInResult.Failed; - } - - - /// - public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 - // to replace the auth scheme - - var auth = await Context.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType); - var items = auth?.Properties?.Items; - if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) - { - return null; - } - - if (expectedXsrf != null) - { - if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) - { - return null; - } - var userId = items[UmbracoSignInMgrXsrfKey]; - if (userId != expectedXsrf) - { - return null; - } - } - - var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - var provider = items[UmbracoSignInMgrLoginProviderKey] as string; - if (providerKey == null || provider == null) - { - return null; - } - - var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; - return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) - { - AuthenticationTokens = auth.Properties.GetTokens(), - AuthenticationProperties = auth.Properties - }; - } - /// /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking /// @@ -367,66 +117,19 @@ namespace Umbraco.Cms.Web.BackOffice.Security return base.GetExternalAuthenticationSchemesAsync(); } - /// - protected override async Task SignInOrTwoFactorAsync(BackOfficeIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) - { - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // to replace custom auth types - - if (!bypassTwoFactor && await IsTfaEnabled(user)) - { - if (!await IsTwoFactorClientRememberedAsync(user)) - { - // Store the userId for use after two factor check - var userId = await UserManager.GetUserIdAsync(user); - await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider)); - return SignInResult.TwoFactorRequired; - } - } - // Cleanup external cookie - if (loginProvider != null) - { - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - } - if (loginProvider == null) - { - await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); - } - else - { - await SignInAsync(user, isPersistent, loginProvider); - } - return SignInResult.Success; - } - /// - /// Called on any login attempt to update the AccessFailedCount and to raise events + /// Overridden to deal with events/notificiations /// /// /// /// /// - private async Task HandleSignIn(BackOfficeIdentityUser user, string username, SignInResult result) + protected override async Task HandleSignIn(BackOfficeIdentityUser user, string username, SignInResult result) { - // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController - - if (username.IsNullOrWhiteSpace()) - { - username = "UNKNOWN"; // could happen in 2fa or something else weird - } + result = await base.HandleSignIn(user, username, result); if (result.Succeeded) { - //track the last login date - user.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - { - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - } - await UserManager.UpdateAsync(user); - - Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); if (user != null) { _userManager.NotifyLoginSuccess(Context.User, user.Id); @@ -435,16 +138,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security else if (result.IsLockedOut) { _userManager.NotifyAccountLocked(Context.User, user.Id); - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); } else if (result.RequiresTwoFactor) { _userManager.NotifyLoginRequiresVerification(Context.User, user.Id); - Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); } else if (!result.Succeeded || result.IsNotAllowed) { - Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); } else { @@ -454,92 +154,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security return result; } - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 - // since it's not public - private async Task IsTfaEnabled(BackOfficeIdentityUser user) - => UserManager.SupportsUserTwoFactor && - await UserManager.GetTwoFactorEnabledAsync(user) && - (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 - // to replace custom auth types - private ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider) - { - var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType); - identity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (loginProvider != null) - { - identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); - } - return new ClaimsPrincipal(identity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task StoreRememberClient(BackOfficeIdentityUser user) - { - var userId = await UserManager.GetUserIdAsync(user); - var rememberBrowserIdentity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType); - rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); - if (UserManager.SupportsUserSecurityStamp) - { - var stamp = await UserManager.GetSecurityStampAsync(user); - rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); - } - return new ClaimsPrincipal(rememberBrowserIdentity); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use custom auth types - private async Task DoTwoFactorSignInAsync(BackOfficeIdentityUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) - { - // When token is verified correctly, clear the access failed count used for lockout - await ResetLockout(user); - - var claims = new List - { - new Claim("amr", "mfa") - }; - - // Cleanup external cookie - if (twoFactorInfo.LoginProvider != null) - { - claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); - await Context.SignOutAsync(Constants.Security.BackOfficeExternalAuthenticationType); - } - // Cleanup two factor user id cookie - await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (rememberClient) - { - await RememberTwoFactorClientAsync(user); - } - await SignInWithClaimsAsync(user, isPersistent, claims); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs - // copy is required in order to use a custom auth type - private async Task RetrieveTwoFactorInfoAsync() - { - var result = await Context.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType); - if (result?.Principal != null) - { - return new TwoFactorAuthenticationInfo - { - UserId = result.Principal.FindFirstValue(ClaimTypes.Name), - LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod) - }; - } - return null; - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 - private class TwoFactorAuthenticationInfo - { - public string UserId { get; set; } - public string LoginProvider { get; set; } - } - - /// /// Used for auto linking/creating user accounts for external logins /// diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index d49eb6e4f5..967e2043f4 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -31,6 +31,11 @@ <_Parameter1>Umbraco.Tests.Integration + + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs index 5182db4e20..b8f524902e 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; @@ -12,6 +11,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.Security; @@ -67,10 +67,11 @@ namespace Umbraco.Extensions services.BuildMembersIdentity() .AddDefaultTokenProviders() .AddMemberManager() + .AddClaimsPrincipalFactory() .AddUserStore() .AddRoleStore() - .AddRoleValidator>() - .AddRoleManager>(); + .AddRoleValidator>() + .AddRoleManager>(); private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services) { @@ -78,7 +79,7 @@ namespace Umbraco.Extensions services.TryAddScoped, UserValidator>(); services.TryAddScoped, PasswordValidator>(); services.TryAddScoped, PasswordHasher>(); - return new MemberIdentityBuilder(typeof(IdentityRole), services); + return new MemberIdentityBuilder(typeof(UmbracoIdentityRole), services); } private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) diff --git a/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..fe9a0eadd4 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; + + +namespace Umbraco.Cms.Web.Common.Security +{ + public class MemberClaimsPrincipalFactory : UserClaimsPrincipalFactory + { + public MemberClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) + : base(userManager, optionsAccessor) + { + } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 887dfa5b92..f3b80ba4bc 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -10,9 +10,9 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Web.Common.Security { + public class MemberManager : UmbracoUserManager, IMemberManager { - public MemberManager( IIpResolver ipResolver, IUserStore store, @@ -24,7 +24,8 @@ namespace Umbraco.Cms.Web.Common.Security IServiceProvider services, ILogger> logger, IOptions passwordConfiguration) - : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, + services, logger, passwordConfiguration) { } } diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs new file mode 100644 index 0000000000..eeec3c2899 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// The sign in manager for members + /// + public class MemberSignInManager : UmbracoSignInManager + { + public MemberSignInManager( + UserManager memberManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) : + base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { } + + // use default scheme for members + protected override string AuthenticationType => IdentityConstants.ApplicationScheme; + + // use default scheme for members + protected override string ExternalAuthenticationType => IdentityConstants.ExternalScheme; + + // use default scheme for members + protected override string TwoFactorAuthenticationType => IdentityConstants.TwoFactorUserIdScheme; + + // use default scheme for members + protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; + + /// + public override Task GetTwoFactorAuthenticationUserAsync() + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task IsTwoFactorClientRememberedAsync(MemberIdentityUser user) + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task RememberTwoFactorClientAsync(MemberIdentityUser user) + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task ForgetTwoFactorClientAsync() + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + => throw new NotImplementedException("Two factor is not yet implemented for members"); + + /// + public override Task GetExternalLoginInfoAsync(string expectedXsrf = null) + => throw new NotImplementedException("External login is not yet implemented for members"); + + /// + public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) + => throw new NotImplementedException("External login is not yet implemented for members"); + + /// + public override Task> GetExternalAuthenticationSchemesAsync() + => throw new NotImplementedException("External login is not yet implemented for members"); + + } +} diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs new file mode 100644 index 0000000000..ea29098bef --- /dev/null +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// Abstract sign in manager implementation allowing modifying all defeault authentication schemes + /// + /// + public abstract class UmbracoSignInManager : SignInManager + where TUser : UmbracoIdentityUser + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider"; + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + protected const string UmbracoSignInMgrXsrfKey = "XsrfId"; + + public UmbracoSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } + + protected abstract string AuthenticationType { get; } + protected abstract string ExternalAuthenticationType { get; } + protected abstract string TwoFactorAuthenticationType { get; } + protected abstract string TwoFactorRememberMeAuthenticationType { get; } + + /// + public override async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + var result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + return await HandleSignIn(user, user.UserName, result); + } + + /// + public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var auth = await Context.AuthenticateAsync(ExternalAuthenticationType); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) + { + return null; + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) + { + return null; + } + var userId = items[UmbracoSignInMgrXsrfKey]; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider) + { + return null; + } + + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens(), + AuthenticationProperties = auth.Properties + }; + } + + /// + public override async Task GetTwoFactorAuthenticationUserAsync() + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // replaced in order to use a custom auth type + + var info = await RetrieveTwoFactorInfoAsync(); + if (info == null) + { + return null; + } + return await UserManager.FindByIdAsync(info.UserId); + } + + /// + public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) + { + // override to handle logging/events + var user = await UserManager.FindByNameAsync(userName); + if (user == null) + { + return await HandleSignIn(null, userName, SignInResult.Failed); + } + + return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); + } + + /// + public override bool IsSignedIn(ClaimsPrincipal principal) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + return principal?.Identities != null && + principal.Identities.Any(i => i.AuthenticationType == AuthenticationType); + } + + /// + public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552 + // replaced in order to use a custom auth type and to implement logging/events + + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + var error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient); + return await HandleSignIn(user, user?.UserName, SignInResult.Success); + } + // If the token is incorrect, record the failure which also may cause the user to be locked out + await UserManager.AccessFailedAsync(user); + return await HandleSignIn(user, user?.UserName, SignInResult.Failed); + } + + /// + public override async Task RefreshSignInAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126 + // replaced in order to use a custom auth type + + var auth = await Context.AuthenticateAsync(AuthenticationType); + IList claims = Array.Empty(); + + var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod); + var amr = auth?.Principal?.FindFirst("amr"); + + if (authenticationMethod != null || amr != null) + { + claims = new List(); + if (authenticationMethod != null) + { + claims.Add(authenticationMethod); + } + if (amr != null) + { + claims.Add(amr); + } + } + + await SignInWithClaimsAsync(user, auth?.Properties, claims); + } + + /// + public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable additionalClaims) + { + // override to replace IdentityConstants.ApplicationScheme with custom AuthenticationType + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // we also override to set the current HttpContext principal since this isn't done by default + + var userPrincipal = await CreateUserPrincipalAsync(user); + foreach (var claim in additionalClaims) + { + userPrincipal.Identities.First().AddClaim(claim); + } + + // FYI (just for informational purposes): + // This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync` + // which then resolves the `IAuthenticationSignInHandler` for the current scheme + // by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);` + // which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync + + // Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our + // own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created + + // Also note, this method gets called when performing 2FA logins + + await Context.SignInAsync( + AuthenticationType, + userPrincipal, + authenticationProperties ?? new AuthenticationProperties()); + } + + /// + public override async Task SignOutAsync() + { + // override to replace IdentityConstants.ApplicationScheme with custom auth types + // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + + await Context.SignOutAsync(AuthenticationType); + await Context.SignOutAsync(ExternalAuthenticationType); + await Context.SignOutAsync(TwoFactorAuthenticationType); + } + + /// + public override async Task IsTwoFactorClientRememberedAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var userId = await UserManager.GetUserIdAsync(user); + var result = await Context.AuthenticateAsync(TwoFactorRememberMeAuthenticationType); + return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId); + } + + /// + public override async Task RememberTwoFactorClientAsync(TUser user) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var principal = await StoreRememberClient(user); + await Context.SignInAsync(TwoFactorRememberMeAuthenticationType, + principal, + new AuthenticationProperties { IsPersistent = true }); + } + + /// + public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) + { + return SignInResult.Failed; + } + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); + if (user == null) + { + return SignInResult.Failed; + } + + var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode); + if (result.Succeeded) + { + await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false); + return SignInResult.Success; + } + + // We don't protect against brute force attacks since codes are expected to be random. + return SignInResult.Failed; + } + + /// + public override Task ForgetTwoFactorClientAsync() + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme + + return Context.SignOutAsync(TwoFactorRememberMeAuthenticationType); + } + + /// + /// Called on any login attempt to update the AccessFailedCount and to raise events + /// + /// + /// + /// + /// + protected virtual async Task HandleSignIn(TUser user, string username, SignInResult result) + { + // TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController + + if (username.IsNullOrWhiteSpace()) + { + username = "UNKNOWN"; // could happen in 2fa or something else weird + } + + if (result.Succeeded) + { + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + if (user.AccessFailedCount > 0) + { + //we have successfully logged in, reset the AccessFailedCount + user.AccessFailedCount = 0; + } + await UserManager.UpdateAsync(user); + + Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (result.IsLockedOut) + { + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress); + } + else if (result.RequiresTwoFactor) + { + Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else if (!result.Succeeded || result.IsNotAllowed) + { + Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress); + } + else + { + throw new ArgumentOutOfRangeException(); + } + + return result; + } + + /// + protected override async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to replace custom auth types + + if (!bypassTwoFactor && await IsTfaEnabled(user)) + { + if (!await IsTwoFactorClientRememberedAsync(user)) + { + // Store the userId for use after two factor check + var userId = await UserManager.GetUserIdAsync(user); + await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider)); + return SignInResult.TwoFactorRequired; + } + } + // Cleanup external cookie + if (loginProvider != null) + { + await Context.SignOutAsync(ExternalAuthenticationType); + } + if (loginProvider == null) + { + await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); + } + else + { + await SignInAsync(user, isPersistent, loginProvider); + } + return SignInResult.Success; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782 + // since it's not public + private async Task IsTfaEnabled(TUser user) + => UserManager.SupportsUserTwoFactor && + await UserManager.GetTwoFactorEnabledAsync(user) && + (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743 + // to replace custom auth types + private ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider) + { + var identity = new ClaimsIdentity(TwoFactorAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (loginProvider != null) + { + identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); + } + return new ClaimsPrincipal(identity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task StoreRememberClient(TUser user) + { + var userId = await UserManager.GetUserIdAsync(user); + var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberMeAuthenticationType); + rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (UserManager.SupportsUserSecurityStamp) + { + var stamp = await UserManager.GetSecurityStampAsync(user); + rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp)); + } + return new ClaimsPrincipal(rememberBrowserIdentity); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use a custom auth type + private async Task RetrieveTwoFactorInfoAsync() + { + var result = await Context.AuthenticateAsync(TwoFactorAuthenticationType); + if (result?.Principal != null) + { + return new TwoFactorAuthenticationInfo + { + UserId = result.Principal.FindFirstValue(ClaimTypes.Name), + LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod) + }; + } + return null; + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // copy is required in order to use custom auth types + private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient) + { + // When token is verified correctly, clear the access failed count used for lockout + await ResetLockout(user); + + var claims = new List + { + new Claim("amr", "mfa") + }; + + // Cleanup external cookie + if (twoFactorInfo.LoginProvider != null) + { + claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider)); + await Context.SignOutAsync(ExternalAuthenticationType); + } + // Cleanup two factor user id cookie + await Context.SignOutAsync(TwoFactorAuthenticationType); + if (rememberClient) + { + await RememberTwoFactorClientAsync(user); + } + await SignInWithClaimsAsync(user, isPersistent, claims); + } + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891 + private class TwoFactorAuthenticationInfo + { + public string UserId { get; set; } + public string LoginProvider { get; set; } + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index bb2ecbc346..24f20f2f4a 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -40,6 +40,11 @@ <_Parameter1>Umbraco.Tests.UnitTests + + + + <_Parameter1>Umbraco.Forms.Web + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 569f49b88a..f7cd32217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -29,10 +29,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var firstFocusableElement = focusableElements[0]; var lastFocusableElement = focusableElements[focusableElements.length -1]; - - // We need to add the tabbing-active class in order to highlight the focused button since the default style is - // outline: none; set in the stylesheet specifically - bodyElement.classList.add('tabbing-active'); // If there is no default focused element put focus on the first focusable element in the nodelist if(defaultFocusedElement === null ){ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 14643dc9cd..f9ebba00ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -572,13 +572,15 @@ * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ function editItem(item, scope) { + if (!item.editPath) { return; } - if (scope.options.useInfiniteEditor) + if (scope && scope.options && scope.options.useInfiniteEditor) { var editorModel = { id: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5ac..1f1c2c0e72 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 90b2dbe37e..17c62037cc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js index a7021b2867..33d526c3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js @@ -1,8 +1,8 @@ (function () { "use strict"; - function UserPickerController($scope, usersResource, localizationService, eventsService) { - + function UserPickerController($scope, entityResource, localizationService, eventsService) { + var vm = this; vm.users = []; @@ -102,17 +102,9 @@ vm.loading = true; // Get users - usersResource.getPagedResults(vm.usersOptions).then(function (users) { - - vm.users = users.items; - - vm.usersOptions.pageNumber = users.pageNumber; - vm.usersOptions.pageSize = users.pageSize; - vm.usersOptions.totalItems = users.totalItems; - vm.usersOptions.totalPages = users.totalPages; - + entityResource.getAll("User").then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); - vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 716ca405c1..94ea4b8604 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,9 +1,9 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, userService, editorService, localizationService) { - - $scope.thumbnailUrl = getThumbnailUrl(); - + function ($scope, userService, editorService, localizationService) { + + $scope.thumbnailUrl = getThumbnailUrl(); + if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { $scope.model.config.startNodeId = -1; @@ -29,16 +29,16 @@ angular.module("umbraco") onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, submit: model => { - updateControlValue(model.selection[0]); + updateControlValue(model.selection[0]); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; editorService.mediaPicker(mediaPicker); }; - $scope.editImage = function() { + $scope.editImage = function() { const mediaCropDetailsConfig = { size: 'small', @@ -47,17 +47,17 @@ angular.module("umbraco") updateControlValue(model.target); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; localizationService.localize('defaultdialogs_editSelectedMedia').then(value => { mediaCropDetailsConfig.title = value; editorService.mediaCropDetails(mediaCropDetailsConfig); - }); + }); } - + /** - * + * */ function getThumbnailUrl() { @@ -94,19 +94,15 @@ angular.module("umbraco") return url; } - + return null; } /** - * - * @param {object} selectedImage + * + * @param {object} selectedImage */ function updateControlValue(selectedImage) { - - const doGetThumbnail = $scope.control.value.focalPoint !== selectedImage.focalPoint - || $scope.control.value.image !== selectedImage.image; - // we could apply selectedImage directly to $scope.control.value, // but this allows excluding fields in future if needed $scope.control.value = { @@ -118,10 +114,6 @@ angular.module("umbraco") caption: selectedImage.caption, altText: selectedImage.altText }; - - - if (doGetThumbnail) { - $scope.thumbnailUrl = getThumbnailUrl(); - } - } + $scope.thumbnailUrl = getThumbnailUrl(); + } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js index f2055fea3a..217a9c8421 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js @@ -1,4 +1,4 @@ -function userPickerController($scope, usersResource , iconHelper, editorService, overlayService){ +function userPickerController($scope, iconHelper, editorService, overlayService, entityResource) { function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -92,17 +92,22 @@ function userPickerController($scope, usersResource , iconHelper, editorService, unsubscribe(); }); - //load user data - var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; - - // entityResource.getByIds doesn't support "User" and we would like to show avatars in umb-user-preview as well. - usersResource.getUsers(modelIds).then(function (data) { - _.each(data, function (item, i) { - // set default icon if it's missing - item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user"; - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon, avatars: item.avatars }); - }); - }); + //load user data - split to an array of ints (map) + const modelIds = $scope.model.value ? $scope.model.value.split(',').map(x => +x) : []; + if(modelIds.length !== 0) { + entityResource.getAll("User").then(function (users) { + const filteredUsers = users.filter(user => modelIds.indexOf(user.id) !== -1); + filteredUsers.forEach(item => { + $scope.renderModel.push({ + name: item.name, + id: item.id, + udi: item.udi, + icon: item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user", + avatars: item.avatars + }); + }); + }); + } } diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index a7c5e7a277..31a2ef25b2 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -34,5 +34,10 @@ <_Parameter1>Umbraco.Tests.Integration + + + + <_Parameter1>Umbraco.Forms.Web +