Skip to content

Commit 723047b

Browse files
Add ErrorOnAspNetCoreAuthorizationAttributes option
1 parent 98c8879 commit 723047b

File tree

7 files changed

+238
-4
lines changed

7 files changed

+238
-4
lines changed

src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ public static class Schema
252252
/// The specified directive argument does not exist.
253253
/// </summary>
254254
public const string UnknownDirectiveArgument = "HC0072";
255+
256+
/// <summary>
257+
/// An underlying schema runtime type / member is annotated with a
258+
/// Microsoft.AspNetCore.Authorization.* attribute that is not supported by Hot Chocolate.
259+
/// </summary>
260+
public const string UnsupportedAspNetCoreAuthorizationAttribute = "HC0081";
255261
}
256262

257263
public static class Scalars

src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs

+108-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Reflection;
12
using System.Runtime.CompilerServices;
23
using System.Runtime.InteropServices;
34
using HotChocolate.Configuration;
@@ -10,11 +11,19 @@
1011
using HotChocolate.Utilities;
1112
using static HotChocolate.Authorization.AuthorizeDirectiveType.Names;
1213
using static HotChocolate.WellKnownContextData;
14+
using static HotChocolate.Authorization.Properties.AuthCoreResources;
1315

1416
namespace HotChocolate.Authorization;
1517

1618
internal sealed partial class AuthorizationTypeInterceptor : TypeInterceptor
1719
{
20+
private const string AspNetCoreAuthorizeAttributeName = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute";
21+
private const string AspNetCoreAllowAnonymousAttributeName =
22+
"Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
23+
24+
private static readonly string _authorizeAttributeName = typeof(AuthorizeAttribute).FullName!;
25+
private static readonly string _allowAnonymousAttributeName = typeof(AllowAnonymousAttribute).FullName!;
26+
1827
private readonly List<ObjectTypeInfo> _objectTypes = [];
1928
private readonly List<UnionTypeInfo> _unionTypes = [];
2029
private readonly Dictionary<ObjectType, IDirectiveCollection> _directives = new();
@@ -114,14 +123,79 @@ public override void OnBeforeCompleteType(
114123
ITypeCompletionContext completionContext,
115124
DefinitionBase definition)
116125
{
126+
if (definition is not ObjectTypeDefinition typeDef)
127+
{
128+
return;
129+
}
130+
117131
// last in the initialization we need to intercept the query type and ensure that
118132
// authorization configuration is applied to the special introspection and node fields.
119-
if (ReferenceEquals(_queryContext, completionContext) &&
120-
definition is ObjectTypeDefinition typeDef)
133+
if (ReferenceEquals(_queryContext, completionContext))
121134
{
122135
var state = _state ?? throw ThrowHelper.StateNotInitialized();
123136
HandleSpecialQueryFields(new ObjectTypeInfo(completionContext, typeDef), state);
124137
}
138+
139+
if (_context.Options.ErrorOnAspNetCoreAuthorizationAttributes && !completionContext.IsIntrospectionType)
140+
{
141+
var runtimeType = typeDef.RuntimeType;
142+
var attributesOnType = runtimeType.GetCustomAttributes().ToArray();
143+
144+
if (ContainsNamedAttribute(attributesOnType, AspNetCoreAuthorizeAttributeName))
145+
{
146+
completionContext.ReportError(
147+
UnsupportedAspNetCoreAttributeError(
148+
AspNetCoreAuthorizeAttributeName,
149+
_authorizeAttributeName,
150+
runtimeType));
151+
return;
152+
}
153+
154+
if (ContainsNamedAttribute(attributesOnType, AspNetCoreAllowAnonymousAttributeName))
155+
{
156+
completionContext.ReportError(
157+
UnsupportedAspNetCoreAttributeError(
158+
AspNetCoreAllowAnonymousAttributeName,
159+
_allowAnonymousAttributeName,
160+
runtimeType));
161+
return;
162+
}
163+
164+
foreach (var field in typeDef.Fields)
165+
{
166+
if (field.IsIntrospectionField)
167+
{
168+
continue;
169+
}
170+
171+
var fieldMember = field.ResolverMember ?? field.Member;
172+
173+
if (fieldMember is not null)
174+
{
175+
var attributesOnResolver = fieldMember.GetCustomAttributes().ToArray();
176+
177+
if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAuthorizeAttributeName))
178+
{
179+
completionContext.ReportError(
180+
UnsupportedAspNetCoreAttributeError(
181+
AspNetCoreAuthorizeAttributeName,
182+
_authorizeAttributeName,
183+
fieldMember));
184+
return;
185+
}
186+
187+
if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAllowAnonymousAttributeName))
188+
{
189+
completionContext.ReportError(
190+
UnsupportedAspNetCoreAttributeError(
191+
AspNetCoreAllowAnonymousAttributeName,
192+
_allowAnonymousAttributeName,
193+
fieldMember));
194+
return;
195+
}
196+
}
197+
}
198+
}
125199
}
126200

