Skip to content

Commit 1cec21f

Browse files
committed
PR feedback for authorization metrics
1 parent 6c69508 commit 1cec21f

File tree

10 files changed

+211
-98
lines changed

10 files changed

+211
-98
lines changed

src/Http/Authentication.Core/src/AuthenticationMetrics.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace Microsoft.AspNetCore.Authentication;
1010

11-
internal sealed class AuthenticationMetrics : IDisposable
11+
internal sealed class AuthenticationMetrics
1212
{
1313
public const string MeterName = "Microsoft.AspNetCore.Authentication";
1414

@@ -245,9 +245,4 @@ private static void AddErrorTag(ref TagList tags, Exception exception)
245245
{
246246
tags.Add("error.type", exception.GetType().FullName);
247247
}
248-
249-
public void Dispose()
250-
{
251-
_meter.Dispose();
252-
}
253248
}

src/Security/Authentication/test/AuthenticationMetricsTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ public async Task Authenticate_ExceptionThrownInHandler()
111111
using var authenticationRequestsCollector = new MetricCollector<double>(meterFactory, AuthenticationMetrics.MeterName, "aspnetcore.authentication.request.duration");
112112

113113
// Act
114-
await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.AuthenticateAsync(httpContext, scheme: "custom"));
114+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.AuthenticateAsync(httpContext, scheme: "custom"));
115115

116116
// Assert
117+
Assert.Equal("An error occurred during authentication", ex.Message);
117118
Assert.Equal(AuthenticationMetrics.MeterName, meter.Name);
118119
Assert.Null(meter.Version);
119120

@@ -162,9 +163,10 @@ public async Task Challenge_ExceptionThrownInHandler()
162163
using var challengesCollector = new MetricCollector<long>(meterFactory, AuthenticationMetrics.MeterName, "aspnetcore.authentication.challenges");
163164

164165
// Act
165-
await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.ChallengeAsync(httpContext, scheme: "custom", properties: null));
166+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.ChallengeAsync(httpContext, scheme: "custom", properties: null));
166167

167168
// Assert
169+
Assert.Equal("An error occurred during challenge", ex.Message);
168170
Assert.Equal(AuthenticationMetrics.MeterName, meter.Name);
169171
Assert.Null(meter.Version);
170172

@@ -212,9 +214,10 @@ public async Task Forbid_ExceptionThrownInHandler()
212214
using var forbidsCollector = new MetricCollector<long>(meterFactory, AuthenticationMetrics.MeterName, "aspnetcore.authentication.forbids");
213215

214216
// Act
215-
await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.ForbidAsync(httpContext, scheme: "custom", properties: null));
217+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.ForbidAsync(httpContext, scheme: "custom", properties: null));
216218

217219
// Assert
220+
Assert.Equal("An error occurred during forbid", ex.Message);
218221
Assert.Equal(AuthenticationMetrics.MeterName, meter.Name);
219222
Assert.Null(meter.Version);
220223

@@ -262,9 +265,10 @@ public async Task SignIn_ExceptionThrownInHandler()
262265
using var signInsCollector = new MetricCollector<long>(meterFactory, AuthenticationMetrics.MeterName, "aspnetcore.authentication.sign_ins");
263266

264267
// Act
265-
await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.SignInAsync(httpContext, scheme: "custom", new ClaimsPrincipal(), properties: null));
268+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.SignInAsync(httpContext, scheme: "custom", new ClaimsPrincipal(), properties: null));
266269

267270
// Assert
271+
Assert.Equal("An error occurred during sign in", ex.Message);
268272
Assert.Equal(AuthenticationMetrics.MeterName, meter.Name);
269273
Assert.Null(meter.Version);
270274

@@ -312,9 +316,10 @@ public async Task SignOut_ExceptionThrownInHandler()
312316
using var signOutsCollector = new MetricCollector<long>(meterFactory, AuthenticationMetrics.MeterName, "aspnetcore.authentication.sign_outs");
313317

314318
// Act
315-
await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.SignOutAsync(httpContext, scheme: "custom", properties: null));
319+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => authenticationService.SignOutAsync(httpContext, scheme: "custom", properties: null));
316320

317321
// Assert
322+
Assert.Equal("An error occurred during sign out", ex.Message);
318323
Assert.Equal(AuthenticationMetrics.MeterName, meter.Name);
319324
Assert.Null(meter.Version);
320325

