Skip to content

Commit d42c05b

Browse files
committed
Add AuthorizationPolicyBuilder.RequireClaim overload that take a Predicate<Claim>
1 parent de37915 commit d42c05b

File tree

7 files changed

+130
-7
lines changed

7 files changed

+130
-7
lines changed

src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Security.Claims;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Authorization.Infrastructure;
910
using Microsoft.AspNetCore.Shared;
@@ -141,6 +142,20 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType)
141142
return this;
142143
}
143144

145+
/// <summary>
146+
/// Adds a <see cref="ClaimsAuthorizationRequirement"/> to the current instance which requires
147+
/// that the current user has a claim that satisfies the specified predicate.
148+
/// </summary>
149+
/// <param name="match">The predicate to evaluate the claims.</param>
150+
/// <returns>A reference to this instance after the operation has completed.</returns>
151+
public AuthorizationPolicyBuilder RequireClaim(Predicate<Claim> match)
152+
{
153+
ArgumentNullThrowHelper.ThrowIfNull(match);
154+
155+
Requirements.Add(new ClaimsAuthorizationRequirement(match));
156+
return this;
157+
}
158+
144159
/// <summary>
145160
/// Adds a <see cref="RolesAuthorizationRequirement"/> to the current instance which enforces that the current user
146161
/// must have at least one of the specified roles.

src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Security.Claims;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Shared;
910

@@ -32,17 +33,35 @@ public ClaimsAuthorizationRequirement(string claimType, IEnumerable<string>? all
3233
_emptyAllowedValues = AllowedValues == null || !AllowedValues.Any();
3334
}
3435

36+
/// <summary>
37+
/// Creates a new instance of <see cref="ClaimsAuthorizationRequirement"/>.
38+
/// </summary>
39+
/// <param name="match">The predicate to evaluate the claims.</param>
40+
public ClaimsAuthorizationRequirement(Predicate<Claim> match)
41+
{
42+
ArgumentNullThrowHelper.ThrowIfNull(match);
43+
44+
Match = match;
45+
_emptyAllowedValues = true;
46+
}
47+
3548
/// <summary>
3649
/// Gets the claim type that must be present.
3750
/// </summary>
38-
public string ClaimType { get; }
51+
public string? ClaimType { get; }
3952

4053
/// <summary>
4154
/// Gets the optional list of claim values, which, if present,
4255
/// the claim must match.
4356
/// </summary>
4457
public IEnumerable<string>? AllowedValues { get; }
4558

59+
/// <summary>
60+
/// A predicate to evaluate the claims.
61+
/// Used if specified instead of <see cref="ClaimType"/> and <see cref="AllowedValues"/>.
62+
/// </summary>
63+
public Predicate<Claim>? Match { get; }
64+
4665
/// <summary>
4766
/// Makes a decision if authorization is allowed based on the claims requirements specified.
4867
/// </summary>
@@ -53,7 +72,12 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
5372
if (context.User != null)
5473
{
5574
var found = false;
56-
if (requirement._emptyAllowedValues)
75+
76+
if (requirement.Match != null)
77+
{
78+
found = context.User.HasClaim(requirement.Match);
79+
}
80+
else if (requirement._emptyAllowedValues)
5781
{
5882
foreach (var claim in context.User.Claims)
5983
{
@@ -76,17 +100,24 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
76100
}
77101
}
78102
}
103+
79104
if (found)
80105
{
81106
context.Succeed(requirement);
82107
}
83108
}
109+
84110
return Task.CompletedTask;
85111
}
86112

