Skip to content

Commit 525705f

Browse files
authored
Cache effective policy in endpoint metadata (#43124)
1 parent dea887d commit 525705f

15 files changed

+626
-7
lines changed

src/Security/Authorization/Core/src/DefaultAuthorizationPolicyProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,11 @@ public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
7171
// A change to either of these behaviors would require shipping a patch of MVC as well.
7272
return Task.FromResult(_options.GetPolicy(policyName));
7373
}
74+
75+
#if NETCOREAPP
76+
/// <summary>
77+
/// Determines if policies from this provider can be cached, which is true only for this type.
78+
/// </summary>
79+
public virtual bool CanCachePolicy => GetType() == typeof(DefaultAuthorizationPolicyProvider);
80+
#endif
7481
}

src/Security/Authorization/Core/src/IAuthorizationPolicyProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ public interface IAuthorizationPolicyProvider
2828
/// </summary>
2929
/// <returns>The fallback authorization policy.</returns>
3030
Task<AuthorizationPolicy?> GetFallbackPolicyAsync();
31+
32+
#if NETCOREAPP
33+
/// <summary>
34+
/// Determines if policies from this provider can be cached, defaults to false.
35+
/// </summary>
36+
bool CanCachePolicy => false;
37+
#endif
3138
}

src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ Microsoft.AspNetCore.Authorization.AuthorizeAttribute</Description>
2020
<Reference Include="Microsoft.Extensions.Options" />
2121
</ItemGroup>
2222

23+
<ItemGroup>
24+
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Shipped.txt" />
25+
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Unshipped.txt" />
26+
</ItemGroup>
27+
2328
</Project>

src/Security/Authorization/Core/src/PublicAPI/net7.0/PublicAPI.Shipped.txt

Lines changed: 171 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationBuilder
3+
Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AuthorizationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void
4+
Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider.CanCachePolicy.get -> bool
5+
Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler.PassThroughAuthorizationHandler(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions!>! options) -> void
6+
static Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizeData!>! authorizeData, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationPolicy!>! policies) -> System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy?>!
7+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
8+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
9+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
10+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
11+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
12+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
13+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
14+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetDefaultPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
15+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetFallbackPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy? policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
16+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetInvokeHandlersAfterFailure(bool invoke) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
17+
virtual Microsoft.AspNetCore.Authorization.DefaultAuthorizationPolicyProvider.CanCachePolicy.get -> bool

src/Security/Authorization/Core/src/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt

Lines changed: 171 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationBuilder
3+
Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AuthorizationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void
4+
Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler.PassThroughAuthorizationHandler(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions!>! options) -> void
5+
static Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizeData!>! authorizeData, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationPolicy!>! policies) -> System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy?>!
6+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
7+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddDefaultPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
8+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
9+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddFallbackPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
10+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
11+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.AddPolicy(string! name, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
12+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
13+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetDefaultPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
14+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetFallbackPolicy(Microsoft.AspNetCore.Authorization.AuthorizationPolicy? policy) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!
15+
virtual Microsoft.AspNetCore.Authorization.AuthorizationBuilder.SetInvokeHandlersAfterFailure(bool invoke) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!

src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class AuthorizationMiddleware
2323

2424
private readonly RequestDelegate _next;
2525
private readonly IAuthorizationPolicyProvider _policyProvider;
26+
private readonly bool _canCache;
27+
private readonly AuthorizationPolicyCache? _policyCache;
2628

2729
/// <summary>
2830
/// Initializes a new instance of <see cref="AuthorizationMiddleware"/>.
@@ -33,6 +35,24 @@ public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvide
3335
{
3436
_next = next ?? throw new ArgumentNullException(nameof(next));
3537
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
38+
_canCache = false;
39+
}
40+
41+
/// <summary>
42+
/// Initializes a new instance of <see cref="AuthorizationMiddleware"/>.
43+
/// </summary>
44+
/// <param name="next">The next middleware in the application middleware pipeline.</param>
45+
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/>.</param>
46+
/// <param name="services">The <see cref="IServiceProvider"/>.</param>
47+
public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider, IServiceProvider services) : this(next, policyProvider)
48+
{
49+
ArgumentNullException.ThrowIfNull(services);
50+
51+
if (_policyProvider.CanCachePolicy)
52+
{
53+
_policyCache = services.GetService<AuthorizationPolicyCache>();
54+
_canCache = _policyCache != null;
55+
}
3656
}
3757

3858
/// <summary>
@@ -47,20 +67,36 @@ public async Task Invoke(HttpContext context)
4767
}
4868

