Skip to content
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

Align authorization behavior to regular ASP.NET Core #7408

Merged
merged 9 commits into from
Sep 2, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Collections.Concurrent;
using HotChocolate.Authorization;
using Microsoft.AspNetCore.Authorization;

namespace HotChocolate.AspNetCore.Authorization;

internal sealed class AuthorizationPolicyCache(IAuthorizationPolicyProvider policyProvider)
{
private readonly ConcurrentDictionary<string, Task<AuthorizationPolicy>> _cache = new();

public Task<AuthorizationPolicy> GetOrCreatePolicyAsync(AuthorizeDirective directive)
{
var cacheKey = directive.GetPolicyCacheKey();

return _cache.GetOrAdd(cacheKey, _ => BuildAuthorizationPolicy(directive.Policy, directive.Roles));
}

private async Task<AuthorizationPolicy> BuildAuthorizationPolicy(
string? policyName,
IReadOnlyList<string>? roles)
{
var policyBuilder = new AuthorizationPolicyBuilder();

if (!string.IsNullOrWhiteSpace(policyName))
{
var policy = await policyProvider.GetPolicyAsync(policyName).ConfigureAwait(false);

if (policy is not null)
{
policyBuilder = policyBuilder.Combine(policy);
}
else
{
throw new MissingAuthorizationPolicyException(policyName);
}
}
else
{
var defaultPolicy = await policyProvider.GetDefaultPolicyAsync().ConfigureAwait(false);

policyBuilder = policyBuilder.Combine(defaultPolicy);
}

if (roles is not null)
{
policyBuilder = policyBuilder.RequireRole(roles);
}

return policyBuilder.Build();
}
}

