diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index c5f3d903ab..323fa6aeca 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -50,7 +50,12 @@ namespace Umbraco.Cms.Core.Cache /// public virtual void RefreshAll() { - OnCacheUpdated(NotificationFactory.Create(null, MessageType.RefreshAll)); + // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with + // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in + // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." + // In this case, all cache refreshers should be checking for the type first before checking for a msg value + // so this shouldn't cause any issues. + OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); } /// diff --git a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs index 26cf00a2d9..2165123b88 100644 --- a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs @@ -55,9 +55,9 @@ namespace Umbraco.Cms.Core.Cache foreach (var payload in payloads.Where(x => x.Id != default)) { //By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); //By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); _idKeyMap.ClearCache(payload.Id); diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs index fa4dca5ecc..3c1ad46f2c 100644 --- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs @@ -53,7 +53,7 @@ namespace Umbraco.Cms.Core.Cache var macroRepoCache = AppCaches.IsolatedCaches.Get(); if (macroRepoCache) { - macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } } diff --git a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs index a0101ab66c..dae367204c 100644 --- a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs @@ -56,8 +56,8 @@ namespace Umbraco.Cms.Core.Cache // repository cache // it *was* done for each pathId but really that does not make sense // only need to do it for the current media - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index b416889363..121dbea738 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -75,8 +75,8 @@ namespace Umbraco.Cms.Core.Cache _idKeyMap.ClearCache(p.Id); if (memberCache) { - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); } } diff --git a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs index 6f15d09554..a9694c92f5 100644 --- a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Core.Cache public override void Refresh(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Refresh(id); } @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Core.Cache public override void Remove(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Remove(id); } diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/UserCacheRefresher.cs index 201ecc1f19..706ed8ae45 100644 --- a/src/Umbraco.Core/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserCacheRefresher.cs @@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Cache var userCache = AppCaches.IsolatedCaches.Get(); if (userCache) { - userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs index 2d278972ec..3a7c8d12b1 100644 --- a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Core.Cache var userGroupCache = AppCaches.IsolatedCaches.Get(); if (userGroupCache) { - userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userGroupCache.Result.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); } diff --git a/src/Umbraco.Core/Compose/PublicAccessComposer.cs b/src/Umbraco.Core/Compose/PublicAccessComposer.cs deleted file mode 100644 index 3f6e20c17f..0000000000 --- a/src/Umbraco.Core/Compose/PublicAccessComposer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Services.Notifications; - -namespace Umbraco.Cms.Core.Compose -{ - /// - /// Used to ensure that the public access data file is kept up to date properly - /// - public sealed class PublicAccessComposer : ICoreComposer - { - public void Compose(IUmbracoBuilder builder) => - builder - .AddNotificationHandler() - .AddNotificationHandler(); - } -} diff --git a/src/Umbraco.Core/Composing/ICoreComposer.cs b/src/Umbraco.Core/Composing/ICoreComposer.cs index 24daae75b1..c6d0b3510c 100644 --- a/src/Umbraco.Core/Composing/ICoreComposer.cs +++ b/src/Umbraco.Core/Composing/ICoreComposer.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing { /// /// Represents a core . @@ -7,5 +7,7 @@ /// Core composers compose after the initial composer, and before user composers. /// public interface ICoreComposer : IComposer - { } + { + // TODO: This should die, there should be exactly zero core composers. + } } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index a31edd5a03..b20ac84700 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.Configuration.Models StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! internal const string - StaticReservedUrls = "~/config/splashes/noNodes.aspx,~/.well-known,"; // must end with a comma! + StaticReservedUrls = "~/.well-known,"; // must end with a comma! /// /// Gets or sets a value for the reserved URLs. diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 831ad8d84d..898798588c 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -1,6 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; +using Umbraco.Cms.Core.Hosting; + namespace Umbraco.Cms.Core.Configuration.Models { /// @@ -16,6 +19,6 @@ namespace Umbraco.Cms.Core.Configuration.Models /// /// Gets a value for the keep alive ping URL. /// - public string KeepAlivePingUrl => "{umbracoApplicationUrl}/api/keepalive/ping"; + public string KeepAlivePingUrl => "~/api/keepalive/ping"; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index 586f1d7d99..6af917078c 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -23,14 +23,7 @@ namespace Umbraco.Cms.Core.DependencyInjection where TNotificationHandler : INotificationHandler where TNotification : INotification { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); - - if (!builder.Services.Contains(descriptor)) - { - builder.Services.Add(descriptor); - } - + builder.Services.AddNotificationHandler(); return builder; } @@ -44,16 +37,39 @@ namespace Umbraco.Cms.Core.DependencyInjection public static IUmbracoBuilder AddNotificationAsyncHandler(this IUmbracoBuilder builder) where TNotificationAsyncHandler : INotificationAsyncHandler where TNotification : INotification + { + builder.Services.AddNotificationAsyncHandler(); + return builder; + } + + internal static IServiceCollection AddNotificationHandler(this IServiceCollection services) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) + { + services.Add(descriptor); + } + + return services; + } + + internal static IServiceCollection AddNotificationAsyncHandler(this IServiceCollection services) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification { // Register the handler as transient. This ensures that anything can be injected into it. var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient); - if (!builder.Services.Contains(descriptor)) + if (!services.Contains(descriptor)) { - builder.Services.Add(descriptor); + services.Add(descriptor); } - return builder; + return services; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 82cf6ffa84..add0ec3fa7 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Editors; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; @@ -31,6 +32,7 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; @@ -219,6 +221,10 @@ namespace Umbraco.Cms.Core.DependencyInjection // which may be replaced by models builder but the default is required to make plain old IPublishedContent // instances. Services.AddSingleton(factory => factory.CreateDefaultPublishedModelFactory()); + + Services + .AddNotificationHandler() + .AddNotificationHandler(); } } } diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index e2cb09c978..db2d63b643 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -76,10 +76,26 @@ namespace Umbraco.Extensions /// The public static Attempt TryConvertTo(this object input) { - var result = TryConvertTo(input, typeof(T)); + Attempt result = TryConvertTo(input, typeof(T)); if (result.Success) + { return Attempt.Succeed((T)result.Result); + } + + if (input == null) + { + if (typeof(T).IsValueType) + { + // fail, cannot convert null to a value type + return Attempt.Fail(); + } + else + { + // sure, null can be any object + return Attempt.Succeed((T)input); + } + } // just try to cast try diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs new file mode 100644 index 0000000000..4e45ea63d8 --- /dev/null +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Extensions +{ + public static class RuntimeStateExtensions + { + /// + /// Returns true if Umbraco is greater than + /// + public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + } +} diff --git a/src/Umbraco.Core/Compose/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs similarity index 98% rename from src/Umbraco.Core/Compose/AuditNotificationsHandler.cs rename to src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs index 1347da05fd..06a7743fee 100644 --- a/src/Umbraco.Core/Compose/AuditNotificationsHandler.cs +++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs @@ -12,7 +12,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Compose +namespace Umbraco.Cms.Core.Handlers { public sealed class AuditNotificationsHandler : INotificationHandler, @@ -61,7 +61,7 @@ namespace Umbraco.Cms.Core.Compose } } - public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Cms.Core.Constants.Security.UnknownUserId, Name = Cms.Core.Constants.Security.UnknownUserName, Email = "" }; + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); diff --git a/src/Umbraco.Core/Compose/PublicAccessHandler.cs b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs similarity index 97% rename from src/Umbraco.Core/Compose/PublicAccessHandler.cs rename to src/Umbraco.Core/Handlers/PublicAccessHandler.cs index a677db25d4..9645e9a88c 100644 --- a/src/Umbraco.Core/Compose/PublicAccessHandler.cs +++ b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs @@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Compose +namespace Umbraco.Cms.Core.Handlers { public sealed class PublicAccessHandler : INotificationHandler, diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index 9f755c2256..a685ab67f1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Persistence.Repositories { diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs index f5388d8f95..035ca6f4ec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Umbraco.Cms.Core.Persistence.Repositories @@ -8,14 +8,30 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// public static class RepositoryCacheKeys { - private static readonly Dictionary Keys = new Dictionary(); + // used to cache keys so we don't keep allocating strings + private static readonly Dictionary s_keys = new Dictionary(); public static string GetKey() { - var type = typeof(T); - return Keys.TryGetValue(type, out var key) ? key : (Keys[type] = "uRepo_" + type.Name + "_"); + Type type = typeof(T); + return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_"); } - public static string GetKey(object id) => GetKey() + id; + public static string GetKey(TId id) + { + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return GetKey() + id; + } + else + { + return GetKey() + id.ToString().ToUpperInvariant(); + } + } } } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs index 67036202dd..fcc1d00103 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMemberCache.cs @@ -1,9 +1,11 @@ -using System.Xml.XPath; +using System.Xml.XPath; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; namespace Umbraco.Cms.Core.PublishedCache { + // TODO: Kill this, why do we want this at all? + // See https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/11487 public interface IPublishedMemberCache : IXPathNavigable { IPublishedContent GetByProviderKey(object key); diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index 39bc94cda1..5de826d222 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Umbraco.Cms.Core.Models.PublishedContent; namespace Umbraco.Cms.Core.Routing { @@ -24,18 +25,28 @@ namespace Umbraco.Cms.Core.Routing Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); /// - /// Updates the request to "not found". + /// Updates the request to use the specified item, or NULL /// /// The request. /// - /// A new based on values from the original - /// This method is invoked when the pipeline decides it cannot render + /// + /// A new based on values from the original + /// and with the re-routed values based on the passed in + /// + /// + /// This method is used for 2 cases: + /// - When the rendering content needs to change due to Public Access rules. + /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. + /// + /// + /// This method is invoked when the pipeline decides it cannot render /// the request, for whatever reason, and wants to force it to be re-routed - /// and rendered as if no document were found (404). - /// This occurs if there is no template found and route hijacking was not matched. + /// and rendered as if no document were found (404). + /// This occurs if there is no template found and route hijacking was not matched. /// In that case it's the same as if there was no content which means even if there was - /// content matched we want to run the request through the last chance finders. + /// content matched we want to run the request through the last chance finders. + /// /// - IPublishedRequestBuilder UpdateRequestToNotFound(IPublishedRequest request); + Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent publishedContent); } } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 86ac97db31..f8697e640a 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -11,7 +11,6 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -33,10 +32,8 @@ namespace Umbraco.Cms.Core.Routing private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IRequestAccessor _requestAccessor; private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IPublicAccessChecker _publicAccessChecker; private readonly IFileService _fileService; private readonly IContentTypeService _contentTypeService; - private readonly IPublicAccessService _publicAccessService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IEventAggregator _eventAggregator; @@ -53,10 +50,8 @@ namespace Umbraco.Cms.Core.Routing IPublishedUrlProvider publishedUrlProvider, IRequestAccessor requestAccessor, IPublishedValueFallback publishedValueFallback, - IPublicAccessChecker publicAccessChecker, IFileService fileService, IContentTypeService contentTypeService, - IPublicAccessService publicAccessService, IUmbracoContextAccessor umbracoContextAccessor, IEventAggregator eventAggregator) { @@ -69,10 +64,8 @@ namespace Umbraco.Cms.Core.Routing _publishedUrlProvider = publishedUrlProvider; _requestAccessor = requestAccessor; _publishedValueFallback = publishedValueFallback; - _publicAccessChecker = publicAccessChecker; _fileService = fileService; _contentTypeService = contentTypeService; - _publicAccessService = publicAccessService; _umbracoContextAccessor = umbracoContextAccessor; _eventAggregator = eventAggregator; } @@ -104,13 +97,11 @@ namespace Umbraco.Cms.Core.Routing { FindDomain(request); - // TODO: This was ported from v8 but how could it possibly have a redirect here? if (request.IsRedirect()) { return request.Build(); } - // TODO: This was ported from v8 but how could it possibly have content here? if (request.HasPublishedContent()) { return request.Build(); @@ -133,54 +124,78 @@ namespace Umbraco.Cms.Core.Routing } /// - public async Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options) + public async Task RouteRequestAsync(IPublishedRequestBuilder builder, RouteRequestOptions options) { // outbound routing performs different/simpler logic if (options.RouteDirection == RouteDirection.Outbound) { - return TryRouteRequest(request); + return TryRouteRequest(builder); } // find domain - FindDomain(request); - - // TODO: This was ported from v8 but how could it possibly have a redirect here? - // if request has been flagged to redirect then return - // whoever called us is in charge of actually redirecting - if (request.IsRedirect()) + if (builder.Domain == null) { - return request.Build(); + FindDomain(builder); + } + + await RouteRequestInternalAsync(builder); + + // complete the PCR and assign the remaining values + return BuildRequest(builder); + } + + private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder) + { + // if request builder was already flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (builder.IsRedirect()) + { + return; } // set the culture - SetVariationContext(request.Culture); + SetVariationContext(builder.Culture); - // find the published content if it's not assigned. This could be manually assigned with a custom route handler, or - // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method + var foundContentByFinders = false; + + // Find the published content if it's not assigned. + // This could be manually assigned with a custom route handler, etc... + // which in turn could call this method // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - // TODO: This might very well change when we look into porting custom routes in netcore like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. - if (!request.HasPublishedContent()) + if (!builder.HasPublishedContent()) { - // find the document & template - FindPublishedContentAndTemplate(request); + _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); + + // run the document finders + foundContentByFinders = FindPublishedContent(builder); } - // handle wildcard domains - HandleWildcardDomains(request); + // if we are not a redirect + if (!builder.IsRedirect()) + { + // handle not-found, redirects, access... + HandlePublishedContent(builder); - // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain - SetVariationContext(request.Culture); + // find a template + FindTemplate(builder, foundContentByFinders); + + // handle umbracoRedirect + FollowExternalRedirect(builder); + + // handle wildcard domains + HandleWildcardDomains(builder); + + // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain + SetVariationContext(builder.Culture); + } // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything // even though the request might be flagged for redirection - we'll redirect _after_ the event - var routingRequest = new RoutingRequestNotification(request); + var routingRequest = new RoutingRequestNotification(builder); await _eventAggregator.PublishAsync(routingRequest); // we don't take care of anything so if the content has changed, it's up to the user // to find out the appropriate template - - // complete the PCR and assign the remaining values - return BuildRequest(request); } /// @@ -193,11 +208,11 @@ namespace Umbraco.Cms.Core.Routing /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values /// but need to finalize it themselves. /// - internal IPublishedRequest BuildRequest(IPublishedRequestBuilder frequest) + internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) { - IPublishedRequest result = frequest.Build(); + IPublishedRequest result = builder.Build(); - if (!frequest.HasPublishedContent()) + if (!builder.HasPublishedContent()) { return result; } @@ -209,23 +224,26 @@ namespace Umbraco.Cms.Core.Routing } /// - public IPublishedRequestBuilder UpdateRequestToNotFound(IPublishedRequest request) + public async Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent publishedContent) { - var builder = new PublishedRequestBuilder(request.Uri, _fileService); - - // clear content + // store the original (if any) IPublishedContent content = request.PublishedContent; - builder.SetPublishedContent(null); - HandlePublishedContent(builder); // will go 404 - FindTemplate(builder, false); + IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); - // if request has been flagged to redirect then return - if (request.IsRedirect()) + // set to the new content (or null if specified) + builder.SetPublishedContent(publishedContent); + + // re-route + await RouteRequestInternalAsync(builder); + + // return if we are redirect + if (builder.IsRedirect()) { - return builder; + return BuildRequest(builder); } + // this will occur if publishedContent is null and the last chance finders also don't assign content if (!builder.HasPublishedContent()) { // means the engine could not find a proper document to handle 404 @@ -233,7 +251,7 @@ namespace Umbraco.Cms.Core.Routing builder.SetPublishedContent(content); } - return builder; + return BuildRequest(builder); } /// @@ -359,44 +377,11 @@ namespace Umbraco.Cms.Core.Routing return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); } - /// - /// Finds the Umbraco document (if any) matching the request, and updates the PublishedRequest accordingly. - /// - private void FindPublishedContentAndTemplate(IPublishedRequestBuilder request) - { - _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); - - // run the document finders - FindPublishedContent(request); - - // if request has been flagged to redirect then return - // whoever called us is in charge of actually redirecting - // -- do not process anything any further -- - if (request.IsRedirect()) - { - return; - } - - var foundContentByFinders = request.HasPublishedContent(); - - // not handling umbracoRedirect here but after LookupDocument2 - // so internal redirect, 404, etc has precedence over redirect - - // handle not-found, redirects, access... - HandlePublishedContent(request); - - // find a template - FindTemplate(request, foundContentByFinders); - - // handle umbracoRedirect - FollowExternalRedirect(request); - } - /// /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. /// /// There is no finder collection. - internal void FindPublishedContent(IPublishedRequestBuilder request) + internal bool FindPublishedContent(IPublishedRequestBuilder request) { const string tracePrefix = "FindPublishedContent: "; @@ -422,6 +407,8 @@ namespace Umbraco.Cms.Core.Routing request.HasDomain() ? request.Domain.ToString() : "NULL", request.Culture ?? "NULL", request.ResponseStatusCode); + + return found; } } @@ -430,7 +417,7 @@ namespace Umbraco.Cms.Core.Routing /// /// The request builder. /// - /// Handles "not found", internal redirects, access validation... + /// Handles "not found", internal redirects ... /// things that must be handled in one place because they can create loops /// private void HandlePublishedContent(IPublishedRequestBuilder request) @@ -469,12 +456,6 @@ namespace Umbraco.Cms.Core.Routing break; } - // ensure access - if (request.PublishedContent != null) - { - EnsurePublishedContentAccess(request); - } - // loop while we don't have page, ie the redirect or access // got us to nowhere and now we need to run the notFoundLookup again // as long as it's not running out of control ie infinite loop of some sort @@ -573,63 +554,6 @@ namespace Umbraco.Cms.Core.Routing return redirect; } - /// - /// Ensures that access to current node is permitted. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - private void EnsurePublishedContentAccess(IPublishedRequestBuilder request) - { - if (request.PublishedContent == null) - { - throw new InvalidOperationException("There is no PublishedContent."); - } - - var path = request.PublishedContent.Path; - - Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); - - if (publicAccessAttempt) - { - _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); - - PublicAccessStatus status = _publicAccessChecker.HasMemberAccessToContent(request.PublishedContent.Id); - switch (status) - { - case PublicAccessStatus.NotLoggedIn: - _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.LoginNodeId); - break; - case PublicAccessStatus.AccessDenied: - _logger.LogDebug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.LockedOut: - _logger.LogDebug("Current member is locked out, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.NotApproved: - _logger.LogDebug("Current member is unapproved, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.AccessAccepted: - _logger.LogDebug("Current member has access"); - break; - } - } - else - { - _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); - } - } - - private void SetPublishedContentAsOtherPage(IPublishedRequestBuilder request, int errorPageId) - { - if (errorPageId != request.PublishedContent.Id) - { - request.SetPublishedContent(_umbracoContextAccessor.UmbracoContext.PublishedSnapshot.Content.GetById(errorPageId)); - } - } - /// /// Finds a template for the current node, if any. /// diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index 503cbb27fd..91313c8278 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -1,34 +1,55 @@ using System; -using System.Linq; -using Umbraco.Extensions; +using System.Text; namespace Umbraco.Cms.Core.Routing { public class WebPath { + public const char PathSeparator = '/'; + public static string Combine(params string[] paths) { - const string separator = "/"; - - if (paths == null) throw new ArgumentNullException(nameof(paths)); - if (!paths.Any()) return string.Empty; - - - - var result = paths[0].TrimEnd(separator); - - if(!(result.StartsWith(separator) || result.StartsWith("~" + separator))) + if (paths == null) { - result = separator + result; + throw new ArgumentNullException(nameof(paths)); } - for (var index = 1; index < paths.Length; index++) + if (paths.Length == 0) { - - result +=separator + paths[index].Trim(separator); + return string.Empty; } - return result; + var sb = new StringBuilder(); + + for (var index = 0; index < paths.Length; index++) + { + var path = paths[index]; + var start = 0; + var count = path.Length; + var isFirst = index == 0; + var isLast = index == paths.Length - 1; + + // don't trim start if it's the first + if (!isFirst && path[0] == PathSeparator) + { + start = 1; + } + + // always trim end + if (path[path.Length - 1] == PathSeparator) + { + count = path.Length - 1; + } + + sb.Append(path, start, count - start); + + if (!isLast) + { + sb.Append(PathSeparator); + } + } + + return sb.ToString(); } } } diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Security/ExternalLogin.cs similarity index 89% rename from src/Umbraco.Core/Models/Identity/ExternalLogin.cs rename to src/Umbraco.Core/Security/ExternalLogin.cs index c599dcab96..48bbfa2091 100644 --- a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs +++ b/src/Umbraco.Core/Security/ExternalLogin.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Models.Identity public string LoginProvider { get; } /// - public string ProviderKey { get; } + public string ProviderKey { get; } /// public string UserData { get; } diff --git a/src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs b/src/Umbraco.Core/Security/ExternalLoginToken.cs similarity index 92% rename from src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs rename to src/Umbraco.Core/Security/ExternalLoginToken.cs index 23f532fdfa..85089ddba6 100644 --- a/src/Umbraco.Core/Models/Identity/ExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/ExternalLoginToken.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// public class ExternalLoginToken : IExternalLoginToken diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Security/IExternalLogin.cs similarity index 91% rename from src/Umbraco.Core/Models/Identity/IExternalLogin.cs rename to src/Umbraco.Core/Security/IExternalLogin.cs index 1cc570c36f..b285bd35eb 100644 --- a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs +++ b/src/Umbraco.Core/Security/IExternalLogin.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// Used to persist external login data for a user diff --git a/src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs b/src/Umbraco.Core/Security/IExternalLoginToken.cs similarity index 91% rename from src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs rename to src/Umbraco.Core/Security/IExternalLoginToken.cs index 7cc3065e35..b3fd4b64b2 100644 --- a/src/Umbraco.Core/Models/Identity/IExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/IExternalLoginToken.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// Used to persist an external login token for a user diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs similarity index 95% rename from src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs rename to src/Umbraco.Core/Security/IIdentityUserLogin.cs index 21d8f842af..67ca739509 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs @@ -1,6 +1,6 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// An external login provider linked to a user diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs b/src/Umbraco.Core/Security/IIdentityUserToken.cs similarity index 94% rename from src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs rename to src/Umbraco.Core/Security/IIdentityUserToken.cs index c3a451f31d..d7d3af6adf 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IIdentityUserToken.cs @@ -1,6 +1,6 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// /// An external login provider token diff --git a/src/Umbraco.Core/Security/IPublicAccessChecker.cs b/src/Umbraco.Core/Security/IPublicAccessChecker.cs index 4eaa184f5a..6ec9eb7ade 100644 --- a/src/Umbraco.Core/Security/IPublicAccessChecker.cs +++ b/src/Umbraco.Core/Security/IPublicAccessChecker.cs @@ -1,7 +1,9 @@ +using System.Threading.Tasks; + namespace Umbraco.Cms.Core.Security { public interface IPublicAccessChecker { - PublicAccessStatus HasMemberAccessToContent(int publishedContentId); + Task HasMemberAccessToContentAsync(int publishedContentId); } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Security/IdentityUserLogin.cs similarity index 96% rename from src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs rename to src/Umbraco.Core/Security/IdentityUserLogin.cs index b719d9cd51..4775b9d3e6 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IdentityUserLogin.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserToken.cs b/src/Umbraco.Core/Security/IdentityUserToken.cs similarity index 97% rename from src/Umbraco.Core/Models/Identity/IdentityUserToken.cs rename to src/Umbraco.Core/Security/IdentityUserToken.cs index 42e79a89dd..4a3c0f21cf 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IdentityUserToken.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { public class IdentityUserToken : EntityBase, IIdentityUserToken { diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs index 8834a4b33f..787631d500 100644 --- a/src/Umbraco.Core/Services/IExternalLoginService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services { diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 41bf9ac349..487a0ad50a 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -1,9 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -64,7 +65,7 @@ namespace Umbraco.Extensions /// /// A callback to retrieve the roles for this member /// - public static bool HasAccess(this IPublicAccessService publicAccessService, string path, string username, Func> rolesCallback) + public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) { if (rolesCallback == null) throw new ArgumentNullException("roles"); if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); @@ -73,13 +74,28 @@ namespace Umbraco.Extensions var entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); if (entry == null) return true; - var roles = rolesCallback(username); + var roles = await rolesCallback(); return HasAccess(entry, username, roles); } private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) { + if (entry is null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + } + + if (roles is null) + { + throw new ArgumentNullException(nameof(roles)); + } + return entry.Rules.Any(x => (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index 106451d32a..ef7255a0d6 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.Cache public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IEntity { - private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const + private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) @@ -33,21 +33,29 @@ namespace Umbraco.Cms.Core.Cache _options = options ?? throw new ArgumentNullException(nameof(options)); } - protected string GetEntityCacheKey(object id) + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; + + protected string GetEntityCacheKey(TId id) { - if (id == null) throw new ArgumentNullException(nameof(id)); - return GetEntityTypeCacheKey() + id; + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; + } + else + { + return EntityTypeCacheKey + id.ToString().ToUpperInvariant(); + } } - protected string GetEntityTypeCacheKey() - { - return $"uRepo_{typeof (TEntity).Name}_"; - } + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; protected virtual void InsertEntity(string cacheKey, TEntity entity) - { - Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); - } + => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); protected virtual void InsertEntities(TId[] ids, TEntity[] entities) { @@ -56,7 +64,7 @@ namespace Umbraco.Cms.Core.Cache // getting all of them, and finding nothing. // if we can cache a zero count, cache an empty array, // for as long as the cache is not cleared (no expiration) - Cache.Insert(GetEntityTypeCacheKey(), () => EmptyEntities); + Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); } else { @@ -85,7 +93,7 @@ namespace Umbraco.Cms.Core.Cache } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -95,7 +103,7 @@ namespace Umbraco.Cms.Core.Cache Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -117,7 +125,7 @@ namespace Umbraco.Cms.Core.Cache } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -127,7 +135,7 @@ namespace Umbraco.Cms.Core.Cache Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -148,7 +156,7 @@ namespace Umbraco.Cms.Core.Cache var cacheKey = GetEntityCacheKey(entity.Id); Cache.Clear(cacheKey); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } } @@ -160,11 +168,16 @@ namespace Umbraco.Cms.Core.Cache // if found in cache then return else fetch and cache if (fromCache != null) + { return fromCache; + } + var entity = performGet(id); if (entity != null && entity.HasIdentity) + { InsertEntity(cacheKey, entity); + } return entity; } @@ -199,7 +212,7 @@ namespace Umbraco.Cms.Core.Cache else { // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey()) + var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) .ToArray(); // no need for null checks, we are not caching nulls if (entities.Length > 0) @@ -222,7 +235,7 @@ namespace Umbraco.Cms.Core.Cache { // if none of them were in the cache // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(GetEntityTypeCacheKey()); + var empty = Cache.GetCacheItem(EntityTypeCacheKey); if (empty != null) return empty; } } @@ -242,7 +255,7 @@ namespace Umbraco.Cms.Core.Cache /// public override void ClearAll() { - Cache.ClearByKey(GetEntityTypeCacheKey()); + Cache.ClearByKey(EntityTypeCacheKey); } } } diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs index 97fcb37bf8..2f9a0ac503 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComposer.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services.Notifications; diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 5ceeed9755..40c250f86e 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -2,14 +2,15 @@ // See LICENSE for more details. using System; +using System.IO; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; @@ -85,21 +86,18 @@ namespace Umbraco.Cms.Infrastructure.HostedServices using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) { - var keepAlivePingUrl = _keepAliveSettings.KeepAlivePingUrl; + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); + return; + } + + // If the config is an absolute path, just use it + string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); + try { - if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}")) - { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return; - } - - keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd(Constants.CharArrays.ForwardSlash)); - } - var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); HttpClient httpClient = _httpClientFactory.CreateClient(); _ = await httpClient.SendAsync(request); diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 772545e2a3..58819f306a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Factories diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 198554be0c..2d47746baa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index 11cb60dc83..4d03031ffd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index a64828ee54..87a112fe08 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -85,7 +85,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.Update(dto); entity.ResetDirtyProperties(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index b953bc5b55..17f0a8101a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -176,8 +176,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement entity.ResetDirtyProperties(); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) @@ -188,8 +188,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); entity.DeleteDate = DateTime.Now; } @@ -205,8 +205,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index a0e651f1dd..63a823c7b5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1176,7 +1176,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) { content[i] = (Content)cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index a262636bfc..2eec8b661b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -5,11 +5,11 @@ using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 3efe35e4c4..ec808cb042 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -511,7 +511,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Core.Models.Media) cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index e97add3f5e..4c107d2a01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -615,7 +615,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Member)cached; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 93700549ac..3fae13d117 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -86,6 +86,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected override IUser PerformGet(int id) { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (id == default || id < -1) + { + return null; + } + var sql = SqlContext.Sql() .Select() .From() diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs similarity index 55% rename from src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs rename to src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs index 67287e9858..60d913f7c5 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityErrorDescriber.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs @@ -5,7 +5,12 @@ namespace Umbraco.Cms.Core.Security /// /// Umbraco back office specific /// - public class BackOfficeIdentityErrorDescriber : IdentityErrorDescriber + public class BackOfficeErrorDescriber : IdentityErrorDescriber + { + // TODO: Override all the methods in order to provide our own translated error messages + } + + public class MembersErrorDescriber : IdentityErrorDescriber { // TODO: Override all the methods in order to provide our own translated error messages } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs index 4e2c1f2704..088afc7149 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs @@ -41,17 +41,17 @@ namespace Umbraco.Cms.Core.Security services => new BackOfficePasswordHasher( new LegacyPasswordSecurity(), services.GetRequiredService())); - services.TryAddScoped, DefaultUserConfirmation>(); + services.TryAddScoped, UmbracoUserConfirmation>(); } + // override to add itself, by default identity only wants a single IdentityErrorDescriber public override IdentityBuilder AddErrorDescriber() { - if (!typeof(BackOfficeIdentityErrorDescriber).IsAssignableFrom(typeof(TDescriber))) + if (!typeof(BackOfficeErrorDescriber).IsAssignableFrom(typeof(TDescriber))) { - throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeIdentityErrorDescriber)}"); + throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(BackOfficeErrorDescriber)}"); } - // Add as itself, by default identity only wants a single IdentityErrorDescriber Services.AddScoped(); return this; } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 843349b9fd..0ca109b741 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; @@ -14,8 +13,6 @@ namespace Umbraco.Cms.Core.Security /// public class BackOfficeIdentityUser : UmbracoIdentityUser { - private string _name; - private string _passwordConfig; private string _culture; private IReadOnlyCollection _groups; private string[] _allowedSections; @@ -55,7 +52,7 @@ namespace Umbraco.Cms.Core.Security user.Id = null; user.HasIdentity = false; user._culture = culture; - user._name = name; + user.Name = name; user.EnableChangeTracking(); return user; } @@ -84,25 +81,6 @@ namespace Umbraco.Cms.Core.Security 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)); - } - - /// - /// Gets or sets the password config - /// - 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) /// @@ -181,23 +159,6 @@ namespace Umbraco.Cms.Core.Security } } - /// - /// Gets a value indicating whether the user is locked out based on the user's lockout end date - /// - public bool IsLockedOut - { - get - { - bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// Gets or sets a value indicating whether the IUser IsApproved - /// - public bool IsApproved { get; set; } - private static string UserIdToString(int userId) => string.Intern(userId.ToString()); } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 99d9d35cea..04a7b12aec 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -8,12 +8,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -46,7 +44,7 @@ namespace Umbraco.Cms.Core.Security IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper, - BackOfficeIdentityErrorDescriber describer, + BackOfficeErrorDescriber describer, AppCaches appCaches) : base(describer) { diff --git a/src/Umbraco.Infrastructure/Security/IMemberManager.cs b/src/Umbraco.Infrastructure/Security/IMemberManager.cs index 081b8cd6c9..1fc035d876 100644 --- a/src/Umbraco.Infrastructure/Security/IMemberManager.cs +++ b/src/Umbraco.Infrastructure/Security/IMemberManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; namespace Umbraco.Cms.Core.Security { @@ -7,6 +8,12 @@ namespace Umbraco.Cms.Core.Security /// public interface IMemberManager : IUmbracoUserManager { + /// + /// Returns the currently logged in member if there is one, else returns null + /// + /// + Task GetCurrentMemberAsync(); + /// /// Checks if the current member is authorized based on the parameters provided. /// @@ -14,15 +21,38 @@ namespace Umbraco.Cms.Core.Security /// Allowed groups. /// Allowed individual members. /// True or false if the currently logged in member is authorized - bool IsMemberAuthorized( + Task IsMemberAuthorizedAsync( IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null); - // TODO: We'll need to add some additional things here that people will be using in their code: + /// + /// Check if a member is logged in + /// + /// + bool IsLoggedIn(); - // bool MemberHasAccess(string path); - // IReadOnlyDictionary MemberHasAccess(IEnumerable paths) - // Possibly some others from the old MembershipHelper + /// + /// Check if the current user has access to a document + /// + /// The full path of the document object to check + /// True if the current user has access or if the current document isn't protected + Task MemberHasAccessAsync(string path); + + /// + /// Checks if the current user has access to the paths + /// + /// + /// + Task> MemberHasAccessAsync(IEnumerable paths); + + /// + /// Check if a document object is protected by the "Protect Pages" functionality in umbraco + /// + /// The full path of the document object to check + /// True if the document object is protected + Task IsProtectedAsync(string path); + + Task> IsProtectedAsync(IEnumerable paths); } } diff --git a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs index 90f7f766f9..6acf52c3cb 100644 --- a/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Umbraco.Cms.Core.Models.Identity; namespace Umbraco.Cms.Core.Security { diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs deleted file mode 100644 index 4e05797a04..0000000000 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Cms.Core.Security -{ - /// - /// Identity options specifically for the Umbraco members identity implementation - /// - public class MemberIdentityOptions : IdentityOptions - { - } -} diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs index b9165e18af..3cfe779d10 100644 --- a/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityUser.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Identity; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; @@ -13,9 +12,7 @@ namespace Umbraco.Cms.Core.Security /// public class MemberIdentityUser : UmbracoIdentityUser { - private string _name; - private string _comments; - private string _passwordConfig; + private string _comments; private IReadOnlyCollection _groups; // Custom comparer for enumerables @@ -53,20 +50,11 @@ namespace Umbraco.Cms.Core.Security user.MemberTypeAlias = memberTypeAlias; user.Id = null; user.HasIdentity = false; - user._name = name; + user.Name = name; user.EnableChangeTracking(); return user; } - /// - /// Gets or sets the member's real name - /// - public string Name - { - get => _name; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - /// /// Gets or sets the member's comments /// @@ -85,15 +73,6 @@ namespace Umbraco.Cms.Core.Security // No change tracking because the persisted value is readonly public Guid Key { get; set; } - /// - /// Gets or sets the password config - /// - public string PasswordConfig - { - get => _passwordConfig; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); - } - /// /// Gets or sets the user groups /// @@ -121,23 +100,6 @@ namespace Umbraco.Cms.Core.Security } } - /// - /// Gets a value indicating whether the member is locked out - /// - public bool IsLockedOut - { - get - { - bool isLocked = LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// Gets or sets a value indicating whether the member is approved - /// - public bool IsApproved { get; set; } - /// /// Gets or sets the alias of the member type /// diff --git a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs index 279735bfa2..a87b3c7f7e 100644 --- a/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberRoleStore.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Security @@ -11,7 +12,7 @@ namespace Umbraco.Cms.Core.Security /// /// A custom user store that uses Umbraco member data /// - public class MemberRoleStore : IRoleStore + public class MemberRoleStore : IRoleStore, IQueryableRoleStore { private readonly IMemberGroupService _memberGroupService; private bool _disposed; @@ -20,7 +21,7 @@ namespace Umbraco.Cms.Core.Security //TODO: How revealing can the error messages be? private readonly IdentityError _intParseError = new IdentityError { Code = "IdentityIdParseError", Description = "Cannot parse ID to int" }; private readonly IdentityError _memberGroupNotFoundError = new IdentityError { Code = "IdentityMemberGroupNotFound", Description = "Member group not found" }; - private const string genericIdentityErrorCode = "IdentityErrorUserStore"; + //private const string genericIdentityErrorCode = "IdentityErrorUserStore"; public MemberRoleStore(IMemberGroupService memberGroupService, IdentityErrorDescriber errorDescriber) { @@ -33,6 +34,8 @@ namespace Umbraco.Cms.Core.Security /// public IdentityErrorDescriber ErrorDescriber { get; set; } + public IQueryable Roles => _memberGroupService.GetAll().Select(MapFromMemberGroup).AsQueryable(); + /// public Task CreateAsync(UmbracoIdentityRole role, CancellationToken cancellationToken = default) { @@ -268,5 +271,6 @@ namespace Umbraco.Cms.Core.Security throw new ObjectDisposedException(GetType().Name); } } + } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 53420ff667..c752e41bb2 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs index 00c4038287..ccf9448604 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { public class UmbracoIdentityRole : IdentityRole, IRememberBeingDirty { diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs index bf553b3d30..f91fac1549 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityUser.cs @@ -6,7 +6,7 @@ using System.ComponentModel; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Identity +namespace Umbraco.Cms.Core.Security { /// @@ -29,6 +29,8 @@ namespace Umbraco.Cms.Core.Models.Identity /// public abstract class UmbracoIdentityUser : IdentityUser, IRememberBeingDirty { + private string _name; + private string _passwordConfig; private string _id; private string _email; private string _userName; @@ -247,6 +249,42 @@ namespace Umbraco.Cms.Core.Models.Identity /// protected BeingDirty BeingDirty { get; } = new BeingDirty(); + /// + /// Gets a value indicating whether the user is locked out based on the user's lockout end date + /// + public bool IsLockedOut + { + get + { + bool isLocked = LockoutEnabled && LockoutEnd.HasValue && LockoutEnd.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// Gets or sets a value indicating whether the IUser IsApproved + /// + public bool IsApproved { get; set; } + + /// + /// Gets or sets the user's real name + /// + public string Name + { + get => _name; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the password config + /// + public string PasswordConfig + { + // TODO: Implement this for members: AB#11550 + get => _passwordConfig; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig)); + } + /// public bool IsDirty() => BeingDirty.IsDirty(); diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs new file mode 100644 index 0000000000..04166e3c0d --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserConfirmation.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Cms.Core.Security +{ + /// + /// Confirms whether a user is approved or not + /// + public class UmbracoUserConfirmation : DefaultUserConfirmation + where TUser: UmbracoIdentityUser + { + public override Task IsConfirmedAsync(UserManager manager, TUser user) + => Task.FromResult(user.IsApproved); + } +} diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 1af7896281..7f77b9d8c6 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Net; namespace Umbraco.Cms.Core.Security diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs index 9e4e438150..259d6e8739 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services.Implement { diff --git a/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs index 6ffe4fd5c5..f2dfad6e22 100644 --- a/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UmbracoIdentityRoleBuilder.cs @@ -1,7 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Tests.Common.Builders.Interfaces; namespace Umbraco.Cms.Tests.Common.Builders diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index 9d00962a9f..f1149bca56 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index d6492f67bb..39afb391aa 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -180,9 +180,14 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest public override void Configure(IApplicationBuilder app) { - app.UseUmbraco(); - app.UseUmbracoBackOffice(); - app.UseUmbracoWebsite(); + app.UseUmbraco() + .WithBackOffice() + .WithWebsite() + .WithEndpoints(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index e56891601c..fc183e0ce4 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs @@ -5,7 +5,7 @@ using System; using System.Linq; using System.Threading; using NUnit.Framework; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Tests.Common.Builders; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 6cdfe5deb9..f55f969837 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice [TestFixture] public class UmbracoBackOfficeServiceCollectionExtensionsTests : UmbracoIntegrationTest { - protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddUmbracoBackOfficeIdentity(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddBackOfficeIdentity(); [Test] public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs index e76716c152..28c61a743c 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Common/MembersServiceCollectionExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.Common [TestFixture] public class MembersServiceCollectionExtensionsTests : UmbracoIntegrationTest { - protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.Services.AddMembersIdentity(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddMembersIdentity(); [Test] public void AddMembersIdentity_ExpectMembersUserStoreResolvable() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs index 2f9c05994f..a3e3da5060 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs @@ -15,13 +15,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing [TestCase("/umbraco", "/config", "/lang", ExpectedResult = "/umbraco/config/lang")] [TestCase("/umbraco/", "/config/", "/lang/", ExpectedResult = "/umbraco/config/lang")] [TestCase("/umbraco/", "config/", "lang/", ExpectedResult = "/umbraco/config/lang")] - [TestCase("umbraco", "config", "lang", ExpectedResult = "/umbraco/config/lang")] - [TestCase("umbraco", ExpectedResult = "/umbraco")] + [TestCase("umbraco", "config", "lang", ExpectedResult = "umbraco/config/lang")] + [TestCase("umbraco", ExpectedResult = "umbraco")] [TestCase("~/umbraco", "config", "lang", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco", "/config", "/lang", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco/", "/config/", "/lang/", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco/", "config/", "lang/", ExpectedResult = "~/umbraco/config/lang")] [TestCase("~/umbraco", ExpectedResult = "~/umbraco")] + [TestCase("https://hello.com/", "/world", ExpectedResult = "https://hello.com/world")] public string Combine(params string[] parts) => WebPath.Combine(parts); [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs index 4266ab22b4..557e87fb0c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs @@ -78,8 +78,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices DisableKeepAliveTask = !enabled, }; - var mockRequestAccessor = new Mock(); - mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); + var mockHostingEnvironment = new Mock(); + mockHostingEnvironment.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); + mockHostingEnvironment.Setup(x => x.ToAbsolute(It.IsAny())) + .Returns((string s) => s.TrimStart('~')); var mockServerRegistrar = new Mock(); mockServerRegistrar.Setup(x => x.CurrentServerRole).Returns(serverRole); @@ -103,7 +105,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); return new KeepAlive( - mockRequestAccessor.Object, + mockHostingEnvironment.Object, mockMainDom.Object, Options.Create(settings), mockLogger.Object, diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index e184263141..c09222bd6f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -26,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security public class MemberManagerTests { private MemberUserStore _fakeMemberStore; - private Mock> _mockIdentityOptions; + private Mock> _mockIdentityOptions; private Mock> _mockPasswordHasher; private Mock _mockMemberService; private Mock _mockServiceProviders; @@ -41,8 +41,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security new Mock().Object, new IdentityErrorDescriber()); - _mockIdentityOptions = new Mock>(); - var idOptions = new MemberIdentityOptions { Lockout = { AllowedForNewUsers = false } }; + _mockIdentityOptions = new Mock>(); + var idOptions = new IdentityOptions { Lockout = { AllowedForNewUsers = false } }; _mockIdentityOptions.Setup(o => o.Value).Returns(idOptions); _mockPasswordHasher = new Mock>(); @@ -70,10 +70,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security _mockPasswordHasher.Object, userValidators, pwdValidators, - new BackOfficeIdentityErrorDescriber(), + new MembersErrorDescriber(), _mockServiceProviders.Object, new Mock>>().Object, - _mockPasswordConfiguration.Object); + _mockPasswordConfiguration.Object, + Mock.Of(), + Mock.Of()); validator.Setup(v => v.ValidateAsync( userManager, diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs index 15f4b7f30d..412de11a9e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberRoleStoreTests.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs index 51929cc445..4b7c58c104 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/StylesheetBuilderTests.cs @@ -16,7 +16,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders public void Is_Built_Correctly() { // Arrange - var testPath = WebPath.Combine("css", "styles.css"); + var testPath = WebPath.PathSeparator + WebPath.Combine("css", "styles.css"); const string testContent = @"body { color:#000; } .bold {font-weight:bold;}"; var builder = new StylesheetBuilder(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs deleted file mode 100644 index 3b12947034..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttributeTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Collections.Generic; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Web.BackOffice.Filters; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Filters -{ - [TestFixture] - public class OnlyLocalRequestsAttributeTests - { - [Test] - public void Does_Not_Set_Result_When_No_Remote_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Is_Null_Ip_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Matches_Local_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.3"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_Local_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "100.1.2.2"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - var typedResult = context.Result as NotFoundResult; - Assert.IsNotNull(typedResult); - } - - [Test] - public void Does_Not_Set_Result_When_Remote_Address_Matches_LoopBack_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "127.0.0.1", localIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - Assert.IsNull(context.Result); - } - - [Test] - public void Returns_Not_Found_When_Remote_Address_Does_Not_Match_LoopBack_Address() - { - // Arrange - ActionExecutingContext context = CreateContext(remoteIpAddress: "100.1.2.3", localIpAddress: "::1"); - var attribute = new OnlyLocalRequestsAttribute(); - - // Act - attribute.OnActionExecuting(context); - - // Assert - var typedResult = context.Result as NotFoundResult; - Assert.IsNotNull(typedResult); - } - - private static ActionExecutingContext CreateContext(string remoteIpAddress = null, string localIpAddress = null) - { - var httpContext = new DefaultHttpContext(); - if (!string.IsNullOrEmpty(remoteIpAddress)) - { - httpContext.Connection.RemoteIpAddress = IPAddress.Parse(remoteIpAddress); - } - - if (!string.IsNullOrEmpty(localIpAddress)) - { - httpContext.Connection.LocalIpAddress = IPAddress.Parse(localIpAddress); - } - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - return new ActionExecutingContext( - actionContext, - new List(), - new Dictionary(), - new Mock().Object); - } - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index c5d24e6bd9..752f4783ad 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -11,8 +11,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security @@ -24,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security private readonly Mock _memberManager = MockMemberManager(); public UserClaimsPrincipalFactory CreateClaimsFactory(MemberManager userMgr) - => new UserClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions())); + => new UserClaimsPrincipalFactory(userMgr, Options.Create(new IdentityOptions())); public MemberSignInManager CreateSut() { @@ -59,14 +61,16 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security => new Mock( Mock.Of(), Mock.Of>(), - Options.Create(new MemberIdentityOptions()), + Options.Create(new IdentityOptions()), Mock.Of>(), Enumerable.Empty>(), Enumerable.Empty>(), - new BackOfficeIdentityErrorDescriber(), + new MembersErrorDescriber(), Mock.Of(), Mock.Of>>(), - Options.Create(new Cms.Core.Configuration.Models.MemberPasswordConfigurationSettings())); + Options.Create(new MemberPasswordConfigurationSettings()), + Mock.Of(), + Mock.Of()); [Test] public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs new file mode 100644 index 0000000000..52c68b551f --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/PublicAccessCheckerTests.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security +{ + [TestFixture] + public class PublicAccessCheckerTests + { + private IHttpContextAccessor GetHttpContextAccessor(IMemberManager memberManager, out HttpContext httpContext) + { + var services = new ServiceCollection(); + services.AddScoped(x => memberManager); + httpContext = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider() + }; + + HttpContext localHttpContext = httpContext; + return Mock.Of(x => x.HttpContext == localHttpContext); + } + + private PublicAccessChecker CreateSut(IMemberManager memberManager, IPublicAccessService publicAccessService, IContentService contentService, out HttpContext httpContext) + { + var publicAccessChecker = new PublicAccessChecker( + GetHttpContextAccessor(memberManager, out httpContext), + publicAccessService, + contentService); + + return publicAccessChecker; + } + + private ClaimsPrincipal GetLoggedInUser() + { + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { + new Claim(ClaimTypes.NameIdentifier, "1234"), + new Claim(ClaimTypes.Name, "test@example.com") + }, "TestAuthentication")); + return user; + } + + private void MockGetRolesAsync(IMemberManager memberManager, IEnumerable roles = null) + { + if (roles == null) + { + roles = new[] + { + "role1", + "role2" + }; + } + Mock.Get(memberManager).Setup(x => x.GetRolesAsync(It.IsAny())) + .Returns(Task.FromResult((IList)new List(roles))); + } + + private void MockGetUserAsync(IMemberManager memberManager, MemberIdentityUser memberIdentityUser) + => Mock.Get(memberManager).Setup(x => x.GetUserAsync(It.IsAny())).Returns(Task.FromResult(memberIdentityUser)); + + private PublicAccessEntry GetPublicAccessEntry(string usernameRuleValue, string roleRuleValue) + => new PublicAccessEntry(Guid.NewGuid(), 123, 987, 987, new List + { + new PublicAccessRule + { + RuleType = Constants.Conventions.PublicAccess.MemberUsernameRuleType, + RuleValue = usernameRuleValue + }, + new PublicAccessRule + { + RuleType = Constants.Conventions.PublicAccess.MemberRoleRuleType, + RuleValue = roleRuleValue + } + }); + + [AutoMoqData] + [Test] + public async Task GivenMemberNotLoggedIn_WhenIdentityIsChecked_ThenNotLoggedInResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = new ClaimsPrincipal(); + MockGetUserAsync(memberManager, new MemberIdentityUser()); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberNotLoggedIn_WhenMemberIsRequested_AndIsNull_ThenNotLoggedInResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, null); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotLoggedIn, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasNoRoles_ThenAccessDeniedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser()); + MockGetRolesAsync(memberManager, Enumerable.Empty()); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessDenied, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberIsLockedOut_ThenLockedOutResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true, LockoutEnd = DateTime.UtcNow.AddDays(10) }); + MockGetRolesAsync(memberManager); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.LockedOut, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberIsNotApproved_ThenNotApprovedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = false }); + MockGetRolesAsync(memberManager); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.NotApproved, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndContentDoesNotExist_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true}); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns((IContent)null); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndGetEntryForContentDoesNotExist_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns((PublicAccessEntry)null); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndEntryRulesDontMatch_ThenAccessDeniedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry(string.Empty, string.Empty)); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessDenied, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndUsernameRuleMatches_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry("MyUsername", string.Empty)); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + + [AutoMoqData] + [Test] + public async Task GivenMemberLoggedIn_WhenMemberHasRoles_AndRoleRuleMatches_ThenAccessAcceptedResult( + IMemberManager memberManager, + IPublicAccessService publicAccessService, + IContentService contentService, + IContent content) + { + PublicAccessChecker sut = CreateSut(memberManager, publicAccessService, contentService, out HttpContext httpContext); + httpContext.User = GetLoggedInUser(); + MockGetUserAsync(memberManager, new MemberIdentityUser { UserName = "MyUsername", IsApproved = true }); + MockGetRolesAsync(memberManager); + Mock.Get(contentService).Setup(x => x.GetById(123)).Returns(content); + Mock.Get(publicAccessService).Setup(x => x.GetEntryForContent(content)).Returns(GetPublicAccessEntry(string.Empty, "role1")); + + var result = await sut.HasMemberAccessToContentAsync(123); + Assert.AreEqual(PublicAccessStatus.AccessAccepted, result); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs index e8e8fec2e0..390f60e5f5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing }); private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request) - => Mock.Of(x => x.Create(It.IsAny(), It.IsAny()) == GetRouteValues(request)); + => Mock.Of(x => x.CreateAsync(It.IsAny(), It.IsAny()) == Task.FromResult(GetRouteValues(request))); private IPublishedRouter GetRouter(IPublishedRequest request) => Mock.Of(x => x.RouteRequestAsync(It.IsAny(), It.IsAny()) == Task.FromResult(request)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs index 54c9fd9438..2bd482b8eb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; @@ -33,11 +34,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing { var builder = new PublishedRequestBuilder(new Uri("https://example.com"), Mock.Of()); builder.SetPublishedContent(Mock.Of()); - request = builder.Build(); + IPublishedRequest builtRequest = request = builder.Build(); publishedRouter = new Mock(); - publishedRouter.Setup(x => x.UpdateRequestToNotFound(It.IsAny())) - .Returns((IPublishedRequest r) => builder) + publishedRouter.Setup(x => x.UpdateRequestAsync(It.IsAny(), null)) + .Returns((IPublishedRequest r, IPublishedContent c) => Task.FromResult(builtRequest)) .Verifiable(); renderingDefaults = new UmbracoRenderingDefaults(); @@ -68,22 +69,22 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing } [Test] - public void Update_Request_To_Not_Found_When_No_Template() + public async Task Update_Request_To_Not_Found_When_No_Template() { UmbracoRouteValuesFactory factory = GetFactory(out Mock publishedRouter, out _, out IPublishedRequest request); - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); + UmbracoRouteValues result = await factory.CreateAsync(new DefaultHttpContext(), request); // The request has content, no template, no hijacked route and no disabled template features so UpdateRequestToNotFound will be called - publishedRouter.Verify(m => m.UpdateRequestToNotFound(It.IsAny()), Times.Once); + publishedRouter.Verify(m => m.UpdateRequestAsync(It.IsAny(), null), Times.Once); } [Test] - public void Adds_Result_To_Route_Value_Dictionary() + public async Task Adds_Result_To_Route_Value_Dictionary() { UmbracoRouteValuesFactory factory = GetFactory(out _, out UmbracoRenderingDefaults renderingDefaults, out IPublishedRequest request); - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); + UmbracoRouteValues result = await factory.CreateAsync(new DefaultHttpContext(), request); Assert.IsNotNull(result); Assert.AreEqual(renderingDefaults.DefaultControllerType, result.ControllerType); diff --git a/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs b/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs deleted file mode 100644 index 8f1e36aa11..0000000000 --- a/src/Umbraco.Tests/Membership/MembersMembershipProviderTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// using System.Collections.Specialized; -// using System.Web.Security; -// using Moq; -// using NUnit.Framework; -// using Umbraco.Core; -// using Umbraco.Core.Cache; -// using Umbraco.Core.Composing; -// using Umbraco.Core.Logging; -// using Umbraco.Core.Models; -// using Umbraco.Core.Services; -// using Umbraco.Core.Sync; -// using Umbraco.Tests.Integration; -// using Umbraco.Tests.TestHelpers; -// using Umbraco.Tests.TestHelpers.Entities; -// using Umbraco.Tests.Testing; -// using Umbraco.Web; -// using Umbraco.Web.Cache; -// using Umbraco.Web.Security.Providers; -// -// namespace Umbraco.Tests.Membership -// { -// [TestFixture] -// [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] -// public class MembersMembershipProviderTests : TestWithDatabaseBase -// { -// private MembersMembershipProvider MembersMembershipProvider { get; set; } -// private IDistributedCacheBinder DistributedCacheBinder { get; set; } -// -// public IMemberService MemberService => Current.Factory.GetInstance(); -// public IMemberTypeService MemberTypeService => Current.Factory.GetInstance(); -// public ILogger Logger => Current.Factory.GetInstance(); -// -// public override void SetUp() -// { -// base.SetUp(); -// -// MembersMembershipProvider = new MembersMembershipProvider(MemberService, MemberTypeService); -// -// MembersMembershipProvider.Initialize("test", new NameValueCollection { { "passwordFormat", MembershipPasswordFormat.Clear.ToString() } }); -// -// DistributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of(), Logger); -// DistributedCacheBinder.BindEvents(true); -// } -// -// [TearDown] -// public void Teardown() -// { -// DistributedCacheBinder?.UnbindEvents(); -// DistributedCacheBinder = null; -// } -// -// protected override void Compose() -// { -// base.Compose(); -// -// // the cache refresher component needs to trigger to refresh caches -// // but then, it requires a lot of plumbing ;( -// // FIXME: and we cannot inject a DistributedCache yet -// // so doing all this mess -// Composition.RegisterUnique(); -// Composition.RegisterUnique(f => Mock.Of()); -// Composition.WithCollectionBuilder() -// .Add(() => Composition.TypeLoader.GetCacheRefreshers()); -// } -// -// protected override AppCaches GetAppCaches() -// { -// // this is what's created core web runtime -// return new AppCaches( -// new DeepCloneAppCache(new ObjectCacheAppCache()), -// NoAppCache.Instance, -// new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); -// } -// -// /// -// /// MembersMembershipProvider.ValidateUser is expected to increase the number of failed attempts and also read that same number. -// /// -// /// -// /// This test requires the caching to be enabled, as it already is correct in the database. -// /// Shows the error described here: https://github.com/umbraco/Umbraco-CMS/issues/9861 -// /// -// [Test] -// public void ValidateUser__must_lock_out_users_after_max_attempts_of_wrong_password() -// { -// // Arrange -// IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); -// ServiceContext.MemberTypeService.Save(memberType); -// var member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "password","test"); -// ServiceContext.MemberService.Save(member); -// -// var wrongPassword = "wrongPassword"; -// var numberOfFailedAttempts = MembersMembershipProvider.MaxInvalidPasswordAttempts+2; -// -// // Act -// var memberBefore = ServiceContext.MemberService.GetById(member.Id); -// for (int i = 0; i < numberOfFailedAttempts; i++) -// { -// MembersMembershipProvider.ValidateUser(member.Username, wrongPassword); -// } -// var memberAfter = ServiceContext.MemberService.GetById(member.Id); -// -// // Assert -// Assert.Multiple(() => -// { -// Assert.AreEqual(0 , memberBefore.FailedPasswordAttempts, "Expected 0 failed password attempts before"); -// Assert.IsFalse(memberBefore.IsLockedOut, "Expected the member NOT to be locked out before"); -// -// Assert.AreEqual(MembersMembershipProvider.MaxInvalidPasswordAttempts, memberAfter.FailedPasswordAttempts, "Expected exactly the max possible failed password attempts after"); -// Assert.IsTrue(memberAfter.IsLockedOut, "Expected the member to be locked out after"); -// }); -// -// } -// } -// } diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 2c8465c1ea..b44683da5a 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -110,10 +110,8 @@ namespace Umbraco.Tests.TestHelpers Mock.Of(), Mock.Of(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), - container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), - container?.GetRequiredService() ?? Current.Factory.GetRequiredService(), umbracoContextAccessor, Mock.Of() ); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index b5cddc058b..98f4312c7a 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -228,7 +228,7 @@ namespace Umbraco.Tests.Testing services.AddUnique(ipResolver); services.AddUnique(); services.AddUnique(TestHelper.ShortStringHelper); - services.AddUnique(); + //services.AddUnique(); var memberService = Mock.Of(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1bce8edcac..32617b4d8b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -157,7 +157,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 9070c55439..3aa3239dbc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -188,6 +188,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "contentApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostSave(null)) }, + { + "publicAccessApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetPublicAccess(0)) + }, { "mediaApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetRootMedia()) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 9518a9b84d..2c8d4bc385 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -53,19 +53,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly ILocalizedTextService _localizedTextService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly UmbracoMapper _umbracoMapper; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IPublicAccessService _publicAccessService; + private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IDomainService _domainService; private readonly IDataTypeService _dataTypeService; - private readonly ILocalizationService _localizationService; - private readonly IMemberService _memberService; + private readonly ILocalizationService _localizationService; private readonly IFileService _fileService; private readonly INotificationService _notificationService; - private readonly ActionCollection _actionCollection; - private readonly IMemberGroupService _memberGroupService; + private readonly ActionCollection _actionCollection; private readonly ISqlContext _sqlContext; private readonly IAuthorizationService _authorizationService; private readonly Lazy> _allLangs; @@ -83,19 +79,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IContentService contentService, IUserService userService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, IContentTypeService contentTypeService, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider, - IPublicAccessService publicAccessService, IDomainService domainService, IDataTypeService dataTypeService, ILocalizationService localizationService, - IMemberService memberService, IFileService fileService, INotificationService notificationService, ActionCollection actionCollection, - IMemberGroupService memberGroupService, ISqlContext sqlContext, IJsonSerializer serializer, IAuthorizationService authorizationService) @@ -106,19 +98,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _localizedTextService = localizedTextService; _userService = userService; _backofficeSecurityAccessor = backofficeSecurityAccessor; - _entityService = entityService; _contentTypeService = contentTypeService; _umbracoMapper = umbracoMapper; _publishedUrlProvider = publishedUrlProvider; - _publicAccessService = publicAccessService; _domainService = domainService; _dataTypeService = dataTypeService; _localizationService = localizationService; - _memberService = memberService; _fileService = fileService; _notificationService = notificationService; _actionCollection = actionCollection; - _memberGroupService = memberGroupService; _sqlContext = sqlContext; _authorizationService = authorizationService; _logger = loggerFactory.CreateLogger(); @@ -2403,142 +2391,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ValidationErrorResult(notificationModel); } - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpGet] - public IActionResult GetPublicAccess(int contentId) - { - var content = _contentService.GetById(contentId); - if (content == null) - { - return NotFound(); - } + - var entry = _publicAccessService.GetEntryForContent(content); - if (entry == null || entry.ProtectedNodeId != content.Id) - { - return Ok(); - } - - var loginPageEntity = _entityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); - var errorPageEntity = _entityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); - - // unwrap the current public access setup for the client - // - this API method is the single point of entry for both "modes" of public access (single user and role based) - var usernames = entry.Rules - .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) - .Select(rule => rule.RuleValue).ToArray(); - - var members = usernames - .Select(username => _memberService.GetByUsername(username)) - .Where(member => member != null) - .Select(_umbracoMapper.Map) - .ToArray(); - - //TODO: change to role manager - var allGroups = _memberGroupService.GetAll().ToArray(); - var groups = entry.Rules - .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Select(rule => allGroups.FirstOrDefault(g => g.Name == rule.RuleValue)) - .Where(memberGroup => memberGroup != null) - .Select(_umbracoMapper.Map) - .ToArray(); - - return Ok(new PublicAccess - { - Members = members, - Groups = groups, - LoginPage = loginPageEntity != null ? _umbracoMapper.Map(loginPageEntity) : null, - ErrorPage = errorPageEntity != null ? _umbracoMapper.Map(errorPageEntity) : null - }); - } - - // set up public access using role based access - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpPost] - public IActionResult PostPublicAccess(int contentId, [FromQuery(Name = "groups[]")] string[] groups, [FromQuery(Name = "usernames[]")] string[] usernames, int loginPageId, int errorPageId) - { - if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) - { - return BadRequest(); - } - - var content = _contentService.GetById(contentId); - var loginPage = _contentService.GetById(loginPageId); - var errorPage = _contentService.GetById(errorPageId); - if (content == null || loginPage == null || errorPage == null) - { - return BadRequest(); - } - - var isGroupBased = groups != null && groups.Any(); - var candidateRuleValues = isGroupBased - ? groups - : usernames; - var newRuleType = isGroupBased - ? Constants.Conventions.PublicAccess.MemberRoleRuleType - : Constants.Conventions.PublicAccess.MemberUsernameRuleType; - - var entry = _publicAccessService.GetEntryForContent(content); - - if (entry == null || entry.ProtectedNodeId != content.Id) - { - entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); - - foreach (var ruleValue in candidateRuleValues) - { - entry.AddRule(ruleValue, newRuleType); - } - } - else - { - entry.LoginNodeId = loginPage.Id; - entry.NoAccessNodeId = errorPage.Id; - - var currentRules = entry.Rules.ToArray(); - var obsoleteRules = currentRules.Where(rule => - rule.RuleType != newRuleType - || candidateRuleValues.Contains(rule.RuleValue) == false - ); - var newRuleValues = candidateRuleValues.Where(group => - currentRules.Any(rule => - rule.RuleType == newRuleType - && rule.RuleValue == group - ) == false - ); - foreach (var rule in obsoleteRules) - { - entry.RemoveRule(rule); - } - foreach (var ruleValue in newRuleValues) - { - entry.AddRule(ruleValue, newRuleType); - } - } - - return _publicAccessService.Save(entry).Success - ? (IActionResult)Ok() - : Problem(); - } - - [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] - [HttpPost] - public IActionResult RemovePublicAccess(int contentId) - { - var content = _contentService.GetById(contentId); - if (content == null) - { - return NotFound(); - } - - var entry = _publicAccessService.GetEntryForContent(content); - if (entry == null) - { - return Ok(); - } - - return _publicAccessService.Delete(entry).Success - ? (IActionResult)Ok() - : Problem(); - } + } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs b/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs deleted file mode 100644 index 650efec70f..0000000000 --- a/src/Umbraco.Web.BackOffice/Controllers/KeepAliveController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.Serialization; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.Attributes; -using Umbraco.Cms.Web.Common.Controllers; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Web.BackOffice.Controllers -{ - [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [IsBackOffice] - public class KeepAliveController : UmbracoApiController - { - [OnlyLocalRequests] - [HttpGet] - public KeepAlivePingResult Ping() - { - return new KeepAlivePingResult - { - Success = true, - Message = "I'm alive!" - }; - } - } - - - public class KeepAlivePingResult - { - [DataMember(Name = "success")] - public bool Success { get; set; } - - [DataMember(Name = "message")] - public string Message { get; set; } - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs new file mode 100644 index 0000000000..15859660f7 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/PublicAccessController.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] + public class PublicAccessController : BackOfficeNotificationsController + { + private readonly IContentService _contentService; + private readonly IPublicAccessService _publicAccessService; + private readonly IEntityService _entityService; + private readonly IMemberService _memberService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMemberRoleManager _memberRoleManager; + + public PublicAccessController( + IPublicAccessService publicAccessService, + IContentService contentService, + IEntityService entityService, + IMemberService memberService, + UmbracoMapper umbracoMapper, + IMemberRoleManager memberRoleManager) + { + _contentService = contentService; + _publicAccessService = publicAccessService; + _entityService = entityService; + _memberService = memberService; + _umbracoMapper = umbracoMapper; + _memberRoleManager = memberRoleManager; + } + + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpGet] + public ActionResult GetPublicAccess(int contentId) + { + IContent content = _contentService.GetById(contentId); + if (content == null) + { + return NotFound(); + } + + PublicAccessEntry entry = _publicAccessService.GetEntryForContent(content); + if (entry == null || entry.ProtectedNodeId != content.Id) + { + return Ok(); + } + + var nodes = _entityService + .GetAll(UmbracoObjectTypes.Document, entry.LoginNodeId, entry.NoAccessNodeId) + .ToDictionary(x => x.Id); + + if (!nodes.TryGetValue(entry.LoginNodeId, out IEntitySlim loginPageEntity)) + { + throw new InvalidOperationException($"Login node with id ${entry.LoginNodeId} was not found"); + } + + if (!nodes.TryGetValue(entry.NoAccessNodeId, out IEntitySlim errorPageEntity)) + { + throw new InvalidOperationException($"Error node with id ${entry.LoginNodeId} was not found"); + } + + // unwrap the current public access setup for the client + // - this API method is the single point of entry for both "modes" of public access (single user and role based) + var usernames = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) + .Select(rule => rule.RuleValue) + .ToArray(); + + MemberDisplay[] members = usernames + .Select(username => _memberService.GetByUsername(username)) + .Where(member => member != null) + .Select(_umbracoMapper.Map) + .ToArray(); + + var allGroups = _memberRoleManager.Roles.ToDictionary(x => x.Name); + MemberGroupDisplay[] groups = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) + .Select(rule => allGroups.TryGetValue(rule.RuleValue, out UmbracoIdentityRole memberRole) ? memberRole : null) + .Where(x => x != null) + .Select(_umbracoMapper.Map) + .ToArray(); + + return new PublicAccess + { + Members = members, + Groups = groups, + LoginPage = loginPageEntity != null ? _umbracoMapper.Map(loginPageEntity) : null, + ErrorPage = errorPageEntity != null ? _umbracoMapper.Map(errorPageEntity) : null + }; + } + + // set up public access using role based access + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpPost] + public IActionResult PostPublicAccess(int contentId, [FromQuery(Name = "groups[]")] string[] groups, [FromQuery(Name = "usernames[]")] string[] usernames, int loginPageId, int errorPageId) + { + if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) + { + return BadRequest(); + } + + var content = _contentService.GetById(contentId); + var loginPage = _contentService.GetById(loginPageId); + var errorPage = _contentService.GetById(errorPageId); + if (content == null || loginPage == null || errorPage == null) + { + return BadRequest(); + } + + var isGroupBased = groups != null && groups.Any(); + var candidateRuleValues = isGroupBased + ? groups + : usernames; + var newRuleType = isGroupBased + ? Constants.Conventions.PublicAccess.MemberRoleRuleType + : Constants.Conventions.PublicAccess.MemberUsernameRuleType; + + var entry = _publicAccessService.GetEntryForContent(content); + + if (entry == null || entry.ProtectedNodeId != content.Id) + { + entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); + + foreach (var ruleValue in candidateRuleValues) + { + entry.AddRule(ruleValue, newRuleType); + } + } + else + { + entry.LoginNodeId = loginPage.Id; + entry.NoAccessNodeId = errorPage.Id; + + var currentRules = entry.Rules.ToArray(); + var obsoleteRules = currentRules.Where(rule => + rule.RuleType != newRuleType + || candidateRuleValues.Contains(rule.RuleValue) == false + ); + var newRuleValues = candidateRuleValues.Where(group => + currentRules.Any(rule => + rule.RuleType == newRuleType + && rule.RuleValue == group + ) == false + ); + foreach (var rule in obsoleteRules) + { + entry.RemoveRule(rule); + } + foreach (var ruleValue in newRuleValues) + { + entry.AddRule(ruleValue, newRuleType); + } + } + + return _publicAccessService.Save(entry).Success + ? Ok() + : Problem(); + } + + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] + [HttpPost] + public IActionResult RemovePublicAccess(int contentId) + { + var content = _contentService.GetById(contentId); + if (content == null) + { + return NotFound(); + } + + var entry = _publicAccessService.GetEntryForContent(content); + if (entry == null) + { + return Ok(); + } + + return _publicAccessService.Delete(entry).Success + ? Ok() + : Problem(); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs similarity index 78% rename from src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs rename to src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index f59f2635e5..d348ccdfc0 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -1,71 +1,105 @@ +using System; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Actions; -using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Web.BackOffice.Authorization; +using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Web.BackOffice.Authorization; + namespace Umbraco.Extensions { - public static class ServiceCollectionExtensions + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions { /// - /// Adds the services required for using Umbraco back office Identity + /// Adds Umbraco back office authentication requirements /// - public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) + public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) { - services.AddDataProtection(); + builder.Services - services.BuildUmbracoBackOfficeIdentity() - .AddDefaultTokenProviders() - .AddUserStore() - .AddUserManager() - .AddSignInManager() - .AddClaimsPrincipalFactory() - .AddErrorDescriber(); + // This just creates a builder, nothing more + .AddAuthentication() - // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance - services.ConfigureOptions(); - services.ConfigureOptions(); + // Add our custom schemes which are cookie handlers + .AddCookie(Constants.Security.BackOfficeAuthenticationType) + .AddCookie(Constants.Security.BackOfficeExternalAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeExternalAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + + // Although we don't natively support this, we add it anyways so that if end-users implement the required logic + // they don't have to worry about manually adding this scheme or modifying the sign in manager + .AddCookie(Constants.Security.BackOfficeTwoFactorAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }); + + builder.Services.ConfigureOptions(); + + builder.Services.AddSingleton(); + + builder.Services.AddUnique(); + builder.Services.AddUnique, PasswordChanger>(); + builder.Services.AddUnique, PasswordChanger>(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + return builder; } - private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services) + /// + /// Adds Umbraco back office authorization policies + /// + public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) { - services.TryAddScoped(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return new BackOfficeIdentityBuilder(services); + builder.AddBackOfficeAuthorizationPoliciesInternal(backOfficeAuthenticationScheme); + + builder.Services.AddSingleton(); + + builder.Services.AddAuthorization(options + => options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy + => policy.Requirements.Add(new FeatureAuthorizeRequirement()))); + + return builder; } /// /// Add authorization handlers and policies /// - public static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) + private static void AddBackOfficeAuthorizationPoliciesInternal(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) { // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for // any auth defining a matching requirement and scheme. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); + builder.Services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); } private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs new file mode 100644 index 0000000000..dfc2423d6d --- /dev/null +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.Common.AspNetCore; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Extensions +{ + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Identity support for Umbraco back office + /// + public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.AddDataProtection(); + + builder.BuildUmbracoBackOfficeIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddUserManager() + .AddSignInManager() + .AddClaimsPrincipalFactory() + .AddErrorDescriber(); + + // Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance + services.ConfigureOptions(); + services.ConfigureOptions(); + + return builder; + } + + private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + services.TryAddScoped(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return new BackOfficeIdentityBuilder(services); + } + + /// + /// Adds support for external login providers in Umbraco + /// + public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) + { + builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services)); + return umbracoBuilder; + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 422b8a87ff..e8eea703a0 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,7 +1,5 @@ -using System; using System.Linq; using Ganss.XSS; -using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -9,8 +7,6 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Models.Identity; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.WebAssets; @@ -23,16 +19,13 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.BackOffice.Services; using Umbraco.Cms.Web.BackOffice.SignalR; using Umbraco.Cms.Web.BackOffice.Trees; -using Umbraco.Cms.Web.Common.Authorization; -using Umbraco.Cms.Web.Common.Security; -using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IWebHostEnvironment; namespace Umbraco.Extensions { /// /// Extension methods for for the Umbraco back office /// - public static class UmbracoBuilderExtensions + public static partial class UmbracoBuilderExtensions { /// /// Adds all required components to run the Umbraco back office @@ -57,86 +50,6 @@ namespace Umbraco.Extensions .AddUnattedInstallCreateUser() .AddExamine(); - /// - /// Adds Umbraco back office authentication requirements - /// - public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder) - { - builder.Services - - // This just creates a builder, nothing more - .AddAuthentication() - - // Add our custom schemes which are cookie handlers - .AddCookie(Constants.Security.BackOfficeAuthenticationType) - .AddCookie(Constants.Security.BackOfficeExternalAuthenticationType, o => - { - o.Cookie.Name = Constants.Security.BackOfficeExternalAuthenticationType; - o.ExpireTimeSpan = TimeSpan.FromMinutes(5); - }) - - // Although we don't natively support this, we add it anyways so that if end-users implement the required logic - // they don't have to worry about manually adding this scheme or modifying the sign in manager - .AddCookie(Constants.Security.BackOfficeTwoFactorAuthenticationType, o => - { - o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; - o.ExpireTimeSpan = TimeSpan.FromMinutes(5); - }); - - builder.Services.ConfigureOptions(); - - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique, PasswordChanger>(); - builder.Services.AddUnique, PasswordChanger>(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - return builder; - } - - /// - /// Adds Identity support for Umbraco back office - /// - public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder) - { - builder.Services.AddUmbracoBackOfficeIdentity(); - - return builder; - } - - /// - /// Adds Identity support for Umbraco members - /// - public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) - { - builder.Services.AddMembersIdentity(); - - return builder; - } - - /// - /// Adds Umbraco back office authorization policies - /// - public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) - { - builder.Services.AddBackOfficeAuthorizationPolicies(backOfficeAuthenticationScheme); - - builder.Services.AddSingleton(); - - builder.Services.AddAuthorization(options - => options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy - => policy.Requirements.Add(new FeatureAuthorizeRequirement()))); - - return builder; - } - /// /// Adds Umbraco preview support /// @@ -147,15 +60,6 @@ namespace Umbraco.Extensions return builder; } - /// - /// Adds support for external login providers in Umbraco - /// - public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) - { - builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services)); - return umbracoBuilder; - } - /// /// Gets the back office tree collection builder /// @@ -164,6 +68,8 @@ namespace Umbraco.Extensions public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) { + builder.Services.AddSingleton(); + builder.Services.ConfigureOptions(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs deleted file mode 100644 index 813d5cd096..0000000000 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Web.BackOffice.Middleware; -using Umbraco.Cms.Web.BackOffice.Routing; -using Umbraco.Cms.Web.BackOffice.Security; - -namespace Umbraco.Extensions -{ - /// - /// extensions for Umbraco - /// - public static class BackOfficeApplicationBuilderExtensions - { - public static IApplicationBuilder UseUmbracoBackOffice(this IApplicationBuilder app) - { - // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - app.UseEndpoints(endpoints => - { - BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); - backOfficeRoutes.CreateRoutes(endpoints); - }); - - app.UseUmbracoRuntimeMinification(); - - app.UseMiddleware(); - - app.UseUmbracoPreview(); - - return app; - } - - public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) - { - app.UseEndpoints(endpoints => - { - PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); - previewRoutes.CreateRoutes(endpoints); - }); - - return app; - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs new file mode 100644 index 0000000000..dc113a99b0 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Web.BackOffice.Middleware; +using Umbraco.Cms.Web.BackOffice.Routing; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Common.Extensions; + +namespace Umbraco.Extensions +{ + /// + /// extensions for Umbraco + /// + public static partial class UmbracoApplicationBuilderExtensions + { + /// + /// Adds all required middleware to run the back office + /// + /// + /// + public static IUmbracoApplicationBuilder WithBackOffice(this IUmbracoApplicationBuilder builder) + { + KeepAliveSettings keepAliveSettings = builder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = builder.ApplicationServices.GetRequiredService(); + builder.AppBuilder.Map( + hostingEnvironment.ToAbsolute(keepAliveSettings.KeepAlivePingUrl), + a => a.UseMiddleware()); + + builder.AppBuilder.UseMiddleware(); + return builder; + } + + public static IUmbracoEndpointBuilder UseBackOfficeEndpoints(this IUmbracoEndpointBuilder app) + { + // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!app.RuntimeState.UmbracoCanBoot()) + { + return app; + } + + BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); + backOfficeRoutes.CreateRoutes(app.EndpointRouteBuilder); + + app.UseUmbracoRuntimeMinificationEndpoints(); + app.UseUmbracoPreviewEndpoints(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs new file mode 100644 index 0000000000..014f81fe8c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.BackOffice.Routing; +using Umbraco.Cms.Web.Common.ApplicationBuilder; + +namespace Umbraco.Extensions +{ + /// + /// extensions for Umbraco + /// + public static partial class UmbracoApplicationBuilderExtensions + { + public static IUmbracoEndpointBuilder UseUmbracoPreviewEndpoints(this IUmbracoEndpointBuilder app) + { + PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); + previewRoutes.CreateRoutes(app.EndpointRouteBuilder); + + return app; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs deleted file mode 100644 index d811287b85..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/OnlyLocalRequestsAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.BackOffice.Filters -{ - public class OnlyLocalRequestsAttribute : ActionFilterAttribute - { - public override void OnActionExecuting(ActionExecutingContext context) - { - if (!context.HttpContext.Request.IsLocal()) - { - context.Result = new NotFoundResult(); - } - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index b5592b08ff..4b2837d9b8 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Mapping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Trees; using Constants = Umbraco.Cms.Core.Constants; @@ -32,6 +33,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping mapper.Define((source, context) => new MemberDisplay(), Map); mapper.Define((source, context) => new MemberBasic(), Map); mapper.Define((source, context) => new MemberGroupDisplay(), Map); + mapper.Define((source, context) => new MemberGroupDisplay(), Map); mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); } @@ -93,7 +95,17 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.Path = $"-1,{source.Id}"; target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); } - + + // Umbraco.Code.MapAll -Icon -Trashed -ParentId -Alias -Key -Udi + private void Map(UmbracoIdentityRole source, MemberGroupDisplay target, MapperContext context) + { + target.Id = source.Id; + //target.Key = source.Key; + target.Name = source.Name; + target.Path = $"-1,{source.Id}"; + //target.Udi = Udi.Create(Constants.UdiEntityType.MemberGroup, source.Key); + } + // Umbraco.Code.MapAll private static void Map(IMember source, ContentPropertyCollectionDto target, MapperContext context) { diff --git a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs index 796443bbf6..b093611282 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddleware.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -9,6 +9,7 @@ using HttpRequestExtensions = Umbraco.Extensions.HttpRequestExtensions; namespace Umbraco.Cms.Web.BackOffice.Middleware { + /// /// Used to handle errors registered by external login providers /// diff --git a/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs b/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs new file mode 100644 index 0000000000..d08e1d7d1f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Middleware/ConfigureGlobalOptionsForKeepAliveMiddlware.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Web.BackOffice.Middleware +{ + /// + /// Ensures the Keep Alive middleware is part of + /// + public sealed class ConfigureGlobalOptionsForKeepAliveMiddlware : IPostConfigureOptions + { + private readonly IOptions _keepAliveSettings; + + public ConfigureGlobalOptionsForKeepAliveMiddlware(IOptions keepAliveSettings) => _keepAliveSettings = keepAliveSettings; + + /// + /// Append the keep alive ping url to the reserved URLs + /// + /// + /// + public void PostConfigure(string name, GlobalSettings options) => options.ReservedUrls += _keepAliveSettings.Value.KeepAlivePingUrl; + } +} diff --git a/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs new file mode 100644 index 0000000000..733b1699aa --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Middleware/KeepAliveMiddleware.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Cms.Web.BackOffice.Middleware +{ + + /// + /// Used for the Umbraco keep alive service. This is terminating middleware. + /// + public class KeepAliveMiddleware : IMiddleware + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (HttpMethods.IsGet(context.Request.Method) || HttpMethods.IsHead(context.Request.Method)) + { + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync("I'm alive"); + + } + else + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs index 842e6c7289..8ffad24d54 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeIdentityOptions.cs @@ -1,10 +1,9 @@ using System; using System.Security.Claims; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; +using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Security @@ -12,7 +11,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// /// Used to configure for the Umbraco Back office /// - public class ConfigureBackOfficeIdentityOptions : IConfigureOptions + public sealed class ConfigureBackOfficeIdentityOptions : IConfigureOptions { private readonly UserPasswordConfigurationSettings _userPasswordConfiguration; @@ -23,26 +22,26 @@ namespace Umbraco.Cms.Web.BackOffice.Security public void Configure(BackOfficeIdentityOptions options) { + options.SignIn.RequireConfirmedAccount = true; // uses our custom IUserConfirmation + options.SignIn.RequireConfirmedEmail = false; // not implemented + options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented + options.User.RequireUniqueEmail = true; + options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; options.ClaimsIdentity.SecurityStampClaimType = Constants.Security.SecurityStampClaimType; + options.Lockout.AllowedForNewUsers = true; + // TODO: Implement this options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); - ConfigurePasswordOptions(_userPasswordConfiguration, options.Password); + options.Password.ConfigurePasswordOptions(_userPasswordConfiguration); options.Lockout.MaxFailedAccessAttempts = _userPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; } - public static void ConfigurePasswordOptions(IPasswordConfiguration input, PasswordOptions output) - { - output.RequiredLength = input.RequiredLength; - output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit; - output.RequireDigit = input.RequireDigit; - output.RequireLowercase = input.RequireLowercase; - output.RequireUppercase = input.RequireUppercase; - } + } } diff --git a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs index d1f90d7bcf..95be44f066 100644 --- a/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/IPasswordChanger.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Web.Common.Security diff --git a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs index 83f68c8754..18e3a4db48 100644 --- a/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs +++ b/src/Umbraco.Web.BackOffice/Security/PasswordChanger.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs new file mode 100644 index 0000000000..34abdf70bd --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + public interface IUmbracoApplicationBuilder + { + IRuntimeState RuntimeState { get; } + IServiceProvider ApplicationServices { get; } + IApplicationBuilder AppBuilder { get; } + + /// + /// Final call during app building to configure endpoints + /// + /// + void WithEndpoints(Action configureUmbraco); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs new file mode 100644 index 0000000000..4db74dea75 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + + /// + /// A builder to allow encapsulating the enabled routing features in Umbraco + /// + public interface IUmbracoEndpointBuilder + { + IRuntimeState RuntimeState { get; } + IServiceProvider ApplicationServices { get; } + IEndpointRouteBuilder EndpointRouteBuilder { get; } + IApplicationBuilder AppBuilder { get; } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs new file mode 100644 index 0000000000..d1cf866da1 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoPipelineFilter.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. + /// + /// + /// Mainly used for package developers. + /// + public interface IUmbracoPipelineFilter + { + /// + /// The name of the filter + /// + /// + /// This can be used by developers to see what is registered and if anything should be re-ordered, removed, etc... + /// + string Name { get; } + + /// + /// Executes before Umbraco middlewares are registered + /// + /// + void OnPrePipeline(IApplicationBuilder app); + + /// + /// Executes after core Umbraco middlewares are registered and before any Endpoints are declared + /// + /// + void OnPostPipeline(IApplicationBuilder app); + + /// + /// Executes after just before any Umbraco endpoints are declared. + /// + /// + void OnEndpoints(IApplicationBuilder app); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs new file mode 100644 index 0000000000..2bef61bbab --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// A builder to allow encapsulating the enabled endpoints in Umbraco + /// + internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder + { + public UmbracoApplicationBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder) + { + ApplicationServices = services; + RuntimeState = runtimeState; + AppBuilder = appBuilder; + } + + public IServiceProvider ApplicationServices { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } + + public void WithEndpoints(Action configureUmbraco) + => AppBuilder.UseEndpoints(endpoints => + { + var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance( + ApplicationServices, + new object[] { AppBuilder, endpoints }); + configureUmbraco(umbAppBuilder); + }); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs new file mode 100644 index 0000000000..56d856a22a --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// A builder to allow encapsulating the enabled endpoints in Umbraco + /// + internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilder + { + public UmbracoEndpointBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) + { + ApplicationServices = services; + EndpointRouteBuilder = endpointRouteBuilder; + RuntimeState = runtimeState; + AppBuilder = appBuilder; + } + + public IServiceProvider ApplicationServices { get; } + public IEndpointRouteBuilder EndpointRouteBuilder { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs new file mode 100644 index 0000000000..625cfc41d8 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineFilter.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Used to modify the pipeline before and after Umbraco registers it's core middlewares. + /// + /// + /// Mainly used for package developers. + /// + public class UmbracoPipelineFilter : IUmbracoPipelineFilter + { + public UmbracoPipelineFilter(string name) : this(name, null, null, null) { } + + public UmbracoPipelineFilter( + string name, + Action prePipeline, + Action postPipeline, + Action endpointCallback) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + PrePipeline = prePipeline; + PostPipeline = postPipeline; + Endpoints = endpointCallback; + } + + public Action PrePipeline { get; set; } + public Action PostPipeline { get; set; } + public Action Endpoints { get; set; } + public string Name { get; } + + public void OnPrePipeline(IApplicationBuilder app) => PrePipeline?.Invoke(app); + public void OnPostPipeline(IApplicationBuilder app) => PostPipeline?.Invoke(app); + public void OnEndpoints(IApplicationBuilder app) => Endpoints?.Invoke(app); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs new file mode 100644 index 0000000000..8cf2b4144a --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoPipelineOptions.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// Options to allow modifying the pipeline before and after Umbraco registers it's core middlewares. + /// + public class UmbracoPipelineOptions + { + /// + /// Returns a mutable list of all registered startup filters + /// + public IList PipelineFilters { get; } = new List(); + + /// + /// Adds a filter to the list + /// + /// + public void AddFilter(IUmbracoPipelineFilter filter) => PipelineFilters.Add(filter); + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 12896b7998..c6bedf7e6e 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; @@ -113,6 +114,6 @@ namespace Umbraco.Cms.Web.Common.Controllers { return new PublishedContentNotFoundResult(UmbracoContext); } - } + } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index b5530f9377..0000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Web.Caching; -using SixLabors.ImageSharp.Web.Commands; -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; - -namespace Umbraco.Extensions -{ - public static class ServiceCollectionExtensions - { - /// - /// Adds Image Sharp with Umbraco settings - /// - public static IServiceCollection AddUmbracoImageSharp(this IServiceCollection services, IConfiguration configuration) - { - var imagingSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) - .Get() ?? new ImagingSettings(); - - services.AddImageSharp(options => - { - options.Configuration = SixLabors.ImageSharp.Configuration.Default; - options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CachedNameLength = imagingSettings.Cache.CachedNameLength; - options.OnParseCommandsAsync = context => - { - RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Width, imagingSettings.Resize.MaxWidth); - RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Height, imagingSettings.Resize.MaxHeight); - - return Task.CompletedTask; - }; - options.OnBeforeSaveAsync = _ => Task.CompletedTask; - options.OnProcessedAsync = _ => Task.CompletedTask; - options.OnPrepareResponseAsync = _ => Task.CompletedTask; - }) - .SetRequestParser() - .SetMemoryAllocator(provider => ArrayPoolMemoryAllocator.CreateWithMinimalPooling()) - .Configure(options => - { - options.CacheFolder = imagingSettings.Cache.CacheFolder; - }) - .SetCache() - .SetCacheHash() - .AddProvider() - .AddProcessor() - .AddProcessor() - .AddProcessor(); - - return services; - } - - /// - /// Adds the services required for using Members Identity - /// - - public static void AddMembersIdentity(this IServiceCollection services) - { - services.AddIdentity() - .AddDefaultTokenProviders() - .AddMemberManager() - .AddSignInManager() - .AddUserStore() - .AddRoleStore(); - - services.ConfigureApplicationCookie(x => - { - // TODO: We may want/need to configure these further - - x.LoginPath = null; - x.AccessDeniedPath = null; - x.LogoutPath = null; - }); - } - - private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) - { - if (commands.TryGetValue(parameter, out var command)) - { - if (int.TryParse(command, out var i)) - { - if (i > maxValue) - { - commands.Remove(parameter); - } - } - } - } - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs new file mode 100644 index 0000000000..fa5adf7aeb --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// A registered to automatically capture application services + /// + internal class UmbracoApplicationServicesCapture : IStartupFilter + { + /// + public Action Configure(Action next) => + app => + { + StaticServiceProvider.Instance = app.ApplicationServices; + next(app); + }; + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs new file mode 100644 index 0000000000..e2b6bba733 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Web.Caching; +using SixLabors.ImageSharp.Web.Commands; +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.DependencyInjection; + +namespace Umbraco.Extensions +{ + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Image Sharp with Umbraco settings + /// + public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) + { + IConfiguration configuration = builder.Config; + IServiceCollection services = builder.Services; + + ImagingSettings imagingSettings = configuration.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) + .Get() ?? new ImagingSettings(); + + services.AddImageSharp(options => + { + options.Configuration = SixLabors.ImageSharp.Configuration.Default; + options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; + options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; + options.CachedNameLength = imagingSettings.Cache.CachedNameLength; + options.OnParseCommandsAsync = context => + { + RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Width, imagingSettings.Resize.MaxWidth); + RemoveIntParamenterIfValueGreatherThen(context.Commands, ResizeWebProcessor.Height, imagingSettings.Resize.MaxHeight); + + return Task.CompletedTask; + }; + options.OnBeforeSaveAsync = _ => Task.CompletedTask; + options.OnProcessedAsync = _ => Task.CompletedTask; + options.OnPrepareResponseAsync = _ => Task.CompletedTask; + }) + .SetRequestParser() + .SetMemoryAllocator(provider => ArrayPoolMemoryAllocator.CreateWithMinimalPooling()) + .Configure(options => + { + options.CacheFolder = imagingSettings.Cache.CacheFolder; + }) + .SetCache() + .SetCacheHash() + .AddProvider() + .AddProcessor() + .AddProcessor() + .AddProcessor(); + + return services; + } + + private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) + { + if (commands.TryGetValue(parameter, out var command)) + { + if (int.TryParse(command, out var i)) + { + if (i > maxValue) + { + commands.Remove(parameter); + } + } + } + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs new file mode 100644 index 0000000000..7ac28b04c8 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Extensions +{ + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds Identity support for Umbraco members + /// + public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + // check if this has already been added, we cannot add twice but both front-end and back end + // depend on this so it's possible it can be called twice. + var distCacheBinder = new UniqueServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped); + if (builder.Services.Contains(distCacheBinder)) + { + return builder; + } + + // TODO: We may need to use services.AddIdentityCore instead if this is doing too much + + services.AddIdentity() + .AddDefaultTokenProviders() + .AddUserStore() + .AddRoleStore() + .AddRoleManager() + .AddMemberManager() + .AddSignInManager() + .AddErrorDescriber() + .AddUserConfirmation>(); + + services.ConfigureOptions(); + + services.ConfigureApplicationCookie(x => + { + // TODO: We may want/need to configure these further + + x.LoginPath = null; + x.AccessDeniedPath = null; + x.LogoutPath = null; + }); + + return builder; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 106fce4b59..5f2326fd5d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -54,7 +54,6 @@ using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Cms.Web.Common.Mvc; using Umbraco.Cms.Web.Common.Profiler; -using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Common.RuntimeMinification; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; @@ -68,7 +67,7 @@ namespace Umbraco.Extensions /// /// Extension methods for for the common Umbraco functionality /// - public static class UmbracoBuilderExtensions + public static partial class UmbracoBuilderExtensions { /// /// Creates an and registers basic Umbraco services @@ -113,7 +112,7 @@ namespace Umbraco.Extensions // adds the umbraco startup filter which will call UseUmbraco early on before // other start filters are applied (depending on the ordering of IStartupFilters in DI). - services.AddTransient(); + services.AddTransient(); return new UmbracoBuilder(services, config, typeLoader, loggerFactory); } @@ -267,7 +266,7 @@ namespace Umbraco.Extensions builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - builder.Services.AddUmbracoImageSharp(builder.Config); + builder.AddUmbracoImageSharp(); // AspNetCore specific services builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs deleted file mode 100644 index f8ca73e283..0000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Options; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - /// - /// A registered early in DI so that it executes before any user IStartupFilters - /// to ensure that all Umbraco service and requirements are started correctly and in order. - /// - public sealed class UmbracoStartupFilter : IStartupFilter - { - private readonly IOptions _options; - public UmbracoStartupFilter(IOptions options) => _options = options; - - /// - public Action Configure(Action next) => - app => - { - StaticServiceProvider.Instance = app.ApplicationServices; - _options.Value.PreUmbracoPipeline(app); - - app.UseUmbraco(); - next(app); - }; - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs deleted file mode 100644 index 46ed09006b..0000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoStartupFilterOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - public class UmbracoStartupFilterOptions - { - /// - /// Represents the pipeline that is executed before umbraco. By default this pipeline only adds UseDeveloperExceptionPage when the environments is Development. - /// - public Action PreUmbracoPipeline { get; set; } = app => - { - IWebHostEnvironment env = app.ApplicationServices.GetRequiredService(); - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - }; - } -} diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 1036a1f630..1195f7dbac 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -5,14 +5,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; using SixLabors.ImageSharp.Web.DependencyInjection; -using Smidge; -using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; @@ -26,8 +25,12 @@ namespace Umbraco.Extensions /// /// Configures and use services required for using Umbraco /// - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) + public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app) { + IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); + + app.RunPrePipeline(startupOptions.Value); + // TODO: Should we do some checks like this to verify that the corresponding "Add" methods have been called for the // corresponding "Use" methods? // https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Mvc/Mvc.Core/src/Builder/MvcApplicationBuilderExtensions.cs#L132 @@ -66,22 +69,46 @@ namespace Umbraco.Extensions // Must be called after UseRouting and before UseEndpoints app.UseSession(); - // Must come after the above! - app.UseUmbracoInstaller(); + // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, + // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder - return app; + app.RunPostPipeline(startupOptions.Value); + app.RunPreEndpointsPipeline(startupOptions.Value); + + return ActivatorUtilities.CreateInstance( + app.ApplicationServices, + new object[] { app }); + } + + private static void RunPrePipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnPrePipeline(app); + } + } + + private static void RunPostPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnPostPipeline(app); + } + } + + private static void RunPreEndpointsPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) + { + foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + { + filter.OnEndpoints(app); + } } /// /// Returns true if Umbraco is greater than /// public static bool UmbracoCanBoot(this IApplicationBuilder app) - { - var state = app.ApplicationServices.GetRequiredService(); - - // can't continue if boot failed - return state.Level > RuntimeLevel.BootFailed; - } + => app.ApplicationServices.GetRequiredService().UmbracoCanBoot(); /// /// Enables core Umbraco functionality @@ -151,27 +178,6 @@ namespace Umbraco.Extensions return app; } - /// - /// Enables runtime minification for Umbraco - /// - public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - app.UseSmidge(); - app.UseSmidgeNuglify(); - - return app; - } - public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index e4c3033e48..77b9f6c8dd 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; namespace Umbraco.Extensions @@ -19,11 +20,22 @@ namespace Umbraco.Extensions where TUserManager : UserManager, TInterface { identityBuilder.AddUserManager(); - identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager)); + // use a UniqueServiceDescriptor so we can check if it's already been added + var memberManagerDescriptor = new UniqueServiceDescriptor(typeof(TInterface), typeof(TUserManager), ServiceLifetime.Scoped); + identityBuilder.Services.Add(memberManagerDescriptor); identityBuilder.Services.AddScoped(typeof(UserManager), factory => factory.GetRequiredService()); return identityBuilder; } + public static IdentityBuilder AddRoleManager(this IdentityBuilder identityBuilder) + where TRoleManager : RoleManager, TInterface + { + identityBuilder.AddRoleManager(); + identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TRoleManager)); + identityBuilder.Services.AddScoped(typeof(RoleManager), factory => factory.GetRequiredService()); + return identityBuilder; + } + /// /// Adds a implementation for /// diff --git a/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs new file mode 100644 index 0000000000..abe0a99730 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/PasswordConfigurationExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Configuration; + +namespace Umbraco.Extensions +{ + public static class PasswordConfigurationExtensions + { + public static void ConfigurePasswordOptions(this PasswordOptions output, IPasswordConfiguration input) + { + output.RequiredLength = input.RequiredLength; + output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit; + output.RequireDigit = input.RequireDigit; + output.RequireLowercase = input.RequireLowercase; + output.RequireUppercase = input.RequireUppercase; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs similarity index 98% rename from src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs rename to src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index 75a177f25a..7fe337a9f9 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions { - public static class UmbracoCoreServiceCollectionExtensions + public static partial class ServiceCollectionExtensions { /// /// Create and configure the logger diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs similarity index 50% rename from src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs rename to src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs index 40c5c63642..3859814775 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Installer.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Install; namespace Umbraco.Extensions @@ -7,23 +8,20 @@ namespace Umbraco.Extensions /// /// extensions for Umbraco installer /// - public static class UmbracoInstallApplicationBuilderExtensions + public static partial class UmbracoApplicationBuilderExtensions { /// /// Enables the Umbraco installer /// - public static IApplicationBuilder UseUmbracoInstaller(this IApplicationBuilder app) + public static IUmbracoEndpointBuilder UseInstallerEndpoints(this IUmbracoEndpointBuilder app) { - if (!app.UmbracoCanBoot()) + if (!app.RuntimeState.UmbracoCanBoot()) { return app; } - app.UseEndpoints(endpoints => - { - InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); - installerRoutes.CreateRoutes(endpoints); - }); + InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); + installerRoutes.CreateRoutes(app.EndpointRouteBuilder); return app; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs new file mode 100644 index 0000000000..0d8c7df72b --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -0,0 +1,32 @@ +using System; +using Smidge; +using Smidge.Nuglify; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Extensions +{ + public static partial class UmbracoApplicationBuilderExtensions + { + /// + /// Enables runtime minification for Umbraco + /// + public static IUmbracoEndpointBuilder UseUmbracoRuntimeMinificationEndpoints(this IUmbracoEndpointBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (!app.RuntimeState.UmbracoCanBoot()) + { + return app; + } + + app.AppBuilder.UseSmidge(); + app.AppBuilder.UseSmidgeNuglify(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 5ad064cd37..b0a640b230 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -11,7 +12,7 @@ namespace Umbraco.Cms.Web.Common.Filters /// /// Ensures authorization is successful for a front-end member /// - public class UmbracoMemberAuthorizeFilter : IAuthorizationFilter + public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter { public UmbracoMemberAuthorizeFilter() { @@ -39,18 +40,18 @@ namespace Umbraco.Cms.Web.Common.Filters /// public string AllowMembers { get; private set; } - public void OnAuthorization(AuthorizationFilterContext context) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { IMemberManager memberManager = context.HttpContext.RequestServices.GetRequiredService(); - if (!IsAuthorized(memberManager)) + if (!await IsAuthorizedAsync(memberManager)) { context.HttpContext.SetReasonPhrase("Resource restricted: either member is not logged on or is not of a permitted type or group."); context.Result = new ForbidResult(); } } - private bool IsAuthorized(IMemberManager memberManager) + private async Task IsAuthorizedAsync(IMemberManager memberManager) { if (AllowMembers.IsNullOrWhiteSpace()) { @@ -76,7 +77,7 @@ namespace Umbraco.Cms.Web.Common.Filters } } - return memberManager.IsMemberAuthorized(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); + return await memberManager.IsMemberAuthorizedAsync(AllowType.Split(Core.Constants.CharArrays.Comma), AllowGroup.Split(Core.Constants.CharArrays.Comma), members); } } } diff --git a/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs deleted file mode 100644 index 0ac3125d87..0000000000 --- a/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Cms.Core.Security; - -namespace Umbraco.Cms.Web.Common.Routing -{ - public class PublicAccessChecker : IPublicAccessChecker - { - //TODO implement - public PublicAccessStatus HasMemberAccessToContent(int publishedContentId) => PublicAccessStatus.AccessAccepted; - } -} diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 459ed57138..6356979409 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Web.Common.Security IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeIdentityErrorDescriber errors, + BackOfficeErrorDescriber errors, IServiceProvider services, IHttpContextAccessor httpContextAccessor, ILogger> logger, diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs new file mode 100644 index 0000000000..3abe5f0428 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberIdentityOptions.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + public sealed class ConfigureMemberIdentityOptions : IConfigureOptions + { + private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; + + public ConfigureMemberIdentityOptions(IOptions memberPasswordConfiguration) + { + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + } + + public void Configure(IdentityOptions options) + { + options.SignIn.RequireConfirmedAccount = true; // uses our custom IUserConfirmation + options.SignIn.RequireConfirmedEmail = false; // not implemented + options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented + + options.User.RequireUniqueEmail = true; + + options.Lockout.AllowedForNewUsers = true; + // TODO: Implement this + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + + options.Password.ConfigurePasswordOptions(_memberPasswordConfiguration); + + options.Lockout.MaxFailedAccessAttempts = _memberPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + } + } +} diff --git a/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs new file mode 100644 index 0000000000..e10bc118be --- /dev/null +++ b/src/Umbraco.Web.Common/Security/IMemberRoleManager.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + public interface IMemberRoleManager + { + IEnumerable Roles { get; } + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 24314a99ec..ce09f02b63 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -1,35 +1,233 @@ using System; +using System.Linq; using System.Collections.Generic; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Extensions; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; - +using Umbraco.Cms.Core.Services; +using System.Threading.Tasks; namespace Umbraco.Cms.Web.Common.Security { public class MemberManager : UmbracoUserManager, IMemberManager { + private readonly IPublicAccessService _publicAccessService; + private readonly IHttpContextAccessor _httpContextAccessor; + private MemberIdentityUser _currentMember; + public MemberManager( IIpResolver ipResolver, IUserStore store, - IOptions optionsAccessor, + IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - BackOfficeIdentityErrorDescriber errors, + IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, - IOptions passwordConfiguration) + IOptions passwordConfiguration, + IPublicAccessService publicAccessService, + IHttpContextAccessor httpContextAccessor) : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration) { + _publicAccessService = publicAccessService; + _httpContextAccessor = httpContextAccessor; } - public bool IsMemberAuthorized(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) - => true; // TODO: Implement! + /// + public async Task IsMemberAuthorizedAsync(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) + { + if (allowTypes == null) + { + allowTypes = Enumerable.Empty(); + } + + if (allowGroups == null) + { + allowGroups = Enumerable.Empty(); + } + + if (allowMembers == null) + { + allowMembers = Enumerable.Empty(); + } + + // Allow by default + var allowAction = true; + + if (IsLoggedIn() == false) + { + // If not logged on, not allowed + allowAction = false; + } + else + { + string username; + + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + + // If a member could not be resolved from the provider, we are clearly not authorized and can break right here + if (currentMember == null) + { + return false; + } + + int memberId = int.Parse(currentMember.Id); + username = currentMember.UserName; + + // If types defined, check member is of one of those types + IList allowTypesList = allowTypes as IList ?? allowTypes.ToList(); + if (allowTypesList.Any(allowType => allowType != string.Empty)) + { + // Allow only if member's type is in list + allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(currentMember.MemberTypeAlias.ToLowerInvariant()); + } + + // If specific members defined, check member is of one of those + if (allowAction && allowMembers.Any()) + { + // Allow only if member's Id is in the list + allowAction = allowMembers.Contains(memberId); + } + + // If groups defined, check member is of one of those groups + IList allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); + if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) + { + // Allow only if member is assigned to a group in the list + IList groups = await GetRolesAsync(currentMember); + allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); + } + } + + return allowAction; + } + + /// + public bool IsLoggedIn() + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated; + } + + /// + public async Task MemberHasAccessAsync(string path) + { + if (await IsProtectedAsync(path)) + { + return await HasAccessAsync(path); + } + return true; + } + + /// + public async Task> MemberHasAccessAsync(IEnumerable paths) + { + IReadOnlyDictionary protectedPaths = await IsProtectedAsync(paths); + + IEnumerable pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); + IReadOnlyDictionary pathsWithAccess = await HasAccessAsync(pathsWithProtection); + + var result = new Dictionary(); + foreach (var path in paths) + { + pathsWithAccess.TryGetValue(path, out var hasAccess); + // if it's not found it's false anyways + result[path] = !pathsWithProtection.Contains(path) || hasAccess; + } + return result; + } + + /// + /// + /// this is a cached call + /// + public Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success); + + /// + public Task> IsProtectedAsync(IEnumerable paths) + { + var result = new Dictionary(); + foreach (var path in paths) + { + //this is a cached call + result[path] = _publicAccessService.IsProtected(path); + } + return Task.FromResult((IReadOnlyDictionary)result); + } + + /// + public async Task GetCurrentMemberAsync() + { + if (_currentMember == null) + { + if (!IsLoggedIn()) + { + return null; + } + _currentMember = await GetUserAsync(_httpContextAccessor.HttpContext.User); + } + return _currentMember; + } + + /// + /// This will check if the member has access to this path + /// + /// + /// + /// + private async Task HasAccessAsync(string path) + { + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + { + return false; + } + + return await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await GetRolesAsync(currentMember)); + } + + private async Task> HasAccessAsync(IEnumerable paths) + { + var result = new Dictionary(); + MemberIdentityUser currentMember = await GetCurrentMemberAsync(); + + if (currentMember == null || !currentMember.IsApproved || currentMember.IsLockedOut) + { + return result; + } + + // ensure we only lookup user roles once + IList userRoles = null; + async Task> getUserRolesAsync() + { + if (userRoles != null) + { + return userRoles; + } + + userRoles = await GetRolesAsync(currentMember); + return userRoles; + } + + foreach (var path in paths) + { + result[path] = await _publicAccessService.HasAccessAsync( + path, + currentMember.UserName, + async () => await getUserRolesAsync()); + } + return result; + } } } diff --git a/src/Umbraco.Web.Common/Security/MemberRoleManager.cs b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs new file mode 100644 index 0000000000..c0cd18aeda --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberRoleManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class MemberRoleManager : RoleManager, IMemberRoleManager + { + public MemberRoleManager( + IRoleStore store, + IEnumerable> roleValidators, + IdentityErrorDescriber errors, + ILogger logger) + : base(store, roleValidators, new NoopLookupNormalizer(), errors, logger) { } + + IEnumerable IMemberRoleManager.Roles => base.Roles.ToList(); + } +} diff --git a/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs new file mode 100644 index 0000000000..8e6174e5f1 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/PublicAccessChecker.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class PublicAccessChecker : IPublicAccessChecker + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPublicAccessService _publicAccessService; + private readonly IContentService _contentService; + + public PublicAccessChecker(IHttpContextAccessor httpContextAccessor, IPublicAccessService publicAccessService, IContentService contentService) + { + _httpContextAccessor = httpContextAccessor; + _publicAccessService = publicAccessService; + _contentService = contentService; + } + + public async Task HasMemberAccessToContentAsync(int publishedContentId) + { + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + IMemberManager memberManager = httpContext.RequestServices.GetRequiredService(); + if (httpContext.User.Identity == null || !httpContext.User.Identity.IsAuthenticated) + { + return PublicAccessStatus.NotLoggedIn; + } + MemberIdentityUser currentMember = await memberManager.GetUserAsync(httpContext.User); + if (currentMember == null) + { + return PublicAccessStatus.NotLoggedIn; + } + + var username = currentMember.UserName; + IList userRoles = await memberManager.GetRolesAsync(currentMember); + + if (userRoles.Count == 0) + { + return PublicAccessStatus.AccessDenied; + } + + if (!currentMember.IsApproved) + { + return PublicAccessStatus.NotApproved; + } + + if (currentMember.IsLockedOut) + { + return PublicAccessStatus.LockedOut; + } + + if (!_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles)) + { + return PublicAccessStatus.AccessDenied; + } + + return PublicAccessStatus.AccessAccepted; + } + } +} diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index ea29098bef..dd52b397d3 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Models.Identity; +using Umbraco.Cms.Core.Security; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security @@ -25,7 +25,15 @@ namespace Umbraco.Cms.Web.Common.Security // 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) + public UmbracoSignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) { } @@ -38,7 +46,7 @@ namespace Umbraco.Cms.Web.Common.Security 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); + SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); return await HandleSignIn(user, user.UserName, result); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 368eab2339..0f359f0689 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -1197,115 +1197,6 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ), "Failed to roll back content item with id " + contentId ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getPublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns the public access protection for a content item - * - * ##usage - *
-          * contentResource.getPublicAccess(contentId)
-          *    .then(function(publicAccess) {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @returns {Promise} resourcePromise object containing the public access protection - * - */ - getPublicAccess: function (contentId) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "GetPublicAccess", { - contentId: contentId - }) - ), - "Failed to get public access for content item with id " + contentId - ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#updatePublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Sets or updates the public access protection for a content item - * - * ##usage - *
-          * contentResource.updatePublicAccess(contentId, userName, password, roles, loginPageId, errorPageId)
-          *    .then(function() {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @param {Array} groups The names of the groups that should have access (if using group based protection) - * @param {Array} usernames The usernames of the members that should have access (if using member based protection) - * @param {Int} loginPageId The Id of the login page - * @param {Int} errorPageId The Id of the error page - * @returns {Promise} resourcePromise object containing the public access protection - * - */ - updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { - var publicAccess = { - contentId: contentId, - loginPageId: loginPageId, - errorPageId: errorPageId - }; - if (Utilities.isArray(groups) && groups.length) { - publicAccess.groups = groups; - } - else if (Utilities.isArray(usernames) && usernames.length) { - publicAccess.usernames = usernames; - } - else { - throw "must supply either userName/password or roles"; - } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostPublicAccess", publicAccess) - ), - "Failed to update public access for content item with id " + contentId - ); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#removePublicAccess - * @methodOf umbraco.resources.contentResource - * - * @description - * Removes the public access protection for a content item - * - * ##usage - *
-          * contentResource.removePublicAccess(contentId)
-          *    .then(function() {
-          *        // do your thing
-          *    });
-          * 
- * - * @param {Int} contentId The content Id - * @returns {Promise} resourcePromise object that's resolved once the public access has been removed - * - */ - removePublicAccess: function (contentId) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "RemovePublicAccess", { - contentId: contentId - }) - ), - "Failed to remove public access for content item with id " + contentId - ); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js new file mode 100644 index 0000000000..d91924a2eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js @@ -0,0 +1,128 @@ +/** + * @ngdoc service + * @name umbraco.resources.publicAccessResource + * @description Handles all transactions of public access data + * + * @requires $q + * @requires $http + * @requires umbDataFormatter + * @requires umbRequestHelper + * + **/ + +function publicAccessResource($http, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#getPublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Returns the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.getPublicAccess(contentId)
+          *    .then(function(publicAccess) {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object containing the public access protection + * + */ + getPublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "GetPublicAccess", { + contentId: contentId + }) + ), + "Failed to get public access for content item with id " + contentId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#updatePublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Sets or updates the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.updatePublicAccess(contentId, userName, password, roles, loginPageId, errorPageId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @param {Array} groups The names of the groups that should have access (if using group based protection) + * @param {Array} usernames The usernames of the members that should have access (if using member based protection) + * @param {Int} loginPageId The Id of the login page + * @param {Int} errorPageId The Id of the error page + * @returns {Promise} resourcePromise object containing the public access protection + * + */ + updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { + var publicAccess = { + contentId: contentId, + loginPageId: loginPageId, + errorPageId: errorPageId + }; + if (Utilities.isArray(groups) && groups.length) { + publicAccess.groups = groups; + } + else if (Utilities.isArray(usernames) && usernames.length) { + publicAccess.usernames = usernames; + } + else { + throw "must supply either userName/password or roles"; + } + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "PostPublicAccess", publicAccess) + ), + "Failed to update public access for content item with id " + contentId + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.publicAccessResource#removePublicAccess + * @methodOf umbraco.resources.publicAccessResource + * + * @description + * Removes the public access protection for a content item + * + * ##usage + *
+          * publicAccessResource.removePublicAccess(contentId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object that's resolved once the public access has been removed + * + */ + removePublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("publicAccessApiBaseUrl", "RemovePublicAccess", { + contentId: contentId + }) + ), + "Failed to remove public access for content item with id " + contentId + ); + } + }; +} + +angular.module('umbraco.resources').factory('publicAccessResource', publicAccessResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index fcd0294849..01ba2567a7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,261 +1,261 @@ (function () { - "use strict"; + "use strict"; - function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { + function ContentProtectController($scope, $q, publicAccessResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { - var vm = this; - var id = $scope.currentNode.id; + var vm = this; + var id = $scope.currentNode.id; + vm.loading = false; + vm.buttonState = "init"; + + vm.isValid = isValid; + vm.next = next; + vm.save = save; + vm.close = close; + vm.toggle = toggle; + vm.pickLoginPage = pickLoginPage; + vm.pickErrorPage = pickErrorPage; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; + vm.removeProtection = removeProtection; + vm.removeProtectionConfirm = removeProtectionConfirm; + + vm.type = null; + vm.step = null; + + function onInit() { + vm.loading = true; + + // get the current public access protection + publicAccessResource.getPublicAccess(id).then(function (publicAccess) { vm.loading = false; - vm.buttonState = "init"; - vm.isValid = isValid; - vm.next = next; - vm.save = save; - vm.close = close; - vm.toggle = toggle; - vm.pickLoginPage = pickLoginPage; - vm.pickErrorPage = pickErrorPage; - vm.pickGroup = pickGroup; - vm.removeGroup = removeGroup; - vm.pickMember = pickMember; - vm.removeMember = removeMember; - vm.removeProtection = removeProtection; - vm.removeProtectionConfirm = removeProtectionConfirm; + // init the current settings for public access (if any) + vm.loginPage = publicAccess.loginPage; + vm.errorPage = publicAccess.errorPage; + vm.groups = publicAccess.groups || []; + vm.members = publicAccess.members || []; + vm.canRemove = true; - vm.type = null; - vm.step = null; - - function onInit() { - vm.loading = true; - - // get the current public access protection - contentResource.getPublicAccess(id).then(function (publicAccess) { - vm.loading = false; - - // init the current settings for public access (if any) - vm.loginPage = publicAccess.loginPage; - vm.errorPage = publicAccess.errorPage; - vm.groups = publicAccess.groups || []; - vm.members = publicAccess.members || []; - vm.canRemove = true; - - if (vm.members.length) { - vm.type = "member"; - next(); - } - else if (vm.groups.length) { - vm.type = "group"; - next(); - } - else { - vm.canRemove = false; - } - }); + if (vm.members.length) { + vm.type = "member"; + next(); } - - function next() { - if (vm.type === "group") { - vm.loading = true; - // get all existing member groups for lookup upon selection - // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore - memberGroupResource.getGroups().then(function (groups) { - vm.step = vm.type; - vm.allGroups = groups; - vm.hasGroups = groups.length > 0; - vm.loading = false; - }); - } - else { - vm.step = vm.type; - } + else if (vm.groups.length) { + vm.type = "group"; + next(); } - - function isValid() { - if (!vm.type) { - return false; - } - if (!vm.protectForm.$valid) { - return false; - } - if (!vm.loginPage || !vm.errorPage) { - return false; - } - if (vm.type === "group") { - return vm.groups && vm.groups.length > 0; - } - if (vm.type === "member") { - return vm.members && vm.members.length > 0; - } - return true; + else { + vm.canRemove = false; } - - function save() { - vm.buttonState = "busy"; - var groups = _.map(vm.groups, function (group) { return group.name; }); - var usernames = _.map(vm.members, function (member) { return member.username; }); - contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( - function () { - localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - $scope.dialog.confirmDiscardChanges = true; - }, function (error) { - vm.error = error; - vm.buttonState = "error"; - } - ); - } - - function close() { - // ensure that we haven't set a locked state on the dialog before closing it - navigationService.allowHideDialog(true); - navigationService.hideDialog(); - } - - function toggle(group) { - group.selected = !group.selected; - $scope.dialog.confirmDiscardChanges = true; - } - - function pickGroup() { - navigationService.allowHideDialog(false); - editorService.memberGroupPicker({ - multiPicker: true, - submit: function(model) { - var selectedGroupIds = model.selectedMemberGroups - ? model.selectedMemberGroups - : [model.selectedMemberGroup]; - _.each(selectedGroupIds, - function (groupId) { - // find the group in the lookup list and add it if it isn't already - var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); - if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { - vm.groups.push(group); - } - }); - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function() { - editorService.close(); - navigationService.allowHideDialog(true); - } - }); - } - - function removeGroup(group) { - vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); - $scope.dialog.confirmDiscardChanges = true; - } - - function pickMember() { - navigationService.allowHideDialog(false); - // TODO: once editorService has a memberPicker method, use that instead - editorService.treePicker({ - multiPicker: true, - entityType: "Member", - section: "member", - treeAlias: "member", - filter: function (i) { - return i.metaData.isContainer; - }, - filterCssClass: "not-allowed", - submit: function (model) { - if (model.selection && model.selection.length) { - var promises = []; - // get the selected member usernames - _.each(model.selection, - function (member) { - // TODO: - // as-is we need to fetch all the picked members one at a time to get their usernames. - // when editorService has a memberPicker method, see if this can't be avoided - otherwise - // add a memberResource.getByKeys() method to do all this in one request - promises.push( - memberResource.getByKey(member.key).then(function(newMember) { - if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { - vm.members.push(newMember); - } - }) - ); - }); - editorService.close(); - navigationService.allowHideDialog(true); - // wait for all the member lookups to complete - vm.loading = true; - $q.all(promises).then(function() { - vm.loading = false; - }); - $scope.dialog.confirmDiscardChanges = true; - } - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); - } - }); - } - - function removeMember(member) { - vm.members = _.without(vm.members, member); - } - - function pickLoginPage() { - pickPage(vm.loginPage); - } - - function pickErrorPage() { - pickPage(vm.errorPage); - } - - function pickPage(page) { - navigationService.allowHideDialog(false); - editorService.contentPicker({ - submit: function (model) { - if (page === vm.loginPage) { - vm.loginPage = model.selection[0]; - } - else { - vm.errorPage = model.selection[0]; - } - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); - } - }); - } - - function removeProtection() { - vm.removing = true; - } - - function removeProtectionConfirm() { - vm.buttonState = "busy"; - contentResource.removePublicAccess(id).then( - function () { - localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - }, function (error) { - vm.error = error; - vm.buttonState = "error"; - } - ); - } - - onInit(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); + function next() { + if (vm.type === "group") { + vm.loading = true; + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore + memberGroupResource.getGroups().then(function (groups) { + vm.step = vm.type; + vm.allGroups = groups; + vm.hasGroups = groups.length > 0; + vm.loading = false; + }); + } + else { + vm.step = vm.type; + } + } + + function isValid() { + if (!vm.type) { + return false; + } + if (!vm.protectForm.$valid) { + return false; + } + if (!vm.loginPage || !vm.errorPage) { + return false; + } + if (vm.type === "group") { + return vm.groups && vm.groups.length > 0; + } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } + return true; + } + + function save() { + vm.buttonState = "busy"; + var groups = _.map(vm.groups, function (group) { return group.name; }); + var usernames = _.map(vm.members, function (member) { return member.username; }); + publicAccessResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( + function () { + localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + $scope.dialog.confirmDiscardChanges = true; + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + function close() { + // ensure that we haven't set a locked state on the dialog before closing it + navigationService.allowHideDialog(true); + navigationService.hideDialog(); + } + + function toggle(group) { + group.selected = !group.selected; + $scope.dialog.confirmDiscardChanges = true; + } + + function pickGroup() { + navigationService.allowHideDialog(false); + editorService.memberGroupPicker({ + multiPicker: true, + submit: function (model) { + var selectedGroupIds = model.selectedMemberGroups + ? model.selectedMemberGroups + : [model.selectedMemberGroup]; + _.each(selectedGroupIds, + function (groupId) { + // find the group in the lookup list and add it if it isn't already + var group = _.find(vm.allGroups, function (g) { return g.id === parseInt(groupId); }); + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { + vm.groups.push(group); + } + }); + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeGroup(group) { + vm.groups = _.reject(vm.groups, function (g) { return g.id === group.id }); + $scope.dialog.confirmDiscardChanges = true; + } + + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames + _.each(model.selection, + function (member) { + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function (newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); + } + }) + ); + }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function () { + vm.loading = false; + }); + $scope.dialog.confirmDiscardChanges = true; + } + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeMember(member) { + vm.members = _.without(vm.members, member); + } + + function pickLoginPage() { + pickPage(vm.loginPage); + } + + function pickErrorPage() { + pickPage(vm.errorPage); + } + + function pickPage(page) { + navigationService.allowHideDialog(false); + editorService.contentPicker({ + submit: function (model) { + if (page === vm.loginPage) { + vm.loginPage = model.selection[0]; + } + else { + vm.errorPage = model.selection[0]; + } + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeProtection() { + vm.removing = true; + } + + function removeProtectionConfirm() { + vm.buttonState = "busy"; + publicAccessResource.removePublicAccess(id).then( + function () { + localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); })(); diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 820ec6d706..38db763230 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; +using Microsoft.Extensions.Hosting; namespace Umbraco.Cms.Web.UI.NetCore { @@ -40,7 +41,7 @@ namespace Umbraco.Cms.Web.UI.NetCore { #pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) - .AddBackOffice() + .AddBackOffice() .AddWebsite() .AddComposers() .Build(); @@ -51,10 +52,22 @@ namespace Umbraco.Cms.Web.UI.NetCore /// /// Configures the application /// - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - app.UseUmbracoBackOffice(); - app.UseUmbracoWebsite(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseUmbraco() + .WithBackOffice() + .WithWebsite() + .WithEndpoints(u => + { + u.UseInstallerEndpoints(); + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); } } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index 6a7b8941be..afeb41a252 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -65,8 +65,11 @@ namespace Umbraco.Cms.Web.Website.Controllers : CurrentPage.AncestorOrSelf(1).Url(PublishedUrlProvider)); } - // Redirect to current page by default. - return RedirectToCurrentUmbracoPage(); + // Redirect to current URL by default. + // This is different from the current 'page' because when using Public Access the current page + // will be the login page, but the URL will be on the requested page so that's where we need + // to redirect too. + return RedirectToCurrentUmbracoUrl(); } if (result.RequiresTwoFactor) diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 1bdad575c0..69618004c7 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -2,12 +2,11 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Web.Common.Routing; -using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Website.Collections; using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Cms.Web.Website.Middleware; using Umbraco.Cms.Web.Website.Models; using Umbraco.Cms.Web.Website.Routing; using Umbraco.Cms.Web.Website.ViewEngines; @@ -48,12 +47,15 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder .AddDistributedCache() .AddModelsBuilder(); + builder.AddMembersIdentity(); + return builder; } - } } diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs new file mode 100644 index 0000000000..54c4ab4186 --- /dev/null +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Website.Middleware; +using Umbraco.Cms.Web.Website.Routing; + +namespace Umbraco.Extensions +{ + /// + /// extensions for the umbraco front-end website + /// + public static partial class UmbracoApplicationBuilderExtensions + { + /// + /// Adds all required middleware to run the website + /// + /// + /// + public static IUmbracoApplicationBuilder WithWebsite(this IUmbracoApplicationBuilder builder) + { + builder.AppBuilder.UseMiddleware(); + return builder; + } + + /// + /// Sets up routes for the front-end umbraco website + /// + public static IUmbracoEndpointBuilder UseWebsiteEndpoints(this IUmbracoEndpointBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (!builder.RuntimeState.UmbracoCanBoot()) + { + return builder; + } + + FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder); + builder.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); + + return builder; + } + } +} diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs deleted file mode 100644 index 4f049abdac..0000000000 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Web.Website.Routing; - -namespace Umbraco.Extensions -{ - /// - /// extensions for the umbraco front-end website - /// - public static class UmbracoWebsiteApplicationBuilderExtensions - { - /// - /// Sets up services and routes for the front-end umbraco website - /// - public static IApplicationBuilder UseUmbracoWebsite(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (!app.UmbracoCanBoot()) - { - return app; - } - - app.UseUmbracoRoutes(); - - return app; - } - - /// - /// Sets up routes for the umbraco front-end - /// - public static IApplicationBuilder UseUmbracoRoutes(this IApplicationBuilder app) - { - app.UseEndpoints(endpoints => - { - FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); - surfaceRoutes.CreateRoutes(endpoints); - - endpoints.MapDynamicControllerRoute("/{**slug}"); - }); - - return app; - } - } -} diff --git a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs new file mode 100644 index 0000000000..cdf721cfbc --- /dev/null +++ b/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs @@ -0,0 +1,154 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.Routing; +using Umbraco.Cms.Web.Website.Routing; + +namespace Umbraco.Cms.Web.Website.Middleware +{ + public class PublicAccessMiddleware : IMiddleware + { + private readonly ILogger _logger; + private readonly IPublicAccessService _publicAccessService; + private readonly IPublicAccessChecker _publicAccessChecker; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory; + private readonly IPublishedRouter _publishedRouter; + + public PublicAccessMiddleware( + ILogger logger, + IPublicAccessService publicAccessService, + IPublicAccessChecker publicAccessChecker, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoRouteValuesFactory umbracoRouteValuesFactory, + IPublishedRouter publishedRouter) + { + _logger = logger; + _publicAccessService = publicAccessService; + _publicAccessChecker = publicAccessChecker; + _umbracoContextAccessor = umbracoContextAccessor; + _umbracoRouteValuesFactory = umbracoRouteValuesFactory; + _publishedRouter = publishedRouter; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + UmbracoRouteValues umbracoRouteValues = context.Features.Get(); + + if (umbracoRouteValues != null) + { + await EnsurePublishedContentAccess(context, umbracoRouteValues); + } + + await next(context); + } + + /// + /// Ensures that access to current node is permitted. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + private async Task EnsurePublishedContentAccess(HttpContext httpContext, UmbracoRouteValues routeValues) + { + // because these might loop, we have to have some sort of infinite loop detection + int i = 0; + const int maxLoop = 8; + PublicAccessStatus publicAccessStatus = PublicAccessStatus.AccessAccepted; + do + { + _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Loop {LoopCounter}", i); + + + IPublishedContent publishedContent = routeValues.PublishedRequest?.PublishedContent; + if (publishedContent == null) + { + return; + } + + var path = publishedContent.Path; + + Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); + + if (publicAccessAttempt) + { + _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); + + publicAccessStatus = await _publicAccessChecker.HasMemberAccessToContentAsync(publishedContent.Id); + switch (publicAccessStatus) + { + case PublicAccessStatus.NotLoggedIn: + _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.LoginNodeId); + break; + case PublicAccessStatus.AccessDenied: + _logger.LogDebug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.LockedOut: + _logger.LogDebug("Current member is locked out, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.NotApproved: + _logger.LogDebug("Current member is unapproved, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.AccessAccepted: + _logger.LogDebug("Current member has access"); + break; + } + } + else + { + publicAccessStatus = PublicAccessStatus.AccessAccepted; + _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); + } + + + //loop until we have access or reached max loops + } while (routeValues != null && publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop); + + if (i == maxLoop) + { + _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Looks like we are running into an infinite loop, abort"); + } + } + + + + private async Task SetPublishedContentAsOtherPageAsync(HttpContext httpContext, IPublishedRequest publishedRequest, int pageId) + { + if (pageId != publishedRequest.PublishedContent.Id) + { + IPublishedContent publishedContent = _umbracoContextAccessor.UmbracoContext.PublishedSnapshot.Content.GetById(pageId); + if (publishedContent == null) + { + throw new InvalidOperationException("No content found by id " + pageId); + } + + IPublishedRequest reRouted = await _publishedRouter.UpdateRequestAsync(publishedRequest, publishedContent); + + // we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues. + UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted); + + // Update the feature + httpContext.Features.Set(updatedRouteValues); + + return updatedRouteValues; + } + else + { + _logger.LogWarning("Public Access rule has a redirect node set to itself, nothing can be routed."); + // Update the feature to nothing - cannot continue + httpContext.Features.Set(null); + return null; + } + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs index 7e30773bf5..e25921bd91 100644 --- a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; @@ -12,6 +13,6 @@ namespace Umbraco.Cms.Web.Website.Routing /// /// Creates /// - UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request); + Task CreateAsync(HttpContext httpContext, IPublishedRequest request); } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index b8446ce718..eceae11462 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; @@ -114,9 +115,9 @@ namespace Umbraco.Cms.Web.Website.Routing return values; } - IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext); + IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, _umbracoContextAccessor.UmbracoContext); - UmbracoRouteValues umbracoRouteValues = _routeValuesFactory.Create(httpContext, publishedRequest); + UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); // Store the route values as a httpcontext feature httpContext.Features.Set(umbracoRouteValues); @@ -137,7 +138,7 @@ namespace Umbraco.Cms.Web.Website.Routing return values; } - private async Task RouteRequestAsync(IUmbracoContext umbracoContext) + private async Task RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext) { // ok, process diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index f44890cf2f..e1b85919f4 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Umbraco.Cms.Core.Features; @@ -63,7 +64,7 @@ namespace Umbraco.Cms.Web.Website.Routing protected string DefaultControllerName => _defaultControllerName.Value; /// - public UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request) + public async Task CreateAsync(HttpContext httpContext, IPublishedRequest request) { if (httpContext is null) { @@ -95,7 +96,7 @@ namespace Umbraco.Cms.Web.Website.Routing def = CheckHijackedRoute(httpContext, def, out bool hasHijackedRoute); - def = CheckNoTemplate(httpContext, def, hasHijackedRoute); + def = await CheckNoTemplateAsync(httpContext, def, hasHijackedRoute); return def; } @@ -129,7 +130,7 @@ namespace Umbraco.Cms.Web.Website.Routing /// /// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline again for last chance finders /// - private UmbracoRouteValues CheckNoTemplate(HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) + private async Task CheckNoTemplateAsync(HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) { IPublishedRequest request = def.PublishedRequest; @@ -147,15 +148,14 @@ namespace Umbraco.Cms.Web.Website.Routing // This is basically a 404 even if there is content found. // We then need to re-run this through the pipeline for the last // chance finders to work. - IPublishedRequestBuilder builder = _publishedRouter.UpdateRequestToNotFound(request); + // Set to null since we are telling it there is no content. + request = await _publishedRouter.UpdateRequestAsync(request, null); - if (builder == null) + if (request == null) { - throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestToNotFound)} cannot return null"); + throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null"); } - request = builder.Build(); - def = new UmbracoRouteValues( request, def.ControllerActionDescriptor, diff --git a/src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs deleted file mode 100644 index 6395a0d193..0000000000 --- a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; -using Umbraco.Extensions; -using AuthorizeAttribute = System.Web.Mvc.AuthorizeAttribute; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.Mvc -{ - /// - /// Attribute for attributing controller actions to restrict them - /// to just authenticated members, and optionally of a particular type and/or group - /// - public sealed class MemberAuthorizeAttribute : AuthorizeAttribute - { - /// - /// Comma delimited list of allowed member types - /// - public string AllowType { get; set; } - - /// - /// Comma delimited list of allowed member groups - /// - public string AllowGroup { get; set; } - - /// - /// Comma delimited list of allowed members - /// - public string AllowMembers { get; set; } - - protected override bool AuthorizeCore(HttpContextBase httpContext) - { - if (AllowMembers.IsNullOrWhiteSpace()) - AllowMembers = ""; - if (AllowGroup.IsNullOrWhiteSpace()) - AllowGroup = ""; - if (AllowType.IsNullOrWhiteSpace()) - AllowType = ""; - - var members = new List(); - foreach (var s in AllowMembers.Split(',')) - { - if (int.TryParse(s, out var id)) - { - members.Add(id); - } - } - - var helper = Current.MembershipHelper; - return helper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); - - } - - /// - /// Override method to throw exception instead of returning a 401 result - /// - /// - protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) - { - throw new HttpException(403, "Resource restricted: either member is not logged on or is not of a permitted type or group."); - } - - } -} diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index e93fcbdfd0..61ed6c9fdd 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -17,15 +17,6 @@ using Umbraco.Web.Security.Providers; namespace Umbraco.Web.Security { // MIGRATED TO NETCORE - // TODO: Analyse all - much can be moved/removed since most methods will occur on the manager via identity implementation - - /// - /// Helper class containing logic relating to the built-in Umbraco members macros and controllers for: - /// - Registration - /// - Updating - /// - Logging in - /// - Current status - /// public class MembershipHelper { private readonly MembersMembershipProvider _membershipProvider; @@ -76,95 +67,6 @@ namespace Umbraco.Web.Security protected IPublishedMemberCache MemberCache { get; } - /// - /// Check if a document object is protected by the "Protect Pages" functionality in umbraco - /// - /// The full path of the document object to check - /// True if the document object is protected - public virtual bool IsProtected(string path) - { - //this is a cached call - return _publicAccessService.IsProtected(path); - } - - public virtual IDictionary IsProtected(IEnumerable paths) - { - var result = new Dictionary(); - foreach (var path in paths) - { - //this is a cached call - result[path] = _publicAccessService.IsProtected(path); - } - return result; - } - - /// - /// Check if the current user has access to a document - /// - /// The full path of the document object to check - /// True if the current user has access or if the current document isn't protected - public virtual bool MemberHasAccess(string path) - { - if (IsProtected(path)) - { - return IsLoggedIn() && HasAccess(path, Roles.Provider); - } - return true; - } - - /// - /// Checks if the current user has access to the paths - /// - /// - /// - public virtual IDictionary MemberHasAccess(IEnumerable paths) - { - var protectedPaths = IsProtected(paths); - - var pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); - var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider); - - var result = new Dictionary(); - foreach (var path in paths) - { - pathsWithAccess.TryGetValue(path, out var hasAccess); - // if it's not found it's false anyways - result[path] = !pathsWithProtection.Contains(path) || hasAccess; - } - return result; - } - - /// - /// This will check if the member has access to this path - /// - /// - /// - /// - private bool HasAccess(string path, RoleProvider roleProvider) - { - return _publicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser); - } - - private IDictionary HasAccess(IEnumerable paths, RoleProvider roleProvider) - { - // ensure we only lookup user roles once - string[] userRoles = null; - string[] getUserRoles(string username) - { - if (userRoles != null) - return userRoles; - userRoles = roleProvider.GetRolesForUser(username).ToArray(); - return userRoles; - } - - var result = new Dictionary(); - foreach (var path in paths) - { - result[path] = IsLoggedIn() && _publicAccessService.HasAccess(path, CurrentUserName, getUserRoles); - } - return result; - } - #region Querying for front-end public virtual IPublishedContent GetByProviderKey(object key) @@ -228,135 +130,7 @@ namespace Umbraco.Web.Security return null; } - /// - /// Returns the currently logged in member as IPublishedContent - /// - /// - public virtual IPublishedContent GetCurrentMember() - { - if (IsLoggedIn() == false) - { - return null; - } - var result = GetCurrentPersistedMember(); - return result == null ? null : MemberCache.GetByMember(result); - } - #endregion - /// - /// Gets the current user's roles. - /// - /// Roles are cached per user name, at request level. - public IEnumerable GetCurrentUserRoles() - => GetUserRoles(CurrentUserName); - - /// - /// Gets a user's roles. - /// - /// Roles are cached per user name, at request level. - public IEnumerable GetUserRoles(string userName) - { - // optimize by caching per-request (v7 cached per PublishedRequest, in PublishedRouter) - var key = "Umbraco.Web.Security.MembershipHelper__Roles__" + userName; - return _appCaches.RequestCache.GetCacheItem(key, () => Roles.Provider.GetRolesForUser(userName)); - } - - /// - /// Check if a member is logged in - /// - /// - public bool IsLoggedIn() - { - var httpContext = _httpContextAccessor.HttpContext; - return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated; - } - - /// - /// Returns the currently logged in username - /// - public string CurrentUserName => _httpContextAccessor.GetRequiredHttpContext().User.Identity.Name; - - /// - /// Returns true or false if the currently logged in member is authorized based on the parameters provided - /// - /// - /// - /// - /// - public virtual bool IsMemberAuthorized( - IEnumerable allowTypes = null, - IEnumerable allowGroups = null, - IEnumerable allowMembers = null) - { - if (allowTypes == null) - allowTypes = Enumerable.Empty(); - if (allowGroups == null) - allowGroups = Enumerable.Empty(); - if (allowMembers == null) - allowMembers = Enumerable.Empty(); - - // Allow by default - var allowAction = true; - - if (IsLoggedIn() == false) - { - // If not logged on, not allowed - allowAction = false; - } - else - { - var provider = _membershipProvider; - - string username; - - var member = GetCurrentPersistedMember(); - // If a member could not be resolved from the provider, we are clearly not authorized and can break right here - if (member == null) - return false; - username = member.Username; - - // If types defined, check member is of one of those types - var allowTypesList = allowTypes as IList ?? allowTypes.ToList(); - if (allowTypesList.Any(allowType => allowType != string.Empty)) - { - // Allow only if member's type is in list - allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(member.ContentType.Alias.ToLowerInvariant()); - } - - // If specific members defined, check member is of one of those - if (allowAction && allowMembers.Any()) - { - // Allow only if member's Id is in the list - allowAction = allowMembers.Contains(member.Id); - } - - // If groups defined, check member is of one of those groups - var allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); - if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) - { - // Allow only if member is assigned to a group in the list - var groups = _roleProvider.GetRolesForUser(username); - allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); - } - } - - return allowAction; - } - - /// - /// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity! - /// - /// - private IMember GetCurrentPersistedMember() - { - var provider = _membershipProvider; - - var username = provider.GetCurrentUserName(); - // The result of this is cached by the MemberRepository - var member = _memberService.GetByUsername(username); - return member; - } - } } diff --git a/src/Umbraco.Web/Security/PublicAccessChecker.cs b/src/Umbraco.Web/Security/PublicAccessChecker.cs deleted file mode 100644 index 8ac0c4be8d..0000000000 --- a/src/Umbraco.Web/Security/PublicAccessChecker.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Web.Security -{ - public class PublicAccessChecker : IPublicAccessChecker - { - //TODO: This is lazy to avoid circular dependency. We don't care right now because all membership is going to be changed. - private readonly Lazy _membershipHelper; - private readonly IPublicAccessService _publicAccessService; - private readonly IContentService _contentService; - private readonly IPublishedValueFallback _publishedValueFallback; - - public PublicAccessChecker(Lazy membershipHelper, IPublicAccessService publicAccessService, IContentService contentService, IPublishedValueFallback publishedValueFallback) - { - _membershipHelper = membershipHelper; - _publicAccessService = publicAccessService; - _contentService = contentService; - _publishedValueFallback = publishedValueFallback; - } - - public PublicAccessStatus HasMemberAccessToContent(int publishedContentId) - { - var membershipHelper = _membershipHelper.Value; - - if (membershipHelper.IsLoggedIn() == false) - { - return PublicAccessStatus.NotLoggedIn; - } - - var username = membershipHelper.CurrentUserName; - var userRoles = membershipHelper.GetCurrentUserRoles(); - - if (_publicAccessService.HasAccess(publishedContentId, _contentService, username, userRoles) == false) - { - return PublicAccessStatus.AccessDenied; - } - - var member = membershipHelper.GetCurrentMember(); - - if (member.HasProperty(Constants.Conventions.Member.IsApproved) == false) - { - return PublicAccessStatus.NotApproved; - } - - if (member.HasProperty(Constants.Conventions.Member.IsLockedOut) && - member.Value(_publishedValueFallback, Constants.Conventions.Member.IsApproved)) - { - return PublicAccessStatus.LockedOut; - } - - return PublicAccessStatus.AccessAccepted; - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d184157658..60299a9b34 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -137,7 +137,6 @@ - @@ -149,7 +148,6 @@ - @@ -183,7 +181,6 @@ - @@ -237,20 +234,20 @@ Name="UmbGenerateSerializationAssemblies" Condition="'$(_SGenGenerateSerializationAssembliesConfig)' == 'On' or ('@(WebReferenceUrl)'!='' and '$(_SGenGenerateSerializationAssembliesConfig)' == 'Auto')" --> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index d396aee2f0..f7f6374ea4 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -189,19 +189,13 @@ namespace Umbraco.Web ///
/// The full path of the document object to check /// True if the current user has access or if the current document isn't protected - public bool MemberHasAccess(string path) - { - return _membershipHelper.MemberHasAccess(path); - } + public bool MemberHasAccess(string path) => throw new NotImplementedException("Migrated to netcore"); /// /// Whether or not the current member is logged in (based on the membership provider) /// /// True is the current user is logged in - public bool MemberIsLoggedOn() - { - return _membershipHelper.IsLoggedIn(); - } + public bool MemberIsLoggedOn() => throw new NotImplementedException("Migrated to netcore"); #endregion diff --git a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs deleted file mode 100644 index d91f164cf2..0000000000 --- a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Web.Http; -using Umbraco.Extensions; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.WebApi -{ - /// - /// Attribute for attributing controller actions to restrict them - /// to just authenticated members, and optionally of a particular type and/or group - /// - public sealed class MemberAuthorizeAttribute : AuthorizeAttribute - { - /// - /// Comma delimited list of allowed member types - /// - public string AllowType { get; set; } - - /// - /// Comma delimited list of allowed member groups - /// - public string AllowGroup { get; set; } - - /// - /// Comma delimited list of allowed members - /// - public string AllowMembers { get; set; } - - protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext) - { - if (AllowMembers.IsNullOrWhiteSpace()) - AllowMembers = ""; - if (AllowGroup.IsNullOrWhiteSpace()) - AllowGroup = ""; - if (AllowType.IsNullOrWhiteSpace()) - AllowType = ""; - - var members = new List(); - foreach (var s in AllowMembers.Split(',')) - { - if (int.TryParse(s, out var id)) - { - members.Add(id); - } - } - - var helper = Current.MembershipHelper; - return helper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); - } - - } -}