127201
public override void OnAfterCompleteTypes()
@@ -179,7 +253,7 @@ private void InspectObjectTypesForAuthDirective(State state)
179253

180254
// if the field contains the AnonymousAllowed flag we will not
181255
// apply authorization on it.
182-
if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
256+
if (fieldDef.GetContextData().ContainsKey(AllowAnonymous))
183257
{
184258
continue;
185259
}
@@ -353,7 +427,7 @@ private void ApplyAuthMiddleware(
353427
{
354428
// if the field contains the AnonymousAllowed flag we will not apply authorization
355429
// on it.
356-
if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
430+
if (fieldDef.GetContextData().ContainsKey(AllowAnonymous))
357431
{
358432
return;
359433
}
@@ -621,6 +695,36 @@ private State CreateState()
621695

622696
return new State(options ?? new());
623697
}
698+
699+
private static bool ContainsNamedAttribute(Attribute[] attributes, string nameOfAttribute)
700+
=> attributes.Any(a => a.GetType().FullName == nameOfAttribute);
701+
702+
private static ISchemaError UnsupportedAspNetCoreAttributeError(
703+
string aspNetCoreAttributeName,
704+
string properAttributeName,
705+
Type runtimeType)
706+
{
707+
return SchemaErrorBuilder.New()
708+
.SetMessage(string.Format(AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType,
709+
aspNetCoreAttributeName, runtimeType.FullName, properAttributeName))
710+
.SetCode(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute)
711+
.Build();
712+
}
713+
714+
private static ISchemaError UnsupportedAspNetCoreAttributeError(
715+
string aspNetCoreAttributeName,
716+
string properAttributeName,
717+
MemberInfo member)
718+
{
719+
var nameOfDeclaringType = member.DeclaringType?.FullName;
720+
var nameOfMember = member.Name;
721+
722+
return SchemaErrorBuilder.New()
723+
.SetMessage(string.Format(AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember,
724+
aspNetCoreAttributeName, nameOfDeclaringType, nameOfMember, properAttributeName))
725+
.SetCode(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute)
726+
.Build();
727+
}
624728
}
625729

626730
static file class AuthorizationTypeInterceptorExtensions

src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.Designer.cs

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Core/src/Authorization/Properties/AuthCoreResources.resx

+6
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@
3939
<data name="ThrowHelper_UnableToResolveTypeReg" xml:space="preserve">
4040
<value>Unable to resolve a type registration.</value>
4141
</data>
42+
<data name="AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnType" xml:space="preserve">
43+
<value>Found unsupported `{0}` on `{1}`. Use `{2}` instead.</value>
44+
</data>
45+
<data name="AuthorizationTypeInterceptor_UnsupportedAspNetCoreAttributeOnMember" xml:space="preserve">
46+
<value>Found unsupported `{0}` on `{1}.{2}`. Use `{3}` instead.</value>
47+
</data>
4248
</root>

src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ public interface IReadOnlySchemaOptions
187187
/// </summary>
188188
bool EnableTag { get; }
189189

190+
/// <summary>
191+
/// Errors if either an ASP.NET Core [Authorize] or [AllowAnonymous] attribute
192+
/// is used on a Hot Chocolate resolver or type definition.
193+
/// </summary>
194+
bool ErrorOnAspNetCoreAuthorizationAttributes { get; }
195+
190196
/// <summary>
191197
/// Specifies the default dependency injection scope for query fields.
192198
/// </summary>

src/HotChocolate/Core/src/Types/SchemaOptions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ public FieldBindingFlags DefaultFieldBindingFlags
123123
/// <inheritdoc cref="IReadOnlySchemaOptions.EnableTag"/>
124124
public bool EnableTag { get; set; } = true;
125125

