Skip to content

Commit d6309e2

Browse files
committed
Add authorization metrics
1 parent e50ed9c commit d6309e2

File tree

9 files changed

+265
-24
lines changed

9 files changed

+265
-24
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.Diagnostics;
7+
using System.Diagnostics.Metrics;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.AspNetCore.Authorization;
13+
14+
internal sealed class AuthorizationMetrics : IDisposable
15+
{
16+
public const string MeterName = "Microsoft.AspNetCore.Authorization";
17+
18+
private readonly Meter _meter;
19+
private readonly Counter<long> _authorizeCount;
20+
21+
public AuthorizationMetrics(IMeterFactory meterFactory)
22+
{
23+
_meter = meterFactory.Create(MeterName);
24+
25+
_authorizeCount = _meter.CreateCounter<long>(
26+
"aspnetcore.authorization.authorized_requests",
27+
unit: "{request}",
28+
description: "The total number of requests requiring authorization");
29+
}
30+
31+
public void AuthorizedRequest(string? policyName, AuthorizationResult result)
32+
{
33+
if (_authorizeCount.Enabled)
34+
{
35+
var resultTagValue = result.Succeeded ? "success" : "failure";
36+
37+
_authorizeCount.Add(1, [
38+
new("aspnetcore.authorization.policy", policyName),
39+
new("aspnetcore.authorization.result", resultTagValue),
40+
]);
41+
}
42+
}
43+
44+
public void Dispose()
45+
{
46+
_meter.Dispose();
47+
}
48+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public static IServiceCollection AddAuthorizationCore(this IServiceCollection se
2727
// aren't included by default.
2828
services.AddOptions();
2929

30+
services.AddMetrics();
31+
32+
services.TryAdd(ServiceDescriptor.Singleton<AuthorizationMetrics, AuthorizationMetrics>());
3033
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
3134
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
3235
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());

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

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

@@ -17,6 +18,7 @@ namespace Microsoft.AspNetCore.Authorization;
1718
public class DefaultAuthorizationService : IAuthorizationService
1819
{
1920
private readonly AuthorizationOptions _options;
21+
private readonly AuthorizationMetrics? _metrics;
2022
private readonly IAuthorizationHandlerContextFactory _contextFactory;
2123
private readonly IAuthorizationHandlerProvider _handlers;
2224
private readonly IAuthorizationEvaluator _evaluator;
@@ -32,7 +34,35 @@ public class DefaultAuthorizationService : IAuthorizationService
3234
/// <param name="contextFactory">The <see cref="IAuthorizationHandlerContextFactory"/> used to create the context to handle the authorization.</param>
3335
/// <param name="evaluator">The <see cref="IAuthorizationEvaluator"/> used to determine if authorization was successful.</param>
3436
/// <param name="options">The <see cref="AuthorizationOptions"/> used.</param>
35-
public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger<DefaultAuthorizationService> logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions<AuthorizationOptions> options)
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)
3666
{
3767
ArgumentNullThrowHelper.ThrowIfNull(options);
3868
ArgumentNullThrowHelper.ThrowIfNull(policyProvider);
@@ -47,6 +77,7 @@ public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider,
4777
_logger = logger;
4878
_evaluator = evaluator;
4979
_contextFactory = contextFactory;
80+
_metrics = services?.GetService<AuthorizationMetrics>();
5081
}
5182

5283
/// <summary>
@@ -59,7 +90,33 @@ public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider,
5990
/// A flag indicating whether authorization has succeeded.
6091
/// This value is <c>true</c> when the user fulfills the policy, otherwise <c>false</c>.
6192
/// </returns>
62-
public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements)
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)
63120
{
64121
ArgumentNullThrowHelper.ThrowIfNull(requirements);
65122

@@ -75,6 +132,9 @@ public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal us
75132
}
76133

