using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Web.BackOffice.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)
{
ControllerActionDescriptor actionDescriptor = controllerContext.ActionDescriptor;
var metadataCollection =
new EndpointMetadataCollection(actionDescriptor.EndpointMetadata.Union(new[] { actionDescriptor }));
IReadOnlyList authorizeData = metadataCollection.GetOrderedMetadata();
IAuthorizationPolicyProvider policyProvider = controllerContext.HttpContext.RequestServices
.GetRequiredService();
AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData);
if (policy is not null)
{
IPolicyEvaluator policyEvaluator =
controllerContext.HttpContext.RequestServices.GetRequiredService();
AuthenticateResult authenticateResult =
await policyEvaluator.AuthenticateAsync(policy, controllerContext.HttpContext);
if (!authenticateResult.Succeeded)
{
return false;
}
// TODO this is super hacky, but we rely on the FeatureAuthorizeHandler can still handle endpoints
// (The way before .NET 5). The .NET 5 way would need to use han http context, for the "inner" request
// with the nested controller
var resource = new Endpoint(null, metadataCollection, null);
PolicyAuthorizationResult authorizeResult =
await policyEvaluator.AuthorizeAsync(policy, authenticateResult, controllerContext.HttpContext, resource);
if (!authorizeResult.Succeeded)
{
return false;
}
}
IList 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 (IAuthorizationFilter authorizationFilter in authorizationFilters)
{
authorizationFilter.OnAuthorization(authorizationFilterContext);
if (!(authorizationFilterContext.Result is null))
{
return false;
}
}
foreach (IAsyncAuthorizationFilter 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();
private readonly List _asyncActionFilters = new();
private readonly List _asyncAuthorizationFilters = new();
private readonly List _asyncExceptionFilters = new();
private readonly List _authorizationFilters = new();
private readonly List _exceptionFilters = new();
public FilterGrouping(IEnumerable filters, IServiceProvider serviceProvider)
{
if (filters == null)
{
throw new ArgumentNullException("filters");
}
foreach (FilterDescriptor f in filters)
{
IFilterMetadata 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);
}
if (filter is T match)
{
list.Add(match);
}
}
}
}