internal sealed class MissingAuthorizationPolicyException(string policyName)
tobias-tengler marked this conversation as resolved.
Show resolved Hide resolved
: Exception($"The policy `{policyName}` does not exist.")
{
public string PolicyName { get; } = policyName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,29 @@ namespace HotChocolate.AspNetCore.Authorization;
internal sealed class DefaultAuthorizationHandler : IAuthorizationHandler
{
private readonly IAuthorizationService _authSvc;
private readonly IAuthorizationPolicyProvider _policyProvider;
private readonly AuthorizationPolicyCache _policyCache;

/// <summary>
/// Initializes a new instance <see cref="DefaultAuthorizationHandler"/>.
/// </summary>
/// <param name="authorizationService">
/// The authorization service.
/// </param>
/// <param name="authorizationPolicyProvider">
/// The authorization policy provider.
/// <param name="policyCache">
/// The authorization policy cache.
/// </param>
/// <exception cref="ArgumentNullException">
/// <paramref name="authorizationService"/> is <c>null</c>.
/// <paramref name="authorizationPolicyProvider"/> is <c>null</c>.
/// <paramref name="policyCache"/> is <c>null</c>.
/// </exception>
public DefaultAuthorizationHandler(
IAuthorizationService authorizationService,
IAuthorizationPolicyProvider authorizationPolicyProvider)
AuthorizationPolicyCache policyCache)
{
_authSvc = authorizationService ??
throw new ArgumentNullException(nameof(authorizationService));
_policyProvider = authorizationPolicyProvider ??
throw new ArgumentNullException(nameof(authorizationPolicyProvider));
_policyCache = policyCache ??
throw new ArgumentNullException(nameof(policyCache));
}

/// <summary>
Expand Down Expand Up @@ -70,8 +70,7 @@ public async ValueTask<AuthorizeResult> AuthorizeAsync(

return await AuthorizeAsync(
user,
directive.Policy,
directive.Roles,
directive,
authenticated,
context)
.ConfigureAwait(false);
Expand Down Expand Up @@ -102,8 +101,7 @@ public async ValueTask<AuthorizeResult> AuthorizeAsync(
{
var result = await AuthorizeAsync(
user,
directive.Policy,
directive.Roles,
directive,
authenticated,
context)
.ConfigureAwait(false);
Expand All @@ -119,62 +117,24 @@ public async ValueTask<AuthorizeResult> AuthorizeAsync(

private async ValueTask<AuthorizeResult> AuthorizeAsync(
ClaimsPrincipal user,
string? policyName,
IReadOnlyList<string>? roles,
AuthorizeDirective directive,
bool authenticated,
object context)
{
var checkRoles = roles is { Count: > 0, };
var checkPolicy = !string.IsNullOrWhiteSpace(policyName);

// if the current directive has neither roles nor policies specified we will check if there
// is a default policy specified.
if (!checkRoles && !checkPolicy)
try
{
var policy = await _policyProvider.GetDefaultPolicyAsync().ConfigureAwait(false);
var combinedPolicy = await _policyCache.GetOrCreatePolicyAsync(directive);

// if there is no default policy specified we will check if at least one of the
// identities are authenticated to authorize the user.
if (policy is null)
{
return authenticated
? AuthorizeResult.Allowed
: AuthorizeResult.NoDefaultPolicy;
}
var result = await _authSvc.AuthorizeAsync(user, context, combinedPolicy).ConfigureAwait(false);

// if we find a default policy we will use this to authorize the access to a resource.
var result = await _authSvc.AuthorizeAsync(user, context, policy).ConfigureAwait(false);
return result.Succeeded
? AuthorizeResult.Allowed
: authenticated ? AuthorizeResult.NotAllowed : AuthorizeResult.NotAuthenticated;
}

// We first check if the user fulfills any of the specified roles.
// If no role was specified the user fulfills them.
if (!checkRoles || FulfillsAnyRole(user, roles!))
catch (MissingAuthorizationPolicyException)
{
if (!checkPolicy)
{
// The user fulfills one or all of the roles and no policy check was required.
return AuthorizeResult.Allowed;
}

// If a policy name was supplied we will try to resolve the policy
// and authorize with it.
var policy = await _policyProvider.GetPolicyAsync(policyName!).ConfigureAwait(false);

if (policy is null)
{
return AuthorizeResult.PolicyNotFound;
}

var result = await _authSvc.AuthorizeAsync(user, context, policy).ConfigureAwait(false);
return result.Succeeded
? AuthorizeResult.Allowed
: authenticated ? AuthorizeResult.NotAllowed : AuthorizeResult.NotAuthenticated;
return AuthorizeResult.PolicyNotFound;
}

return authenticated ? AuthorizeResult.NotAllowed : AuthorizeResult.NotAuthenticated;
}

private static UserState GetUserState(IDictionary<string, object?> contextData)
Expand All @@ -193,17 +153,4 @@ private static UserState GetUserState(IDictionary<string, object?> contextData)

private static void SetUserState(IDictionary<string, object?> contextData, UserState state)
=> contextData[WellKnownContextData.UserState] = state;

private static bool FulfillsAnyRole(ClaimsPrincipal principal, IReadOnlyList<string> roles)
{
for (var i = 0; i < roles.Count; i++)
{
if (principal.IsInRole(roles[i]))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using HotChocolate.AspNetCore.Authorization;
using HotChocolate.Execution.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -27,7 +28,7 @@ public static IRequestExecutorBuilder AddAuthorization(
}

builder.Services.AddAuthorization();
builder.AddAuthorizationHandler<DefaultAuthorizationHandler>();
builder.AddAuthorizationServices();
return builder;
}

Expand Down Expand Up @@ -60,7 +61,13 @@ public static IRequestExecutorBuilder AddAuthorization(
}

builder.Services.AddAuthorization(configure);
builder.AddAuthorizationHandler<DefaultAuthorizationHandler>();
builder.AddAuthorizationServices();
return builder;
}

private static void AddAuthorizationServices(this IRequestExecutorBuilder builder)
{
builder.Services.TryAddSingleton<AuthorizationPolicyCache>();
builder.AddAuthorizationHandler<DefaultAuthorizationHandler>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class Query
[GraphQLName("roles_ab")]
public string? GetRolesAb() => "foo";

[Authorize(ApplyPolicy.BeforeResolver, Roles = ["a", "b"], Policy = "HasAgeDefined")]
[GraphQLName("rolesAndPolicy")]
public string? GetRolesAndPolicy() => "foo";

[Authorize(ApplyPolicy.BeforeResolver, Policy = "a")]
[Authorize(ApplyPolicy.BeforeResolver, Policy = "b")]
public string? GetPiped() => "foo";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Query {
age: String @authorize(policy: ""HasAgeDefined"" apply: BEFORE_RESOLVER)
roles: String @authorize(roles: [""a""] apply: BEFORE_RESOLVER)
roles_ab: String @authorize(roles: [""a"" ""b""] apply: BEFORE_RESOLVER)
rolesAndPolicy: String @authorize(roles: [""a"" ""b""] policy: ""HasAgeDefined"" apply: BEFORE_RESOLVER)
piped: String
@authorize(policy: ""a"" apply: BEFORE_RESOLVER)
@authorize(policy: ""b"" apply: BEFORE_RESOLVER)
Expand Down
Loading
Loading