src/Security/Authorization/Core/src/AuthorizationMetrics.cs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,73 @@
66
using System.Diagnostics;
77
using System.Diagnostics.Metrics;
88
using System.Linq;
9+
using System.Runtime.CompilerServices;
910
using System.Text;
1011
using System.Threading.Tasks;
1112

1213
namespace Microsoft.AspNetCore.Authorization;
1314

14-
internal sealed class AuthorizationMetrics : IDisposable
15+
internal sealed class AuthorizationMetrics
1516
{
1617
public const string MeterName = "Microsoft.AspNetCore.Authorization";
1718

1819
private readonly Meter _meter;
19-
private readonly Counter<long> _authorizeCount;
20+
private readonly Counter<long> _authorizedRequestCount;
2021

2122
public AuthorizationMetrics(IMeterFactory meterFactory)
2223
{
2324
_meter = meterFactory.Create(MeterName);
2425

25-
_authorizeCount = _meter.CreateCounter<long>(
26-
"aspnetcore.authorization.authorized_requests",
26+
_authorizedRequestCount = _meter.CreateCounter<long>(
27+
"aspnetcore.authorization.requests",
2728
unit: "{request}",
28-
description: "The total number of requests requiring authorization");
29+
description: "The total number of requests for which authorization was attempted");
2930
}
3031

31-
public void AuthorizedRequest(string? policyName, AuthorizationResult result)
32+
public void AuthorizedRequestSucceeded(string? policyName, AuthorizationResult result)
3233
{
33-
if (_authorizeCount.Enabled)
34+
if (_authorizedRequestCount.Enabled)
3435
{
35-
var resultTagValue = result.Succeeded ? "success" : "failure";
36+
AuthorizedRequestSucceededCore(policyName, result);
37+
}
38+
}
39+
40+
[MethodImpl(MethodImplOptions.NoInlining)]
41+
private void AuthorizedRequestSucceededCore(string? policyName, AuthorizationResult result)
42+
{
43+
var tags = new TagList();
44+
TryAddPolicyTag(ref tags, policyName);
3645

37-
_authorizeCount.Add(1, [
38-
new("aspnetcore.authorization.policy", policyName),
39-
new("aspnetcore.authorization.result", resultTagValue),
40-
]);
46+
var resultTagValue = result.Succeeded ? "success" : "failure";
47+
tags.Add("aspnetcore.authorization.result", resultTagValue);
48+
49+
_authorizedRequestCount.Add(1, tags);
50+
}
51+
52+
public void AuthorizedRequestFailed(string? policyName, Exception exception)
53+
{
54+
if (_authorizedRequestCount.Enabled)
55+
{
56+
AuthorizedRequestFailedCore(policyName, exception);
4157
}
4258
}
4359

44-
public void Dispose()
60+
[MethodImpl(MethodImplOptions.NoInlining)]
61+
private void AuthorizedRequestFailedCore(string? policyName, Exception exception)
62+
{
63+
var tags = new TagList();
64+
TryAddPolicyTag(ref tags, policyName);
65+
66+
tags.Add("error.type", exception.GetType().FullName);
67+
68+
_authorizedRequestCount.Add(1, tags);
69+
}
70+
71+
private static void TryAddPolicyTag(ref TagList tags, string? policyName)
4572
{
46-
_meter.Dispose();
73+
if (policyName is not null)
74+
{
75+
tags.Add("aspnetcore.authorization.policy", policyName);
76+
}
4777
}
4878
}

src/Security/Authorization/Core/src/AuthorizationServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static IServiceCollection AddAuthorizationCore(this IServiceCollection se
3030
services.AddMetrics();
3131

3232
services.TryAdd(ServiceDescriptor.Singleton<AuthorizationMetrics, AuthorizationMetrics>());
33-
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
33+
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationServiceImpl>());
3434
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
3535
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());
3636
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>());

src/Security/Authorization/Core/src/DefaultAuthorizationService.cs

Lines changed: 25 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Security.Claims;
77
using System.Threading.Tasks;
88
using Microsoft.AspNetCore.Shared;
9-
using Microsoft.Extensions.DependencyInjection;
109
using Microsoft.Extensions.Logging;
1110
using Microsoft.Extensions.Options;
1211