77134
var result = _evaluator.Evaluate(authContext);
135+
136+
_metrics?.AuthorizedRequest(policyName, result);
137+
78138
if (result.Succeeded)
79139
{
80140
_logger.UserAuthorizationSucceeded();
@@ -83,28 +143,7 @@ public virtual async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal us
83143
{
84144
_logger.UserAuthorizationFailed(result.Failure);
85145
}
86-
return result;
87-
}
88146

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-
ArgumentNullThrowHelper.ThrowIfNull(policyName);
102-
103-
var policy = await _policyProvider.GetPolicyAsync(policyName).ConfigureAwait(false);
104-
if (policy == null)
105-
{
106-
throw new InvalidOperationException($"No policy found: {policyName}.");
107-
}
108-
return await this.AuthorizeAsync(user, resource, policy).ConfigureAwait(false);
147+
return result;
109148
}
110149
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Microsoft.AspNetCore.Authorization.AuthorizeAttribute</Description>
1717
<ItemGroup>
1818
<Reference Include="Microsoft.AspNetCore.Metadata" />
1919
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
20+
<Reference Include="Microsoft.Extensions.Diagnostics" />
2021
<Reference Include="Microsoft.Extensions.Options" />
2122
</ItemGroup>
2223

@@ -31,4 +32,8 @@ Microsoft.AspNetCore.Authorization.AuthorizeAttribute</Description>
3132
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
3233
</ItemGroup>
3334

