diff --git a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs index 6ec0250d90..674012e797 100644 --- a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs @@ -14,13 +14,14 @@ namespace Umbraco.Web.Scheduling private readonly IMainDom _mainDom; private readonly IRuntimeState _runtime; private readonly IServerMessenger _serverMessenger; + private readonly IBackofficeSecurityFactory _backofficeSecurityFactory; private readonly IServerRegistrar _serverRegistrar; private readonly IUmbracoContextFactory _umbracoContextFactory; public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, - IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger) + IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger, IBackofficeSecurityFactory backofficeSecurityFactory) : base(runner, delayMilliseconds, periodMilliseconds) { _runtime = runtime; @@ -30,6 +31,7 @@ namespace Umbraco.Web.Scheduling _umbracoContextFactory = umbracoContextFactory; _logger = logger; _serverMessenger = serverMessenger; + _backofficeSecurityFactory = backofficeSecurityFactory; } public override bool IsAsync => false; @@ -76,6 +78,7 @@ namespace Umbraco.Web.Scheduling // but then what should be its "scope"? could we attach it to scopes? // - and we should definitively *not* have to flush it here (should be auto) // + _backofficeSecurityFactory.EnsureBackofficeSecurity(); using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) { try diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs index cbf1df197d..5fb4f19473 100644 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Scheduling private readonly HealthChecksSettings _healthChecksSettings; private readonly IServerMessenger _serverMessenger; private readonly IRequestAccessor _requestAccessor; + private readonly IBackofficeSecurityFactory _backofficeSecurityFactory; private readonly LoggingSettings _loggingSettings; private readonly KeepAliveSettings _keepAliveSettings; private readonly IHostingEnvironment _hostingEnvironment; @@ -60,7 +61,8 @@ namespace Umbraco.Web.Scheduling IApplicationShutdownRegistry applicationShutdownRegistry, IOptions healthChecksSettings, IServerMessenger serverMessenger, IRequestAccessor requestAccessor, IOptions loggingSettings, IOptions keepAliveSettings, - IHostingEnvironment hostingEnvironment) + IHostingEnvironment hostingEnvironment, + IBackofficeSecurityFactory backofficeSecurityFactory) { _runtime = runtime; _mainDom = mainDom; @@ -77,6 +79,7 @@ namespace Umbraco.Web.Scheduling _healthChecksSettings = healthChecksSettings.Value ?? throw new ArgumentNullException(nameof(healthChecksSettings)); _serverMessenger = serverMessenger; _requestAccessor = requestAccessor; + _backofficeSecurityFactory = backofficeSecurityFactory; _loggingSettings = loggingSettings.Value; _keepAliveSettings = keepAliveSettings.Value; _hostingEnvironment = hostingEnvironment; @@ -150,7 +153,7 @@ namespace Umbraco.Web.Scheduling { // scheduled publishing/unpublishing // install on all, will only run on non-replica servers - var task = new ScheduledPublishing(_publishingRunner, DefaultDelayMilliseconds, OneMinuteMilliseconds, _runtime, _mainDom, _serverRegistrar, _contentService, _umbracoContextFactory, _logger, _serverMessenger); + var task = new ScheduledPublishing(_publishingRunner, DefaultDelayMilliseconds, OneMinuteMilliseconds, _runtime, _mainDom, _serverRegistrar, _contentService, _umbracoContextFactory, _logger, _serverMessenger, _backofficeSecurityFactory); _publishingRunner.TryAdd(task); return task; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs index 810e3e5646..c579a3ec1d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -23,6 +24,7 @@ namespace Umbraco.Web.Editors public class SectionController : UmbracoAuthorizedJsonController { private readonly IControllerFactory _controllerFactory; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IDashboardService _dashboardService; private readonly ILocalizedTextService _localizedTextService; private readonly ISectionService _sectionService; @@ -34,7 +36,8 @@ namespace Umbraco.Web.Editors IBackofficeSecurityAccessor backofficeSecurityAccessor, ILocalizedTextService localizedTextService, IDashboardService dashboardService, ISectionService sectionService, ITreeService treeService, - UmbracoMapper umbracoMapper, IControllerFactory controllerFactory) + UmbracoMapper umbracoMapper, IControllerFactory controllerFactory, + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _localizedTextService = localizedTextService; @@ -43,6 +46,7 @@ namespace Umbraco.Web.Editors _treeService = treeService; _umbracoMapper = umbracoMapper; _controllerFactory = controllerFactory; + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; } public IEnumerable
GetSections() @@ -54,7 +58,7 @@ namespace Umbraco.Web.Editors // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that // since tree's by nature are controllers and require request contextual data var appTreeController = - new ApplicationTreeController(_treeService, _sectionService, _localizedTextService, _controllerFactory) + new ApplicationTreeController(_treeService, _sectionService, _localizedTextService, _controllerFactory, _actionDescriptorCollectionProvider) { ControllerContext = ControllerContext }; diff --git a/src/Umbraco.Web.BackOffice/Extensions/ControllerContextExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ControllerContextExtensions.cs new file mode 100644 index 0000000000..ca17d97fc7 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/ControllerContextExtensions.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Umbraco.Extensions +{ + internal static class ControllerContextExtensions + { + /// + /// Invokes the authorization filters for the controller action. + /// + /// Whether the user is authenticated or not. + internal static async Task InvokeAuthorizationFiltersForRequest(this ControllerContext controllerContext, ActionContext actionContext) + { + var actionDescriptor = controllerContext.ActionDescriptor; + + var filters = actionDescriptor.FilterDescriptors; + var filterGrouping = new FilterGrouping(filters, controllerContext.HttpContext.RequestServices); + + // because the continuation gets built from the inside out we need to reverse the filter list + // so that least specific filters (Global) get run first and the most specific filters (Action) get run last. + var authorizationFilters = filterGrouping.AuthorizationFilters.Reverse().ToList(); + var asyncAuthorizationFilters = filterGrouping.AsyncAuthorizationFilters.Reverse().ToList(); + + if (authorizationFilters.Count == 0 && asyncAuthorizationFilters.Count == 0) + return true; + + // if the authorization filter returns a result, it means it failed to authorize + var authorizationFilterContext = new AuthorizationFilterContext(actionContext, filters.Select(x=>x.Filter).ToArray()); + return await ExecuteAuthorizationFiltersAsync(authorizationFilterContext, authorizationFilters, asyncAuthorizationFilters); + } + + /// + /// Executes a chain of filters. + /// + /// + /// Recursively calls in to itself as its continuation for the next filter in the chain. + /// + private static async Task ExecuteAuthorizationFiltersAsync( + AuthorizationFilterContext authorizationFilterContext, + IList authorizationFilters, + IList asyncAuthorizationFilters) + { + + foreach (var authorizationFilter in authorizationFilters) + { + authorizationFilter.OnAuthorization(authorizationFilterContext); + if (!(authorizationFilterContext.Result is null)) + { + return false; + } + + } + + foreach (var asyncAuthorizationFilter in asyncAuthorizationFilters) + { + await asyncAuthorizationFilter.OnAuthorizationAsync(authorizationFilterContext); + if (!(authorizationFilterContext.Result is null)) + { + return false; + } + } + + return true; + } + + /// + /// Quickly split filters into different types + /// + private class FilterGrouping + { + private readonly List _actionFilters = new List(); + private readonly List _asyncActionFilters = new List(); + private readonly List _authorizationFilters = new List(); + private readonly List _asyncAuthorizationFilters = new List(); + private readonly List _exceptionFilters = new List(); + private readonly List _asyncExceptionFilters = new List(); + + public FilterGrouping(IEnumerable filters, IServiceProvider serviceProvider) + { + if (filters == null) throw new ArgumentNullException("filters"); + + foreach (FilterDescriptor f in filters) + { + var filter = f.Filter; + Categorize(filter, _actionFilters, serviceProvider); + Categorize(filter, _authorizationFilters, serviceProvider); + Categorize(filter, _exceptionFilters, serviceProvider); + Categorize(filter, _asyncActionFilters, serviceProvider); + Categorize(filter, _asyncAuthorizationFilters, serviceProvider); + } + } + + public IEnumerable ActionFilters => _actionFilters; + public IEnumerable AsyncActionFilters => _asyncActionFilters; + public IEnumerable AuthorizationFilters => _authorizationFilters; + + public IEnumerable AsyncAuthorizationFilters => _asyncAuthorizationFilters; + + public IEnumerable ExceptionFilters => _exceptionFilters; + + public IEnumerable AsyncExceptionFilters => _asyncExceptionFilters; + + private static void Categorize(IFilterMetadata filter, List list, IServiceProvider serviceProvider) where T : class + { + if(filter is TypeFilterAttribute typeFilterAttribute) + { + filter = typeFilterAttribute.CreateInstance(serviceProvider); + } + + T match = filter as T; + if (match != null) + { + list.Add(match); + } + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 6253aa1ff4..92cb5b1b93 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Umbraco.Core; using Umbraco.Core.Services; +using Umbraco.Extensions; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Attributes; @@ -35,18 +37,22 @@ namespace Umbraco.Web.Trees private readonly ISectionService _sectionService; private readonly ILocalizedTextService _localizedTextService; private readonly IControllerFactory _controllerFactory; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + public ApplicationTreeController( ITreeService treeService, ISectionService sectionService, ILocalizedTextService localizedTextService, - IControllerFactory controllerFactory + IControllerFactory controllerFactory, + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) { _treeService = treeService; _sectionService = sectionService; _localizedTextService = localizedTextService; _controllerFactory = controllerFactory; + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; } /// @@ -251,67 +257,36 @@ namespace Umbraco.Web.Trees private async Task GetApiControllerProxy(Type controllerType, string action, FormCollection querystring) { // note: this is all required in order to execute the auth-filters for the sub request, we - // need to "trick" web-api into thinking that it is actually executing the proxied controller. - + // need to "trick" mvc into thinking that it is actually executing the proxied controller. + var controllerName = controllerType.Name.Substring(0, controllerType.Name.Length - 10); // remove controller part of name; // create proxy route data specifying the action & controller to execute var routeData = new RouteData(new RouteValueDictionary() { ["action"] = action, - ["controller"] = controllerType.Name.Substring(0,controllerType.Name.Length-10) // remove controller part of name; - + ["controller"] = controllerName }); + if (!(querystring is null)) + { + foreach (var (key,value) in querystring) + { + routeData.Values[key] = value; + } + } + var actionDescriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items + .Cast() + .First(x => + x.ControllerName.Equals(controllerName) && + x.ActionName == action); - var controllerContext = new ControllerContext( - new ActionContext( - HttpContext, - routeData, - new ControllerActionDescriptor() - { - ControllerTypeInfo = controllerType.GetTypeInfo() - } - )); + var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor); + var proxyControllerContext = new ControllerContext(actionContext); + var controller = (TreeController) _controllerFactory.CreateController(proxyControllerContext); - var controller = (TreeController) _controllerFactory.CreateController(controllerContext); - - - //TODO Refactor trees or reimplement this hacks to check authentication. - //https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/3694 - - // var context = ControllerContext; - // - // // get the controller - // var controller = (TreeController) DependencyResolver.Current.GetService(controllerType) - // ?? throw new Exception($"Failed to create controller of type {controllerType.FullName}."); - // - // // create the proxy URL for the controller action - // var proxyUrl = HttpContext.Request.RequestUri.GetLeftPart(UriPartial.Authority) - // + HttpContext.Request.GetUrlHelper().GetUmbracoApiService(action, controllerType) - // + "?" + querystring.ToQueryString(); - // - // - // - // // create a proxy request - // var proxyRequest = new HttpRequestMessage(HttpMethod.Get, proxyUrl); - // - // // create a proxy controller context - // var proxyContext = new HttpControllerContext(context.Configuration, proxyRoute, proxyRequest) - // { - // ControllerDescriptor = new HttpControllerDescriptor(context.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(controllerType), controllerType), - // RequestContext = context.RequestContext, - // Controller = controller - // }; - // - // // wire everything - // controller.ControllerContext = proxyContext; - // controller.Request = proxyContext.Request; - // controller.RequestContext.RouteData = proxyRoute; - // - // // auth - // var authResult = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(); - // if (authResult != null) - // throw new HttpResponseException(authResult); + var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); + if (!isAllowed) + throw new HttpResponseException(HttpStatusCode.Forbidden); return controller; } diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index 6b4ce706e3..5e5b6f6910 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -40,16 +40,16 @@ namespace Umbraco.Web IRequestAccessor requestAccessor) { if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); - if (backofficeSecurity == null) throw new ArgumentNullException(nameof(backofficeSecurity)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + _hostingEnvironment = hostingEnvironment; _cookieManager = cookieManager; _requestAccessor = requestAccessor; ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); - Security = backofficeSecurity; + Security = backofficeSecurity ?? throw new ArgumentNullException(nameof(backofficeSecurity)); // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing _publishedSnapshot = new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dfade80794..962c8ed528 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -296,7 +296,6 @@ - diff --git a/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs b/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs deleted file mode 100644 index 323e52eb36..0000000000 --- a/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.Web.WebApi -{ - internal static class HttpControllerContextExtensions - { - /// - /// Invokes the authorization filters for the controller action. - /// - /// The response of the first filter returning a result, if any, otherwise null (authorized). - internal static async Task InvokeAuthorizationFiltersForRequest(this HttpControllerContext controllerContext) - { - var controllerDescriptor = controllerContext.ControllerDescriptor; - var controllerServices = controllerDescriptor.Configuration.Services; - var actionDescriptor = controllerServices.GetActionSelector().SelectAction(controllerContext); - - var filters = actionDescriptor.GetFilterPipeline(); - var filterGrouping = new FilterGrouping(filters); - - // because the continuation gets built from the inside out we need to reverse the filter list - // so that least specific filters (Global) get run first and the most specific filters (Action) get run last. - var authorizationFilters = filterGrouping.AuthorizationFilters.Reverse().ToList(); - - if (authorizationFilters.Count == 0) - return null; - - // if the authorization filter returns a result, it means it failed to authorize - var actionContext = new HttpActionContext(controllerContext, actionDescriptor); - return await ExecuteAuthorizationFiltersAsync(actionContext, CancellationToken.None, authorizationFilters); - } - - /// - /// Executes a chain of filters. - /// - /// - /// Recursively calls in to itself as its continuation for the next filter in the chain. - /// - private static async Task ExecuteAuthorizationFiltersAsync(HttpActionContext actionContext, CancellationToken token, IList filters, int index = 0) - { - return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token, - () => ++index == filters.Count - ? Task.FromResult(null) - : ExecuteAuthorizationFiltersAsync(actionContext, token, filters, index)); - } - } -}