@@ -18,7 +17,6 @@ namespace Microsoft.AspNetCore.Authorization;
1817
public class DefaultAuthorizationService : IAuthorizationService
1918
{
2019
private readonly AuthorizationOptions _options;
21-
private readonly AuthorizationMetrics? _metrics;
2220
private readonly IAuthorizationHandlerContextFactory _contextFactory;
2321
private readonly IAuthorizationHandlerProvider _handlers;
2422
private readonly IAuthorizationEvaluator _evaluator;
@@ -34,35 +32,7 @@ public class DefaultAuthorizationService : IAuthorizationService
3432
/// <param name="contextFactory">The <see cref="IAuthorizationHandlerContextFactory"/> used to create the context to handle the authorization.</param>
3533
/// <param name="evaluator">The <see cref="IAuthorizationEvaluator"/> used to determine if authorization was successful.</param>
3634
/// <param name="options">The <see cref="AuthorizationOptions"/> used.</param>
37-
public DefaultAuthorizationService(
38-
IAuthorizationPolicyProvider policyProvider,
39-
IAuthorizationHandlerProvider handlers,
40-
ILogger<DefaultAuthorizationService> logger,
41-
IAuthorizationHandlerContextFactory contextFactory,
42-
IAuthorizationEvaluator evaluator,
43-
IOptions<AuthorizationOptions> options)
44-
: this(policyProvider, handlers, logger, contextFactory, evaluator, options, services: null)
45-
{
46-
}
47-
48-
/// <summary>
49-
/// Creates a new instance of <see cref="DefaultAuthorizationService"/>.
50-
/// </summary>
51-
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/> used to provide policies.</param>
52-
/// <param name="handlers">The handlers used to fulfill <see cref="IAuthorizationRequirement"/>s.</param>
53-
/// <param name="logger">The logger used to log messages, warnings and errors.</param>
54-
/// <param name="contextFactory">The <see cref="IAuthorizationHandlerContextFactory"/> used to create the context to handle the authorization.</param>
55-
/// <param name="evaluator">The <see cref="IAuthorizationEvaluator"/> used to determine if authorization was successful.</param>
56-
/// <param name="options">The <see cref="AuthorizationOptions"/> used.</param>
57-
/// <param name="services">The <see cref="IServiceProvider"/> used to provide other services.</param>
58-
public DefaultAuthorizationService(
59-
IAuthorizationPolicyProvider policyProvider,
60-
IAuthorizationHandlerProvider handlers,
61-
ILogger<DefaultAuthorizationService> logger,
62-
IAuthorizationHandlerContextFactory contextFactory,
63-
IAuthorizationEvaluator evaluator,
64-
IOptions<AuthorizationOptions> options,
65-
IServiceProvider? services)
35+
public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger<DefaultAuthorizationService> logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions<AuthorizationOptions> options)
6636
{
6737
ArgumentNullThrowHelper.ThrowIfNull(options);
6838
ArgumentNullThrowHelper.ThrowIfNull(policyProvider);
@@ -77,7 +47,6 @@ public DefaultAuthorizationService(
7747
_logger = logger;
7848
_evaluator = evaluator;
7949
_contextFactory = contextFactory;
80-
_metrics = services?.GetService<AuthorizationMetrics>();
8150
}
8251

8352
/// <summary>
@@ -90,33 +59,7 @@ public DefaultAuthorizationService(
9059
/// A flag indicating whether authorization has succeeded.
9160
/// This value is <c>true</c> when the user fulfills the policy, otherwise <c>false</c>.
9261
/// </returns>
93-
public virtual Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
94-
=> AuthorizeCoreAsync(user, resource, requirements, policyName: null);
95-
96-
/// <summary>
97-
/// Checks if a user meets a specific authorization policy.
98-
/// </summary>
99-
/// <param name="user">The user to check the policy against.</param>
100-
/// <param name="resource">The resource the policy should be checked with.</param>
101-
/// <param name="policyName">The name of the policy to check against a specific context.</param>
102-
/// <returns>
103-
/// A flag indicating whether authorization has succeeded.
104-
/// This value is <c>true</c> when the user fulfills the policy otherwise <c>false</c>.
105-
/// </returns>
106-
public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName)
107-
{
108-
ArgumentNullThrowHelper.ThrowIfNull(policyName);
109-
110-
var policy = await _policyProvider.GetPolicyAsync(policyName).ConfigureAwait(false);
111-
if (policy == null)
112-
{
113-
throw new InvalidOperationException($"No policy found: {policyName}.");
114-
}
115-
116-
return await AuthorizeCoreAsync(user, resource, policy.Requirements, policyName).ConfigureAwait(false);
117-
}
118-
119-
private async Task<AuthorizationResult> AuthorizeCoreAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements, string? policyName)
62+
public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
12063
{
12164
ArgumentNullThrowHelper.ThrowIfNull(requirements);
12265

@@ -132,9 +75,6 @@ private async Task<AuthorizationResult> AuthorizeCoreAsync(ClaimsPrincipal user,
13275
}
13376

13477
var result = _evaluator.Evaluate(authContext);
135-
136-
_metrics?.AuthorizedRequest(policyName, result);
137-
13878
if (result.Succeeded)
13979
{
14080
_logger.UserAuthorizationSucceeded();
@@ -143,7 +83,29 @@ private async Task<AuthorizationResult> AuthorizeCoreAsync(ClaimsPrincipal user,
14383
{
14484
_logger.UserAuthorizationFailed(result.Failure);
14585
}
146-
14786
return result;
14887
}
88+
89+
/// <summary>
90+
/// Checks if a user meets a specific authorization policy.
91+
/// </summary>
92+
/// <param name="user">The user to check the policy against.</param>
93+
/// <param name="resource">The resource the policy should be checked with.</param>
94+
/// <param name="policyName">The name of the policy to check against a specific context.</param>
95+
/// <returns>
96+
/// A flag indicating whether authorization has succeeded.
97+
/// This value is <c>true</c> when the user fulfills the policy otherwise <c>false</c>.
98+
/// </returns>
99+
public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName)
100+
{
101+
var policy = await GetPolicyAsync(policyName).ConfigureAwait(false);
102+
return await this.AuthorizeAsync(user, resource, policy).ConfigureAwait(false);
103+
}
104+
105+
// For use in DefaultAuthorizationServiceImpl.
106+
private protected async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
107+
{
108+
ArgumentNullThrowHelper.ThrowIfNull(policyName);
109+
return await _policyProvider.GetPolicyAsync(policyName).ConfigureAwait(false) ?? throw new InvalidOperationException($"No policy found: {policyName}.");
110+
}
149111
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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;
5+
using System.Collections.Generic;
6+
using System.Security.Claims;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Shared;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.AspNetCore.Authorization;
13+
14+
internal sealed class DefaultAuthorizationServiceImpl(
15+
IAuthorizationPolicyProvider policyProvider,
16+
IAuthorizationHandlerProvider handlers,
17+
ILogger<DefaultAuthorizationService> logger,
18+
IAuthorizationHandlerContextFactory contextFactory,
19+
IAuthorizationEvaluator evaluator,
20+
IOptions<AuthorizationOptions> options,
21+
AuthorizationMetrics metrics)
22+
: DefaultAuthorizationService(policyProvider, handlers, logger, contextFactory, evaluator, options)
23+
{
24+
public override async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
25+
{
26+
AuthorizationResult result;
27+
try
28+
{
29+
result = await base.AuthorizeAsync(user, resource, requirements).ConfigureAwait(false);
30+
}
31+
catch (Exception ex)
32+
{
33+
metrics.AuthorizedRequestFailed(policyName: null, ex);
34+
throw;
35+
}
36+
37+
metrics.AuthorizedRequestSucceeded(policyName: null, result);
38+
return result;
39+
}
40+
41+
public override async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName)
42+
{
43+
AuthorizationResult result;
44+
try
45+
{
46+
var policy = await GetPolicyAsync(policyName).ConfigureAwait(false);
47+
48+
// Note that we deliberately call the base method of the other overload here.
49+
// This is because the base implementation for this overload dispatches to the other overload,
50+
// which would cause metrics to be recorded twice.
51+
result = await base.AuthorizeAsync(user, resource, policy.Requirements).ConfigureAwait(false);
52+
}
53+
catch (Exception ex)
54+
{
55+
metrics.AuthorizedRequestFailed(policyName, ex);
56+
throw;
57+
}
58+
59+
metrics.AuthorizedRequestSucceeded(policyName, result);
60+
return result;
61+
}
62+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService.DefaultAuthorizationService(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider! handlers, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Authorization.DefaultAuthorizationService!>! logger, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory! contextFactory, Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator! evaluator, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions!>! options, System.IServiceProvider? services) -> void
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService.DefaultAuthorizationService(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider! handlers, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Authorization.DefaultAuthorizationService!>! logger, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory! contextFactory, Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator! evaluator, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions!>! options, System.IServiceProvider? services) -> void
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
#nullable enable
2-
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService.DefaultAuthorizationService(Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider! policyProvider, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider! handlers, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Authorization.DefaultAuthorizationService!>! logger, Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory! contextFactory, Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator! evaluator, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions!>! options, System.IServiceProvider? services) -> void

0 commit comments

Comments
 (0)