Skip to content

Commit 0fa9b52

Browse files
tobias-tenglermichaelstaib
authored andcommitted
Only cache authorization policies if permitted by policy provider (#7705)
1 parent 0c91a72 commit 0fa9b52

File tree

3 files changed

+211
-42
lines changed

3 files changed

+211
-42
lines changed

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization/AuthorizationPolicyCache.cs

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,21 @@
44

55
namespace HotChocolate.AspNetCore.Authorization;
66

7-
internal sealed class AuthorizationPolicyCache(IAuthorizationPolicyProvider policyProvider)
7+
internal sealed class AuthorizationPolicyCache
88
{
9-
private readonly ConcurrentDictionary<string, Task<AuthorizationPolicy>> _cache = new();
9+
private readonly ConcurrentDictionary<string, AuthorizationPolicy> _cache = new();
1010

11-
public Task<AuthorizationPolicy> GetOrCreatePolicyAsync(AuthorizeDirective directive)
11+
public AuthorizationPolicy? LookupPolicy(AuthorizeDirective directive)
1212
{
1313
var cacheKey = directive.GetPolicyCacheKey();
1414

15-
return _cache.GetOrAdd(cacheKey, _ => BuildAuthorizationPolicy(directive.Policy, directive.Roles));
15+
return _cache.GetValueOrDefault(cacheKey);
1616
}
1717

18-
private async Task<AuthorizationPolicy> BuildAuthorizationPolicy(
19-
string? policyName,
20-
IReadOnlyList<string>? roles)
18+
public void CachePolicy(AuthorizeDirective directive, AuthorizationPolicy policy)
2119
{
22-
var policyBuilder = new AuthorizationPolicyBuilder();
23-
24-
if (!string.IsNullOrWhiteSpace(policyName))
25-
{
26-
var policy = await policyProvider.GetPolicyAsync(policyName).ConfigureAwait(false);
27-
28-
if (policy is not null)
29-
{
30-
policyBuilder = policyBuilder.Combine(policy);
31-
}
32-
else
33-
{
34-
throw new MissingAuthorizationPolicyException(policyName);
35-
}
36-
}
37-
else
38-
{
39-
var defaultPolicy = await policyProvider.GetDefaultPolicyAsync().ConfigureAwait(false);
40-
41-
policyBuilder = policyBuilder.Combine(defaultPolicy);
42-
}
43-
44-
if (roles is not null)
45-
{
46-
policyBuilder = policyBuilder.RequireRole(roles);
47-
}
20+
var cacheKey = directive.GetPolicyCacheKey();
4821

49-
return policyBuilder.Build();
22+
_cache.TryAdd(cacheKey, policy);
5023
}
5124
}

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization/DefaultAuthorizationHandler.cs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,39 @@ namespace HotChocolate.AspNetCore.Authorization;
1212
internal sealed class DefaultAuthorizationHandler : IAuthorizationHandler
1313
{
1414
private readonly IAuthorizationService _authSvc;
15-
private readonly AuthorizationPolicyCache _policyCache;
15+
private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
16+
private readonly AuthorizationPolicyCache _authorizationPolicyCache;
17+
private readonly bool _canCachePolicies;
1618

1719
/// <summary>
1820
/// Initializes a new instance <see cref="DefaultAuthorizationHandler"/>.
1921
/// </summary>
2022
/// <param name="authorizationService">
2123
/// The authorization service.
2224
/// </param>
23-
/// <param name="policyCache">
25+
/// <param name="authorizationPolicyProvider">
26+
/// The authorization policy provider.
27+
/// </param>
28+
/// <param name="authorizationPolicyCache">
2429
/// The authorization policy cache.
2530
/// </param>
2631
/// <exception cref="ArgumentNullException">
2732
/// <paramref name="authorizationService"/> is <c>null</c>.
28-
/// <paramref name="policyCache"/> is <c>null</c>.
33+
/// <paramref name="authorizationPolicyCache"/> is <c>null</c>.
2934
/// </exception>
3035
public DefaultAuthorizationHandler(
3136
IAuthorizationService authorizationService,
32-
AuthorizationPolicyCache policyCache)
37+
IAuthorizationPolicyProvider authorizationPolicyProvider,
38+
AuthorizationPolicyCache authorizationPolicyCache)
3339
{
3440
_authSvc = authorizationService ??
3541
throw new ArgumentNullException(nameof(authorizationService));
36-
_policyCache = policyCache ??
37-
throw new ArgumentNullException(nameof(policyCache));
42+
_authorizationPolicyProvider = authorizationPolicyProvider ??
43+
throw new ArgumentNullException(nameof(authorizationPolicyProvider));
44+
_authorizationPolicyCache = authorizationPolicyCache ??
45+
throw new ArgumentNullException(nameof(authorizationPolicyCache));
46+
47+
_canCachePolicies = _authorizationPolicyProvider.AllowsCachingPolicies;
3848
}
3949

4050
/// <summary>
@@ -123,9 +133,24 @@ private async ValueTask<AuthorizeResult> AuthorizeAsync(
123133
{
124134
try
125135
{
126-
var combinedPolicy = await _policyCache.GetOrCreatePolicyAsync(directive);
136+
AuthorizationPolicy? authorizationPolicy = null;
137+
138+
if (_canCachePolicies)
139+
{
140+
authorizationPolicy = _authorizationPolicyCache.LookupPolicy(directive);
141+
}
142+
143+
if (authorizationPolicy is null)
144+
{
145+
authorizationPolicy = await BuildAuthorizationPolicy(directive.Policy, directive.Roles);
127146

128-
var result = await _authSvc.AuthorizeAsync(user, context, combinedPolicy).ConfigureAwait(false);
147+
if (_canCachePolicies)
148+
{
149+
_authorizationPolicyCache.CachePolicy(directive, authorizationPolicy);
150+
}
151+
}
152+
153+
var result = await _authSvc.AuthorizeAsync(user, context, authorizationPolicy).ConfigureAwait(false);
129154

130155
return result.Succeeded
131156
? AuthorizeResult.Allowed
@@ -137,6 +162,40 @@ private async ValueTask<AuthorizeResult> AuthorizeAsync(
137162
}
138163
}
139164

165+
private async Task<AuthorizationPolicy> BuildAuthorizationPolicy(
166+
string? policyName,
167+
IReadOnlyList<string>? roles)
168+
{
169+
var policyBuilder = new AuthorizationPolicyBuilder();
170+
171+
if (!string.IsNullOrWhiteSpace(policyName))
172+
{
173+
var policy = await _authorizationPolicyProvider.GetPolicyAsync(policyName).ConfigureAwait(false);
174+
175+
if (policy is not null)
176+
{
177+
policyBuilder = policyBuilder.Combine(policy);
178+
}
179+
else
180+
{
181+
throw new MissingAuthorizationPolicyException(policyName);
182+
}
183+
}
184+
else
185+
{
186+
var defaultPolicy = await _authorizationPolicyProvider.GetDefaultPolicyAsync().ConfigureAwait(false);
187+
188+
policyBuilder = policyBuilder.Combine(defaultPolicy);
189+
}
190+
191+
if (roles is not null)
192+
{
193+
policyBuilder = policyBuilder.RequireRole(roles);
194+
}
195+
196+
return policyBuilder.Build();
197+
}
198+
140199
private static UserState GetUserState(IDictionary<string, object?> contextData)
141200
{
142201
if (contextData.TryGetValue(WellKnownContextData.UserState, out var value) &&
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Net;
2+
using System.Security.Claims;
3+
using HotChocolate.AspNetCore.Tests.Utilities;
4+
using HotChocolate.Execution.Configuration;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Authorization.Infrastructure;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.TestHost;
10+
using Microsoft.Extensions.DependencyInjection;
11+
12+
namespace HotChocolate.AspNetCore.Authorization;
13+
14+
public class AuthorizationPolicyProviderTess(TestServerFactory serverFactory) : ServerTestBase(serverFactory)
15+
{
16+
[Fact]
17+
public async Task Policies_Are_Cached_If_PolicyProvider_Allows_Caching()
18+
{
19+
// arrange
20+
var policyProvider = new CustomAuthorizationPolicyProvider(allowsCaching: true);
21+
22+
var server = CreateTestServer(
23+
builder =>
24+
{
25+
builder.Services.AddSingleton<IAuthorizationPolicyProvider>(_ => policyProvider);
26+
27+
builder
28+
.AddQueryType<Query>()
29+
.AddAuthorization();
30+
},
31+
context =>
32+
{
33+
var identity = new ClaimsIdentity("testauth");
34+
context.User = new ClaimsPrincipal(identity);
35+
});
36+
37+
// act
38+
var result1 =
39+
await server.PostAsync(new ClientQueryRequest { Query = "{ bar }", });
40+
var result2 =
41+
await server.PostAsync(new ClientQueryRequest { Query = "{ bar }", });
42+
43+
// assert
44+
Assert.Equal(HttpStatusCode.OK, result1.StatusCode);
45+
Assert.Null(result1.Errors);
46+
Assert.Equal(HttpStatusCode.OK, result2.StatusCode);
47+
Assert.Null(result2.Errors);
48+
Assert.Equal(1, policyProvider.InvocationsOfGetPolicyAsync);
49+
}
50+
51+
[Fact]
52+
public async Task Policies_Are_Not_Cached_If_PolicyProvider_Disallows_Caching()
53+
{
54+
// arrange
55+
var policyProvider = new CustomAuthorizationPolicyProvider(allowsCaching: false);
56+
57+
var server = CreateTestServer(
58+
builder =>
59+
{
60+
builder.Services.AddSingleton<IAuthorizationPolicyProvider>(_ => policyProvider);
61+
62+
builder
63+
.AddQueryType<Query>()
64+
.AddAuthorization();
65+
},
66+
context =>
67+
{
68+
var identity = new ClaimsIdentity("testauth");
69+
context.User = new ClaimsPrincipal(identity);
70+
});
71+
72+
// act
73+
var result1 =
74+
await server.PostAsync(new ClientQueryRequest { Query = "{ bar }", });
75+
var result2 =
76+
await server.PostAsync(new ClientQueryRequest { Query = "{ bar }", });
77+
78+
// assert
79+
Assert.Equal(HttpStatusCode.OK, result1.StatusCode);
80+
Assert.Null(result1.Errors);
81+
Assert.Equal(HttpStatusCode.OK, result2.StatusCode);
82+
Assert.Null(result2.Errors);
83+
Assert.Equal(2, policyProvider.InvocationsOfGetPolicyAsync);
84+
}
85+
86+
public class Query
87+
{
88+
[HotChocolate.Authorization.Authorize(Policy = "policy")]
89+
public string Bar() => "bar";
90+
}
91+
92+
private TestServer CreateTestServer(
93+
Action<IRequestExecutorBuilder> build,
94+
Action<HttpContext> configureUser)
95+
{
96+
return ServerFactory.Create(
97+
services =>
98+
{
99+
build(services
100+
.AddRouting()
101+
.AddGraphQLServer()
102+
.AddHttpRequestInterceptor(
103+
(context, requestExecutor, requestBuilder, cancellationToken) =>
104+
{
105+
configureUser(context);
106+
return default;
107+
}));
108+
},
109+
app =>
110+
{
111+
app.UseRouting();
112+
app.UseEndpoints(b => b.MapGraphQL());
113+
});
114+
}
115+
116+
public class CustomAuthorizationPolicyProvider(bool allowsCaching) : IAuthorizationPolicyProvider
117+
{
118+
public int InvocationsOfGetPolicyAsync { get; private set; }
119+
120+
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
121+
{
122+
InvocationsOfGetPolicyAsync++;
123+
124+
var policy = new AuthorizationPolicyBuilder()
125+
.AddRequirements(new DenyAnonymousAuthorizationRequirement())
126+
.Build();
127+
128+
return Task.FromResult<AuthorizationPolicy?>(policy);
129+
}
130+
131+
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => throw new NotImplementedException();
132+
133+
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => throw new NotImplementedException();
134+
135+
public virtual bool AllowsCachingPolicies => allowsCaching;
136+
}
137+
}

0 commit comments

Comments
 (0)