Skip to content

Commit

Permalink
Add ErrorOnAspNetCoreAuthorizationAttributes option
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-tengler committed Nov 7, 2024
1 parent b271436 commit c30f25a
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ public static class Schema
/// The specified directive argument does not exist.
/// </summary>
public const string UnknownDirectiveArgument = "HC0072";

/// <summary>
/// An underlying schema runtime member is annotated with an ASP.NET Core
/// Authorization attribute that is not supported by Hot Chocolate.
/// </summary>
public const string UnsupportedAspNetCoreAttribute = "HC0081";
}

public static class Scalars
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HotChocolate.Configuration;
Expand All @@ -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<ObjectTypeInfo> _objectTypes = [];
private readonly List<UnionTypeInfo> _unionTypes = [];
private readonly Dictionary<ObjectType, IDirectiveCollection> _directives = new();
Expand Down Expand Up @@ -114,14 +123,109 @@ 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;
}
}
}
}
}

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.UnsupportedAspNetCoreAttribute)
.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.UnsupportedAspNetCoreAttribute)
.Build();
}

public override void OnAfterCompleteTypes()
Expand Down Expand Up @@ -179,7 +283,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;
}
Expand Down Expand Up @@ -353,7 +457,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;
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@
<data name="ThrowHelper_UnableToResolveTypeReg" xml:space="preserve">
<value>Unable to resolve a type registration.</value>
</data>
<data name="AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType" xml:space="preserve">
<value>Found unsupported `{0}` on `{1}`. Use `{2}` instead.</value>
</data>
<data name="AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember" xml:space="preserve">
<value>Found unsupported `{0}` on `{1}.{2}`. Use `{3}` instead.</value>
</data>
</root>
6 changes: 6 additions & 0 deletions src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ public interface IReadOnlySchemaOptions
/// </summary>
bool EnableTag { get; }

/// <summary>
/// Errors if either an ASP.NET Core [Authorize] or [AllowAnonymous] attribute
/// is used on a Hot Chocolate resolver or type definition.
/// </summary>
bool ErrorOnAspNetCoreAuthorizationAttributes { get; }

/// <summary>
/// Specifies the default dependency injection scope for query fields.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/HotChocolate/Core/src/Types/SchemaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ public FieldBindingFlags DefaultFieldBindingFlags
/// </summary>
public bool EnableTag { get; set; } = true;

public bool ErrorOnAspNetCoreAuthorizationAttributes { get; set; } = true;

/// <summary>
/// Defines the default dependency injection scope for query fields.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<QueryWithMicrosoftAuthorizeAttributeOnMethod>()
.AddAuthorizationCore();

var act = async () => await builder.BuildSchemaAsync();

var exception = await Assert.ThrowsAsync<SchemaException>(act);
var error = exception.Errors.First();
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAttribute, 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<QueryWithMicrosoftAllowAnonymousAttributeOnMethod>()
.AddAuthorizationCore();

var act = async () => await builder.BuildSchemaAsync();

var exception = await Assert.ThrowsAsync<SchemaException>(act);
var error = exception.Errors.First();
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAttribute, 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<QueryWithMicrosoftAuthorizeAttribute>()
.AddAuthorizationCore();

var act = async () => await builder.BuildSchemaAsync();

var exception = await Assert.ThrowsAsync<SchemaException>(act);
var error = exception.Errors.First();
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAttribute, 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<QueryWithMicrosoftAllowAnonymousAttribute>()
.AddAuthorizationCore();

var act = async () => await builder.BuildSchemaAsync();

var exception = await Assert.ThrowsAsync<SchemaException>(act);
var error = exception.Errors.First();
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAttribute, 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()
{
Expand Down Expand Up @@ -1138,4 +1210,28 @@ public ValueTask<AuthorizeResult> 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";
}
}

0 comments on commit c30f25a

Please sign in to comment.