4969
var endpoint = context.GetEndpoint();
50-
5170
if (endpoint != null)
5271
{
5372
// EndpointRoutingMiddleware uses this flag to check if the Authorization middleware processed auth metadata on the endpoint.
5473
// The Authorization middleware can only make this claim if it observes an actual endpoint.
5574
context.Items[AuthorizationMiddlewareInvokedWithEndpointKey] = AuthorizationMiddlewareWithEndpointInvokedValue;
5675
}
5776

58-
// IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
59-
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
77+
// Use the computed policy for this endpoint if we can
78+
AuthorizationPolicy? policy = null;
79+
var canCachePolicy = _canCache && endpoint != null;
80+
if (canCachePolicy)
81+
{
82+
policy = _policyCache!.Lookup(endpoint!);
83+
}
84+
85+
if (policy == null)
86+
{
87+
// IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
88+
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
6089

61-
var policies = endpoint?.Metadata.GetOrderedMetadata<AuthorizationPolicy>() ?? Array.Empty<AuthorizationPolicy>();
90+
var policies = endpoint?.Metadata.GetOrderedMetadata<AuthorizationPolicy>() ?? Array.Empty<AuthorizationPolicy>();
6291

63-
var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData, policies);
92+
policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData, policies);
93+
94+
// Cache the computed policy
95+
if (policy != null && canCachePolicy)
96+
{
97+
_policyCache!.Store(endpoint!, policy);
98+
}
99+
}
64100

65101
if (policy == null)
66102
{
@@ -108,4 +144,5 @@ public async Task Invoke(HttpContext context)
108144
var authorizationMiddlewareResultHandler = context.RequestServices.GetRequiredService<IAuthorizationMiddlewareResultHandler>();
109145
await authorizationMiddlewareResultHandler.HandleAsync(_next, context, policy, authorizeResult);
110146
}
147+
111148
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Routing;
7+
8+
namespace Microsoft.AspNetCore.Authorization.Policy;
9+
10+
internal sealed class AuthorizationPolicyCache : IDisposable
11+
{
12+
// Caches AuthorizationPolicy instances
13+
private readonly DataSourceDependentCache<ConcurrentDictionary<Endpoint, AuthorizationPolicy>> _policyCache;
14+
15+
public AuthorizationPolicyCache(EndpointDataSource dataSource)
16+
{
17+
// We cache AuthorizationPolicy instances per-Endpoint for performance, but we want to wipe out
18+
// that cache if the endpoints change so that we don't allow unbounded memory growth.
19+
_policyCache = new DataSourceDependentCache<ConcurrentDictionary<Endpoint, AuthorizationPolicy>>(dataSource, (_) =>
20+
{
21+
// We don't eagerly fill this cache because there's no real reason to.
22+
return new ConcurrentDictionary<Endpoint, AuthorizationPolicy>();
23+
});
24+
_policyCache.EnsureInitialized();
25+
}
26+
27+
public AuthorizationPolicy? Lookup(Endpoint endpoint)
28+
{
29+
_policyCache.Value!.TryGetValue(endpoint, out var policy);
30+
return policy;
31+
}
32+
33+
public void Store(Endpoint endpoint, AuthorizationPolicy policy)
34+
{
35+
_policyCache.Value![endpoint] = policy;
36+
}
37+
38+
public void Dispose()
39+
{
40+
_policyCache.Dispose();
41+
}
42+
}

src/Security/Authorization/Policy/src/Microsoft.AspNetCore.Authorization.Policy.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<ItemGroup>
1414
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
15+
<Compile Include="..\..\..\..\Http\Routing\src\DataSourceDependentCache.cs" Link="DataSourceDependentCache.cs" />
1516
</ItemGroup>
1617

1718
<ItemGroup>

src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public static IServiceCollection AddAuthorization(this IServiceCollection servic
5252

5353
services.AddAuthorizationCore();
5454
services.AddAuthorizationPolicyEvaluator();
55+
services.AddSingleton<AuthorizationPolicyCache>();
5556
return services;
5657
}
5758

@@ -70,6 +71,7 @@ public static IServiceCollection AddAuthorization(this IServiceCollection servic
7071

7172
services.AddAuthorizationCore(configure);
7273
services.AddAuthorizationPolicyEvaluator();
74+
services.AddSingleton<AuthorizationPolicyCache>();
7375
return services;
7476
}
7577
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.AuthorizationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, System.IServiceProvider! services) -> void
23
static Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization<TBuilder>(this TBuilder builder, Microsoft.AspNetCore.Authorization.AuthorizationPolicy! policy) -> TBuilder
34
static Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization<TBuilder>(this TBuilder builder, System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!>! configurePolicy) -> TBuilder
45
static Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions.AddAuthorizationBuilder(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Authorization.AuthorizationBuilder!

0 commit comments

Comments
 (0)