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 27, 2024
1 parent 98c8879 commit 723047b
Show file tree
Hide file tree
Showing 7 changed files with 238 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 @@ -252,6 +252,12 @@ public static class Schema
/// The specified directive argument does not exist.
/// </summary>
public const string UnknownDirectiveArgument = "HC0072";

/// <summary>
/// An underlying schema runtime type / member is annotated with a
/// Microsoft.AspNetCore.Authorization.* attribute that is not supported by Hot Chocolate.
/// </summary>
public const string UnsupportedAspNetCoreAuthorizationAttribute = "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,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()
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
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 @@ -187,6 +187,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
4 changes: 4 additions & 0 deletions src/HotChocolate/Core/src/Types/SchemaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ public FieldBindingFlags DefaultFieldBindingFlags
/// <inheritdoc cref="IReadOnlySchemaOptions.EnableTag"/>
public bool EnableTag { get; set; } = true;

/// <inheritdoc cref="IReadOnlySchemaOptions.ErrorOnAspNetCoreAuthorizationAttributes"/>
public bool ErrorOnAspNetCoreAuthorizationAttributes { get; set; } = true;

/// <inheritdoc cref="IReadOnlySchemaOptions.DefaultQueryDependencyInjectionScope"/>
public DependencyInjectionScope DefaultQueryDependencyInjectionScope { get; set; } =
DependencyInjectionScope.Resolver;
Expand Down Expand Up @@ -170,6 +173,7 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize,
StripLeadingIFromInterface = options.StripLeadingIFromInterface,
EnableTag = options.EnableTag,
ErrorOnAspNetCoreAuthorizationAttributes = options.ErrorOnAspNetCoreAuthorizationAttributes,
DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope,
DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope,
};
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.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<QueryWithMicrosoftAllowAnonymousAttributeOnMethod>()
.AddAuthorizationCore();

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

var exception = await Assert.ThrowsAsync<SchemaException>(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<QueryWithMicrosoftAuthorizeAttribute>()
.AddAuthorizationCore();

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

var exception = await Assert.ThrowsAsync<SchemaException>(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<QueryWithMicrosoftAllowAnonymousAttribute>()
.AddAuthorizationCore();

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

var exception = await Assert.ThrowsAsync<SchemaException>(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()
{
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 723047b

Please sign in to comment.