87113
/// <inheritdoc />
88114
public override string ToString()
89115
{
116+
if (Match != null)
117+
{
118+
return $"{nameof(ClaimsAuthorizationRequirement)}:Evaluates using a custom predicate";
119+
}
120+
90121
var value = (_emptyAllowedValues)
91122
? string.Empty
92123
: $" and Claim.Value is one of the following values: ({string.Join("|", AllowedValues!)})";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Predicate<System.Security.Claims.Claim!>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
3+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Predicate<System.Security.Claims.Claim!>! match) -> void
4+
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
5+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
6+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Predicate<System.Security.Claims.Claim!>?
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Predicate<System.Security.Claims.Claim!>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
3+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Predicate<System.Security.Claims.Claim!>! match) -> void
4+
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
5+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
6+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Predicate<System.Security.Claims.Claim!>?
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Predicate<System.Security.Claims.Claim!>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
3+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Predicate<System.Security.Claims.Claim!>! match) -> void
4+
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
5+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
6+
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Predicate<System.Security.Claims.Claim!>?

src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Security.Claims;
45
using Microsoft.AspNetCore.Authorization.Infrastructure;
56

67
namespace Microsoft.AspNetCore.Authorization.Test;
78

89
public class ClaimsAuthorizationRequirementTests
910
{
10-
public ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
11-
{
12-
return new ClaimsAuthorizationRequirement(claimType, allowedValues);
13-
}
14-
1511
[Fact]
1612
public void ToString_ShouldReturnAndDescriptionWhenAllowedValuesNotNull()
1713
{
@@ -50,4 +46,28 @@ public void ToString_ShouldReturnWithoutAllowedDescriptionWhenAllowedValuesIsEmp
5046
// Assert
5147
Assert.Equal("ClaimsAuthorizationRequirement:Claim.Type=Custom", formattedValue);
5248
}
49+
50+
[Fact]
51+
public void ToString_ShouldReturnPredicateDescriptionWhenPredicateIsUsed()
52+
{
53+
// Arrange
54+
Predicate<Claim> claimPredicate = claim => claim.Type == "Permissions" && claim.Value.Contains("CanViewPage");
55+
var requirement = CreateRequirement(claimPredicate);
56+
57+
// Act
58+
var formattedValue = requirement.ToString();
59+
60+
// Assert
61+
Assert.Equal("ClaimsAuthorizationRequirement:Evaluates using a custom predicate", formattedValue);
62+
}
63+
64+
private ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
65+
{
66+
return new ClaimsAuthorizationRequirement(claimType, allowedValues);
67+
}
68+
69+
private ClaimsAuthorizationRequirement CreateRequirement(Predicate<Claim> match)
70+
{
71+
return new ClaimsAuthorizationRequirement(match);
72+
}
5373
}

src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues()
9090
Assert.True(allowed.Succeeded);
9191
}
9292

93+
[Fact]
94+
public async Task Authorize_ShouldAllowIfClaimMatchesPredicate()
95+
{
96+
// Arrange
97+
var authorizationService = BuildAuthorizationService(services =>
98+
{
99+
services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
100+
{
101+
policy.AddAuthenticationSchemes("Basic");
102+
policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewPage");
103+
});
104+
});
105+
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));
106+
107+
// Act
108+
var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
109+
110+
// Assert
111+
Assert.True(allowed.Succeeded);
112+
}
113+
93114
[Fact]
94115
public async Task Authorize_ShouldInvokeAllHandlersByDefault()
95116
{
@@ -260,6 +281,27 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent()
260281
Assert.False(allowed.Succeeded);
261282
}
262283

284+
[Fact]
285+
public async Task Authorize_ShouldNotAllowIfClaimDoesNotMatchPredicate()
286+
{
287+
// Arrange
288+
var authorizationService = BuildAuthorizationService(services =>
289+
{
290+
services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
291+
{
292+
policy.AddAuthenticationSchemes("Basic");
293+
policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewAnything");
294+
});
295+
});
296+
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));
297+
298+
// Act
299+
var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
300+
301+
// Assert
302+
Assert.False(allowed.Succeeded);
303+
}
304+
263305
[Fact]
264306
public async Task Authorize_ShouldNotAllowIfNoClaims()
265307
{

0 commit comments

Comments
 (0)