35+
<ItemGroup>
36+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Authorization.Test" />
37+
</ItemGroup>
38+
3439
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.Security.Claims;
5+
using Microsoft.AspNetCore.InternalTesting;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
8+
9+
namespace Microsoft.AspNetCore.Authorization.Test;
10+
11+
public class AuthorizationMetricsTest
12+
{
13+
[Fact]
14+
public async Task Authorize_WithPolicyName_Success()
15+
{
16+
// Arrange
17+
var meterFactory = new TestMeterFactory();
18+
var authorizationService = BuildAuthorizationService(meterFactory);
19+
var meter = meterFactory.Meters.Single();
20+
var user = new ClaimsPrincipal(new ClaimsIdentity([new Claim("Permission", "CanViewPage")]));
21+
22+
using var authorizedRequestsCollector = new MetricCollector<long>(meterFactory, AuthorizationMetrics.MeterName, "aspnetcore.authorization.authorized_requests");
23+
24+
// Act
25+
await authorizationService.AuthorizeAsync(user, "Basic");
26+
27+
// Assert
28+
Assert.Equal(AuthorizationMetrics.MeterName, meter.Name);
29+
Assert.Null(meter.Version);
30+
31+
var measurement = Assert.Single(authorizedRequestsCollector.GetMeasurementSnapshot());
32+
Assert.Equal(1, measurement.Value);
33+
Assert.Equal("Basic", (string)measurement.Tags["aspnetcore.authorization.policy"]);
34+
Assert.Equal("success", (string)measurement.Tags["aspnetcore.authorization.result"]);
35+
}
36+
37+
[Fact]
38+
public async Task Authorize_WithPolicyName_Failure()
39+
{
40+
// Arrange
41+
var meterFactory = new TestMeterFactory();
42+
var authorizationService = BuildAuthorizationService(meterFactory);
43+
var meter = meterFactory.Meters.Single();
44+
var user = new ClaimsPrincipal(new ClaimsIdentity([])); // Will fail due to missing required claim
45+
46+
using var authorizedRequestsCollector = new MetricCollector<long>(meterFactory, AuthorizationMetrics.MeterName, "aspnetcore.authorization.authorized_requests");
47+
48+
// Act
49+
await authorizationService.AuthorizeAsync(user, "Basic");
50+
51+
// Assert
52+
Assert.Equal(AuthorizationMetrics.MeterName, meter.Name);
53+
Assert.Null(meter.Version);
54+
55+
var measurement = Assert.Single(authorizedRequestsCollector.GetMeasurementSnapshot());
56+
Assert.Equal(1, measurement.Value);
57+
Assert.Equal("Basic", (string)measurement.Tags["aspnetcore.authorization.policy"]);
58+
Assert.Equal("failure", (string)measurement.Tags["aspnetcore.authorization.result"]);
59+
}
60+
61+
[Fact]
62+
public async Task Authorize_WithoutPolicyName_Success()
63+
{
64+
// Arrange
65+
var meterFactory = new TestMeterFactory();
66+
var authorizationService = BuildAuthorizationService(meterFactory, services =>
67+
{
68+
services.AddSingleton<IAuthorizationHandler>(new AlwaysHandler(succeed: true));
69+
});
70+
var meter = meterFactory.Meters.Single();
71+
var user = new ClaimsPrincipal(new ClaimsIdentity([]));
72+
73+
using var authorizedRequestsCollector = new MetricCollector<long>(meterFactory, AuthorizationMetrics.MeterName, "aspnetcore.authorization.authorized_requests");
74+
75+
// Act
76+
await authorizationService.AuthorizeAsync(user, resource: null, new TestRequirement());
77+
78+
// Assert
79+
Assert.Equal(AuthorizationMetrics.MeterName, meter.Name);
80+
Assert.Null(meter.Version);
81+
82+
var measurement = Assert.Single(authorizedRequestsCollector.GetMeasurementSnapshot());
83+
Assert.Equal(1, measurement.Value);
84+
Assert.Null(measurement.Tags["aspnetcore.authorization.policy"]);
85+
Assert.Equal("success", (string)measurement.Tags["aspnetcore.authorization.result"]);
86+
}
87+
88+
[Fact]
89+
public async Task Authorize_WithoutPolicyName_Failure()
90+
{
91+
// Arrange
92+
var meterFactory = new TestMeterFactory();
93+
var authorizationService = BuildAuthorizationService(meterFactory); // Will fail because there is no handler registered
94+
var meter = meterFactory.Meters.Single();
95+
var user = new ClaimsPrincipal(new ClaimsIdentity([]));
96+
97+
using var authorizedRequestsCollector = new MetricCollector<long>(meterFactory, AuthorizationMetrics.MeterName, "aspnetcore.authorization.authorized_requests");
98+
99+
// Act
100+
await authorizationService.AuthorizeAsync(user, resource: null, new TestRequirement());
101+
102+
// Assert
103+
Assert.Equal(AuthorizationMetrics.MeterName, meter.Name);
104+
Assert.Null(meter.Version);
105+
106+
var measurement = Assert.Single(authorizedRequestsCollector.GetMeasurementSnapshot());
107+
Assert.Equal(1, measurement.Value);
108+
Assert.Null(measurement.Tags["aspnetcore.authorization.policy"]);
109+
Assert.Equal("failure", (string)measurement.Tags["aspnetcore.authorization.result"]);
110+
}
111+
112+
private static IAuthorizationService BuildAuthorizationService(TestMeterFactory meterFactory, Action<IServiceCollection> setupServices = null)
113+
{
114+
var services = new ServiceCollection();
115+
services.AddSingleton(new AuthorizationMetrics(meterFactory));
116+
services.AddAuthorizationBuilder()
117+
.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
118+
services.AddLogging();
119+
services.AddOptions();
120+
setupServices?.Invoke(services);
121+
return services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
122+
}
123+
124+
private sealed class AlwaysHandler(bool succeed) : AuthorizationHandler<TestRequirement>
125+
{
126+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TestRequirement requirement)
127+
{
128+
if (succeed)
129+
{
130+
context.Succeed(requirement);
131+
}
132+
133+
return Task.CompletedTask;
134+
}
135+
}
136+
137+
private sealed class TestRequirement : IAuthorizationRequirement;
138+
}

src/Security/Authorization/test/Microsoft.AspNetCore.Authorization.Test.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
<Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
1111
<Reference Include="Microsoft.AspNetCore.Http" />
1212
<Reference Include="Microsoft.Extensions.DependencyInjection" />
13+
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" />
1314
<Reference Include="Microsoft.Extensions.Logging" />
1415
</ItemGroup>
1516

17+
<ItemGroup>
18+
<Compile Include="$(SharedSourceRoot)Metrics\TestMeterFactory.cs" />
19+
</ItemGroup>
20+
1621
</Project>

0 commit comments

Comments
 (0)