From 0c7598f7ea4a573d572add300f40dad34d4898d9 Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Thu, 7 Nov 2024 19:20:12 +0100
Subject: [PATCH] Add ErrorOnAspNetCoreAuthorizationAttributes option
---
.../Core/src/Abstractions/ErrorCodes.cs | 6 +
.../AuthorizationTypeInterceptor.cs | 112 +++++++++++++++++-
.../Properties/AuthCoreResources.Designer.cs | 12 ++
.../Properties/AuthCoreResources.resx | 6 +
.../Core/src/Types/IReadOnlySchemaOptions.cs | 6 +
.../Core/src/Types/SchemaOptions.cs | 2 +
.../AnnotationBasedAuthorizationTests.cs | 96 +++++++++++++++
7 files changed, 236 insertions(+), 4 deletions(-)
diff --git a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
index dad7fd68e68..0ebfa63540c 100644
--- a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
+++ b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
@@ -251,6 +251,12 @@ public static class Schema
/// The specified directive argument does not exist.
///
public const string UnknownDirectiveArgument = "HC0072";
+
+ ///
+ /// An underlying schema runtime type / member is annotated with a
+ /// Microsoft.AspNetCore.Authorization.* attribute that is not supported by Hot Chocolate.
+ ///
+ public const string UnsupportedAspNetCoreAuthorizationAttribute = "HC0081";
}
public static class Scalars
diff --git a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs
index fd4c4091eb0..4099678af84 100644
--- a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HotChocolate.Configuration;
@@ -10,11 +11,19 @@
using HotChocolate.Utilities;
using static HotChocolate.Authorization.AuthorizeDirectiveType.Names;
using static HotChocolate.WellKnownContextData;
+using static HotChocolate.Authorization.Properties.AuthCoreResources;
namespace HotChocolate.Authorization;
internal sealed partial class AuthorizationTypeInterceptor : TypeInterceptor
{
+ private const string AspNetCoreAuthorizeAttributeName = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute";
+ private const string AspNetCoreAllowAnonymousAttributeName =
+ "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
+
+ private static readonly string _authorizeAttributeName = typeof(AuthorizeAttribute).FullName!;
+ private static readonly string _allowAnonymousAttributeName = typeof(AllowAnonymousAttribute).FullName!;
+
private readonly List _objectTypes = [];
private readonly List _unionTypes = [];
private readonly Dictionary _directives = new();
@@ -114,14 +123,79 @@ public override void OnBeforeCompleteType(
ITypeCompletionContext completionContext,
DefinitionBase definition)
{
+ if (definition is not ObjectTypeDefinition typeDef)
+ {
+ return;
+ }
+
// last in the initialization we need to intercept the query type and ensure that
// authorization configuration is applied to the special introspection and node fields.
- if (ReferenceEquals(_queryContext, completionContext) &&
- definition is ObjectTypeDefinition typeDef)
+ if (ReferenceEquals(_queryContext, completionContext))
{
var state = _state ?? throw ThrowHelper.StateNotInitialized();
HandleSpecialQueryFields(new ObjectTypeInfo(completionContext, typeDef), state);
}
+
+ if (_context.Options.ErrorOnAspNetCoreAuthorizationAttributes && !completionContext.IsIntrospectionType)
+ {
+ var runtimeType = typeDef.RuntimeType;
+ var attributesOnType = runtimeType.GetCustomAttributes().ToArray();
+
+ if (ContainsNamedAttribute(attributesOnType, AspNetCoreAuthorizeAttributeName))
+ {
+ completionContext.ReportError(
+ UnsupportedAspNetCoreAttributeError(
+ AspNetCoreAuthorizeAttributeName,
+ _authorizeAttributeName,
+ runtimeType));
+ return;
+ }
+
+ if (ContainsNamedAttribute(attributesOnType, AspNetCoreAllowAnonymousAttributeName))
+ {
+ completionContext.ReportError(
+ UnsupportedAspNetCoreAttributeError(
+ AspNetCoreAllowAnonymousAttributeName,
+ _allowAnonymousAttributeName,
+ runtimeType));
+ return;
+ }
+
+ foreach (var field in typeDef.Fields)
+ {
+ if (field.IsIntrospectionField)
+ {
+ continue;
+ }
+
+ var fieldMember = field.ResolverMember ?? field.Member;
+
+ if (fieldMember is not null)
+ {
+ var attributesOnResolver = fieldMember.GetCustomAttributes().ToArray();
+
+ if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAuthorizeAttributeName))
+ {
+ completionContext.ReportError(
+ UnsupportedAspNetCoreAttributeError(
+ AspNetCoreAuthorizeAttributeName,
+ _authorizeAttributeName,
+ fieldMember));
+ return;
+ }
+
+ if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAllowAnonymousAttributeName))
+ {
+ completionContext.ReportError(
+ UnsupportedAspNetCoreAttributeError(
+ AspNetCoreAllowAnonymousAttributeName,
+ _allowAnonymousAttributeName,
+ fieldMember));
+ return;
+ }
+ }
+ }
+ }
}
public override void OnAfterCompleteTypes()
@@ -179,7 +253,7 @@ private void InspectObjectTypesForAuthDirective(State state)
// if the field contains the AnonymousAllowed flag we will not
// apply authorization on it.
- if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
+ if (fieldDef.GetContextData().ContainsKey(AllowAnonymous))
{
continue;
}
@@ -353,7 +427,7 @@ private void ApplyAuthMiddleware(
{
// if the field contains the AnonymousAllowed flag we will not apply authorization
// on it.
- if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
+ if (fieldDef.GetContextData().ContainsKey(AllowAnonymous))
{
return;
}
@@ -621,6 +695,36 @@ private State CreateState()
return new State(options ?? new());
}
+
+ private static bool ContainsNamedAttribute(Attribute[] attributes, string nameOfAttribute)
+ => attributes.Any(a => a.GetType().FullName == nameOfAttribute);
+
+ private static ISchemaError UnsupportedAspNetCoreAttributeError(
+ string aspNetCoreAttributeName,
+ string properAttributeName,
+ Type runtimeType)
+ {
+ return SchemaErrorBuilder.New()
+ .SetMessage(string.Format(AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType,
+ aspNetCoreAttributeName, runtimeType.FullName, properAttributeName))
+ .SetCode(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute)
+ .Build();
+ }
+
+ private static ISchemaError UnsupportedAspNetCoreAttributeError(
+ string aspNetCoreAttributeName,
+ string properAttributeName,
+ MemberInfo member)
+ {
+ var nameOfDeclaringType = member.DeclaringType?.FullName;
+ var nameOfMember = member.Name;
+
+ return SchemaErrorBuilder.New()
+ .SetMessage(string.Format(AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember,
+ aspNetCoreAttributeName, nameOfDeclaringType, nameOfMember, properAttributeName))
+ .SetCode(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute)
+ .Build();
+ }
}
static file class AuthorizationTypeInterceptorExtensions
diff --git a/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.Designer.cs b/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.Designer.cs
index 519551433f2..90610d65f1b 100644
--- a/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.Designer.cs
+++ b/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.Designer.cs
@@ -86,5 +86,17 @@ internal static string ThrowHelper_UnableToResolveTypeReg {
return ResourceManager.GetString("ThrowHelper_UnableToResolveTypeReg", resourceCulture);
}
}
+
+ internal static string AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType {
+ get {
+ return ResourceManager.GetString("AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType", resourceCulture);
+ }
+ }
+
+ internal static string AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember {
+ get {
+ return ResourceManager.GetString("AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember", resourceCulture);
+ }
+ }
}
}
diff --git a/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.resx b/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.resx
index d6642c013c7..cae96320485 100644
--- a/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.resx
+++ b/src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.resx
@@ -39,4 +39,10 @@
Unable to resolve a type registration.
+
+ Found unsupported `{0}` on `{1}`. Use `{2}` instead.
+
+
+ Found unsupported `{0}` on `{1}.{2}`. Use `{3}` instead.
+
diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
index c84f69b8999..423058e96d3 100644
--- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
@@ -180,6 +180,12 @@ public interface IReadOnlySchemaOptions
///
bool EnableTag { get; }
+ ///
+ /// Errors if either an ASP.NET Core [Authorize] or [AllowAnonymous] attribute
+ /// is used on a Hot Chocolate resolver or type definition.
+ ///
+ bool ErrorOnAspNetCoreAuthorizationAttributes { get; }
+
///
/// Specifies the default dependency injection scope for query fields.
///
diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
index 564cd82ebbb..d6dedbe97e6 100644
--- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
@@ -210,6 +210,8 @@ public FieldBindingFlags DefaultFieldBindingFlags
///
public bool EnableTag { get; set; } = true;
+ public bool ErrorOnAspNetCoreAuthorizationAttributes { get; set; } = true;
+
///
/// Defines the default dependency injection scope for query fields.
///
diff --git a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs
index 938aea87016..8980ed3d16d 100644
--- a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs
+++ b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs
@@ -13,6 +13,78 @@ namespace HotChocolate.Authorization;
public class AnnotationBasedAuthorizationTests
{
+ [Fact]
+ public async Task Microsoft_AuthorizeAttribute_On_Method_Produces_Error()
+ {
+ var builder = new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .AddAuthorizationCore();
+
+ var act = async () => await builder.BuildSchemaAsync();
+
+ var exception = await Assert.ThrowsAsync(act);
+ var error = exception.Errors.First();
+ Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
+ Assert.Equal(
+ "Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
+ error.Message);
+ }
+
+ [Fact]
+ public async Task Microsoft_AllowAnonymousAttribute_On_Method_Produces_Error()
+ {
+ var builder = new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .AddAuthorizationCore();
+
+ var act = async () => await builder.BuildSchemaAsync();
+
+ var exception = await Assert.ThrowsAsync(act);
+ var error = exception.Errors.First();
+ Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
+ Assert.Equal(
+ "Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
+ error.Message);
+ }
+
+ [Fact]
+ public async Task Microsoft_AuthorizeAttribute_On_Type_Produces_Error()
+ {
+ var builder = new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .AddAuthorizationCore();
+
+ var act = async () => await builder.BuildSchemaAsync();
+
+ var exception = await Assert.ThrowsAsync(act);
+ var error = exception.Errors.First();
+ Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
+ Assert.Equal(
+ "Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttribute`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
+ error.Message);
+ }
+
+ [Fact]
+ public async Task Microsoft_AllowAnonymousAttribute_On_Type_Produces_Error()
+ {
+ var builder = new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .AddAuthorizationCore();
+
+ var act = async () => await builder.BuildSchemaAsync();
+
+ var exception = await Assert.ThrowsAsync(act);
+ var error = exception.Errors.First();
+ Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
+ Assert.Equal(
+ "Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttribute`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
+ error.Message);
+ }
+
[Fact]
public async Task Authorize_Person_NoAccess()
{
@@ -1138,4 +1210,28 @@ public ValueTask AuthorizeAsync(
CancellationToken cancellationToken = default)
=> new(AuthorizeResult.NotAllowed);
}
+
+ public class QueryWithMicrosoftAuthorizeAttributeOnMethod
+ {
+ [Microsoft.AspNetCore.Authorization.Authorize]
+ public string Field() => "foo";
+ }
+
+ public class QueryWithMicrosoftAllowAnonymousAttributeOnMethod
+ {
+ [Microsoft.AspNetCore.Authorization.AllowAnonymous]
+ public string Field() => "foo";
+ }
+
+ [Microsoft.AspNetCore.Authorization.Authorize]
+ public class QueryWithMicrosoftAuthorizeAttribute
+ {
+ public string Field() => "foo";
+ }
+
+ [Microsoft.AspNetCore.Authorization.AllowAnonymous]
+ public class QueryWithMicrosoftAllowAnonymousAttribute
+ {
+ public string Field() => "foo";
+ }
}