Netcore: Handle tree authentication (#8866)

* Added helper methods to invoke the authorization filters of the other controller action

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

* Implemented Tree Auth

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

* cleanup

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

* Throw forbidden if user has no access instead of InternalServerError

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

* EnsureBackofficeSecurity for background jobs

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
This commit is contained in:
Bjarke Berg
2020-09-22 13:19:54 +02:00
committed by GitHub
parent 96facc4d35
commit a80de91031
8 changed files with 170 additions and 115 deletions

View File

@@ -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<RecurringTaskBase> 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

View File

@@ -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> healthChecksSettings,
IServerMessenger serverMessenger, IRequestAccessor requestAccessor,
IOptions<LoggingSettings> loggingSettings, IOptions<KeepAliveSettings> 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;
}

View File

@@ -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<Section> 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
};

View File

@@ -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
{
/// <summary>
/// Invokes the authorization filters for the controller action.
/// </summary>
/// <returns>Whether the user is authenticated or not.</returns>
internal static async Task<bool> 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);
}
/// <summary>
/// Executes a chain of filters.
/// </summary>
/// <remarks>
/// Recursively calls in to itself as its continuation for the next filter in the chain.
/// </remarks>
private static async Task<bool> ExecuteAuthorizationFiltersAsync(
AuthorizationFilterContext authorizationFilterContext,
IList<IAuthorizationFilter> authorizationFilters,
IList<IAsyncAuthorizationFilter> 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;
}
/// <summary>
/// Quickly split filters into different types
/// </summary>
private class FilterGrouping
{
private readonly List<IActionFilter> _actionFilters = new List<IActionFilter>();
private readonly List<IAsyncActionFilter> _asyncActionFilters = new List<IAsyncActionFilter>();
private readonly List<IAuthorizationFilter> _authorizationFilters = new List<IAuthorizationFilter>();
private readonly List<IAsyncAuthorizationFilter> _asyncAuthorizationFilters = new List<IAsyncAuthorizationFilter>();
private readonly List<IExceptionFilter> _exceptionFilters = new List<IExceptionFilter>();
private readonly List<IAsyncExceptionFilter> _asyncExceptionFilters = new List<IAsyncExceptionFilter>();
public FilterGrouping(IEnumerable<FilterDescriptor> 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<IActionFilter> ActionFilters => _actionFilters;
public IEnumerable<IAsyncActionFilter> AsyncActionFilters => _asyncActionFilters;
public IEnumerable<IAuthorizationFilter> AuthorizationFilters => _authorizationFilters;
public IEnumerable<IAsyncAuthorizationFilter> AsyncAuthorizationFilters => _asyncAuthorizationFilters;
public IEnumerable<IExceptionFilter> ExceptionFilters => _exceptionFilters;
public IEnumerable<IAsyncExceptionFilter> AsyncExceptionFilters => _asyncExceptionFilters;
private static void Categorize<T>(IFilterMetadata filter, List<T> 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);
}
}
}
}
}

View File

@@ -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;
}
/// <summary>
@@ -251,67 +257,36 @@ namespace Umbraco.Web.Trees
private async Task<object> 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<ControllerActionDescriptor>()
.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;
}

View File

@@ -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<IPublishedSnapshot>(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken));

View File

@@ -296,7 +296,6 @@
<Compile Include="WebApi\Filters\DisableBrowserCacheAttribute.cs" />
<Compile Include="WebApi\Filters\FilterGrouping.cs" />
<Compile Include="WebApi\Filters\ValidateAngularAntiForgeryTokenAttribute.cs" />
<Compile Include="WebApi\HttpControllerContextExtensions.cs" />
<Compile Include="Controllers\UmbRegisterController.cs" />
<Compile Include="Models\ProfileModel.cs" />
<Compile Include="Models\LoginStatusModel.cs" />

View File

@@ -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
{
/// <summary>
/// Invokes the authorization filters for the controller action.
/// </summary>
/// <returns>The response of the first filter returning a result, if any, otherwise null (authorized).</returns>
internal static async Task<HttpResponseMessage> 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);
}
/// <summary>
/// Executes a chain of filters.
/// </summary>
/// <remarks>
/// Recursively calls in to itself as its continuation for the next filter in the chain.
/// </remarks>
private static async Task<HttpResponseMessage> ExecuteAuthorizationFiltersAsync(HttpActionContext actionContext, CancellationToken token, IList<IAuthorizationFilter> filters, int index = 0)
{
return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token,
() => ++index == filters.Count
? Task.FromResult<HttpResponseMessage>(null)
: ExecuteAuthorizationFiltersAsync(actionContext, token, filters, index));
}
}
}