Skip to content

Add AuthorizationPolicyBuilder.RequireClaim overload that take a Func<Claim, bool> #56346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Shared;
Expand Down Expand Up @@ -141,6 +142,20 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType)
return this;
}

/// <summary>
/// Adds a <see cref="ClaimsAuthorizationRequirement"/> to the current instance which requires
/// that the current user has a claim that satisfies the specified predicate.
/// </summary>
/// <param name="match">The predicate to evaluate the claims.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public AuthorizationPolicyBuilder RequireClaim(Func<Claim, bool> match)
{
ArgumentNullThrowHelper.ThrowIfNull(match);

Requirements.Add(new ClaimsAuthorizationRequirement(match));
return this;
}

/// <summary>
/// Adds a <see cref="RolesAuthorizationRequirement"/> to the current instance which enforces that the current user
/// must have at least one of the specified roles.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Shared;

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

/// <summary>
/// Creates a new instance of <see cref="ClaimsAuthorizationRequirement"/>.
/// </summary>
/// <param name="match">The predicate to evaluate the claims.</param>
public ClaimsAuthorizationRequirement(Func<Claim, bool> match)
{
ArgumentNullThrowHelper.ThrowIfNull(match);

Match = match;
_emptyAllowedValues = true;
}

/// <summary>
/// Gets the claim type that must be present.
/// </summary>
public string ClaimType { get; }
public string? ClaimType { get; }

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

/// <summary>
/// A predicate to evaluate the claims.
/// Used if specified instead of <see cref="ClaimType"/> and <see cref="AllowedValues"/>.
/// </summary>
public Func<Claim, bool>? Match { get; }

/// <summary>
/// Makes a decision if authorization is allowed based on the claims requirements specified.
/// </summary>
Expand All @@ -53,7 +72,12 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
if (context.User != null)
{
var found = false;
if (requirement._emptyAllowedValues)

if (requirement.Match != null)
{
found = context.User.HasClaim(new Predicate<Claim>(requirement.Match));
}
else if (requirement._emptyAllowedValues)
{
foreach (var claim in context.User.Claims)
{
Expand All @@ -76,17 +100,24 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
}
}
}

if (found)
{
context.Succeed(requirement);
}
}

return Task.CompletedTask;
}

/// <inheritdoc />
public override string ToString()
{
if (Match != null)
{
return $"{nameof(ClaimsAuthorizationRequirement)}:Evaluates using a custom predicate";
}

var value = (_emptyAllowedValues)
? string.Empty
: $" and Claim.Value is one of the following values: ({string.Join("|", AllowedValues!)})";
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func<System.Security.Claims.Claim!, bool>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func<System.Security.Claims.Claim!, bool>! match) -> void
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func<System.Security.Claims.Claim!, bool>?
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func<System.Security.Claims.Claim!, bool>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func<System.Security.Claims.Claim!, bool>! match) -> void
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func<System.Security.Claims.Claim!, bool>?
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func<System.Security.Claims.Claim!, bool>! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func<System.Security.Claims.Claim!, bool>! match) -> void
*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func<System.Security.Claims.Claim!, bool>?
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace Microsoft.AspNetCore.Authorization.Test;

public class ClaimsAuthorizationRequirementTests
{
public ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
{
return new ClaimsAuthorizationRequirement(claimType, allowedValues);
}

[Fact]
public void ToString_ShouldReturnAndDescriptionWhenAllowedValuesNotNull()
{
Expand Down Expand Up @@ -50,4 +46,28 @@ public void ToString_ShouldReturnWithoutAllowedDescriptionWhenAllowedValuesIsEmp
// Assert
Assert.Equal("ClaimsAuthorizationRequirement:Claim.Type=Custom", formattedValue);
}

[Fact]
public void ToString_ShouldReturnPredicateDescriptionWhenPredicateIsUsed()
{
// Arrange
Func<Claim, bool> match = claim => claim.Type == "Permissions" && claim.Value.Contains("CanViewPage");
var requirement = CreateRequirement(match);

// Act
var formattedValue = requirement.ToString();

// Assert
Assert.Equal("ClaimsAuthorizationRequirement:Evaluates using a custom predicate", formattedValue);
}

private ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
{
return new ClaimsAuthorizationRequirement(claimType, allowedValues);
}

private ClaimsAuthorizationRequirement CreateRequirement(Func<Claim, bool> match)
{
return new ClaimsAuthorizationRequirement(match);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues()
Assert.True(allowed.Succeeded);
}

[Fact]
public async Task Authorize_ShouldAllowIfClaimMatchesPredicate()
{
// Arrange
var authorizationService = BuildAuthorizationService(services =>
{
services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
{
policy.AddAuthenticationSchemes("Basic");
policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewPage");
});
});
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));

// Act
var allowed = await authorizationService.AuthorizeAsync(user, "Basic");

// Assert
Assert.True(allowed.Succeeded);
}

[Fact]
public async Task Authorize_ShouldInvokeAllHandlersByDefault()
{
Expand Down Expand Up @@ -260,6 +281,27 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent()
Assert.False(allowed.Succeeded);
}

[Fact]
public async Task Authorize_ShouldNotAllowIfClaimDoesNotMatchPredicate()
{
// Arrange
var authorizationService = BuildAuthorizationService(services =>
{
services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
{
policy.AddAuthenticationSchemes("Basic");
policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewAnything");
});
});
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));

// Act
var allowed = await authorizationService.AuthorizeAsync(user, "Basic");

// Assert
Assert.False(allowed.Succeeded);
}

[Fact]
public async Task Authorize_ShouldNotAllowIfNoClaims()
{
Expand Down
Loading