126+
/// <inheritdoc cref="IReadOnlySchemaOptions.ErrorOnAspNetCoreAuthorizationAttributes"/>
127+
public bool ErrorOnAspNetCoreAuthorizationAttributes { get; set; } = true;
128+
126129
/// <inheritdoc cref="IReadOnlySchemaOptions.DefaultQueryDependencyInjectionScope"/>
127130
public DependencyInjectionScope DefaultQueryDependencyInjectionScope { get; set; } =
128131
DependencyInjectionScope.Resolver;
@@ -170,6 +173,7 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
170173
MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize,
171174
StripLeadingIFromInterface = options.StripLeadingIFromInterface,
172175
EnableTag = options.EnableTag,
176+
ErrorOnAspNetCoreAuthorizationAttributes = options.ErrorOnAspNetCoreAuthorizationAttributes,
173177
DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope,
174178
DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope,
175179
};

src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs

+96
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,78 @@ namespace HotChocolate.Authorization;
1313

1414
public class AnnotationBasedAuthorizationTests
1515
{
16+
[Fact]
17+
public async Task Microsoft_AuthorizeAttribute_On_Method_Produces_Error()
18+
{
19+
var builder = new ServiceCollection()
20+
.AddGraphQL()
21+
.AddQueryType<QueryWithMicrosoftAuthorizeAttributeOnMethod>()
22+
.AddAuthorizationCore();
23+
24+
var act = async () => await builder.BuildSchemaAsync();
25+
26+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
27+
var error = exception.Errors.First();
28+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
29+
Assert.Equal(
30+
"Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
31+
error.Message);
32+
}
33+
34+
[Fact]
35+
public async Task Microsoft_AllowAnonymousAttribute_On_Method_Produces_Error()
36+
{
37+
var builder = new ServiceCollection()
38+
.AddGraphQL()
39+
.AddQueryType<QueryWithMicrosoftAllowAnonymousAttributeOnMethod>()
40+
.AddAuthorizationCore();
41+
42+
var act = async () => await builder.BuildSchemaAsync();
43+
44+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
45+
var error = exception.Errors.First();
46+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
47+
Assert.Equal(
48+
"Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
49+
error.Message);
50+
}
51+
52+
[Fact]
53+
public async Task Microsoft_AuthorizeAttribute_On_Type_Produces_Error()
54+
{
55+
var builder = new ServiceCollection()
56+
.AddGraphQL()
57+
.AddQueryType<QueryWithMicrosoftAuthorizeAttribute>()
58+
.AddAuthorizationCore();
59+
60+
var act = async () => await builder.BuildSchemaAsync();
61+
62+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
63+
var error = exception.Errors.First();
64+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
65+
Assert.Equal(
66+
"Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttribute`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
67+
error.Message);
68+
}
69+
70+
[Fact]
71+
public async Task Microsoft_AllowAnonymousAttribute_On_Type_Produces_Error()
72+
{
73+
var builder = new ServiceCollection()
74+
.AddGraphQL()
75+
.AddQueryType<QueryWithMicrosoftAllowAnonymousAttribute>()
76+
.AddAuthorizationCore();
77+
78+
var act = async () => await builder.BuildSchemaAsync();
79+
80+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
81+
var error = exception.Errors.First();
82+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
83+
Assert.Equal(
84+
"Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttribute`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
85+
error.Message);
86+
}
87+
1688
[Fact]
1789
public async Task Authorize_Person_NoAccess()
1890
{
@@ -1138,4 +1210,28 @@ public ValueTask<AuthorizeResult> AuthorizeAsync(
11381210
CancellationToken cancellationToken = default)
11391211
=> new(AuthorizeResult.NotAllowed);
11401212
}
1213+
1214+
public class QueryWithMicrosoftAuthorizeAttributeOnMethod
1215+
{
1216+
[Microsoft.AspNetCore.Authorization.Authorize]
1217+
public string Field() => "foo";
1218+
}
1219+
1220+
public class QueryWithMicrosoftAllowAnonymousAttributeOnMethod
1221+
{
1222+
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
1223+
public string Field() => "foo";
1224+
}
1225+
1226+
[Microsoft.AspNetCore.Authorization.Authorize]
1227+
public class QueryWithMicrosoftAuthorizeAttribute
1228+
{
1229+
public string Field() => "foo";
1230+
}
1231+
1232+
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
1233+
public class QueryWithMicrosoftAllowAnonymousAttribute
1234+
{
1235+
public string Field() => "foo";
1236+
}
11411237
}

0 commit comments

Comments
 (0)