Skip to content

Commit a453980

Browse files
Add ErrorOnAspNetCoreAuthorizationAttributes option
1 parent 103078d commit a453980

File tree

7 files changed

+238
-4
lines changed

7 files changed

+238
-4
lines changed

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

Lines changed: 106 additions & 2 deletions
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;
@@ -8,11 +9,19 @@
89
using HotChocolate.Types.Relay;
910
using HotChocolate.Utilities;
1011
using static HotChocolate.Authorization.AuthorizeDirectiveType.Names;
12+
using static HotChocolate.Authorization.Properties.AuthCoreResources;
1113

1214
namespace HotChocolate.Authorization;
1315

1416
internal sealed partial class AuthorizationTypeInterceptor : TypeInterceptor
1517
{
18+
private const string AspNetCoreAuthorizeAttributeName = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute";
19+
private const string AspNetCoreAllowAnonymousAttributeName =
20+
"Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
21+
22+
private static readonly string s_authorizeAttributeName = typeof(AuthorizeAttribute).FullName!;
23+
private static readonly string s_allowAnonymousAttributeName = typeof(AllowAnonymousAttribute).FullName!;
24+
1625
private readonly List<ObjectTypeInfo> _objectTypes = [];
1726
private readonly List<UnionTypeInfo> _unionTypes = [];
1827
private readonly Dictionary<ObjectType, DirectiveCollection> _directives = [];
@@ -121,14 +130,79 @@ public override void OnBeforeCompleteMetadata(
121130
ITypeCompletionContext completionContext,
122131
TypeSystemConfiguration configuration)
123132
{
133+
if (configuration is not ObjectTypeConfiguration typeDef)
134+
{
135+
return;
136+
}
137+
124138
// last in the initialization we need to intercept the query type and ensure that
125139
// authorization configuration is applied to the special introspection and node fields.
126-
if (ReferenceEquals(_queryContext, completionContext) &&
127-
configuration is ObjectTypeConfiguration typeDef)
140+
if (ReferenceEquals(_queryContext, completionContext))
128141
{
129142
var state = _state ?? throw ThrowHelper.StateNotInitialized();
130143
HandleSpecialQueryFields(new ObjectTypeInfo(completionContext, typeDef), state);
131144
}
145+
146+
if (_context.Options.ErrorOnAspNetCoreAuthorizationAttributes && !completionContext.IsIntrospectionType)
147+
{
148+
var runtimeType = typeDef.RuntimeType;
149+
var attributesOnType = runtimeType.GetCustomAttributes().ToArray();
150+
151+
if (ContainsNamedAttribute(attributesOnType, AspNetCoreAuthorizeAttributeName))
152+
{
153+
completionContext.ReportError(
154+
UnsupportedAspNetCoreAttributeError(
155+
AspNetCoreAuthorizeAttributeName,
156+
s_authorizeAttributeName,
157+
runtimeType));
158+
return;
159+
}
160+
161+
if (ContainsNamedAttribute(attributesOnType, AspNetCoreAllowAnonymousAttributeName))
162+
{
163+
completionContext.ReportError(
164+
UnsupportedAspNetCoreAttributeError(
165+
AspNetCoreAllowAnonymousAttributeName,
166+
s_allowAnonymousAttributeName,
167+
runtimeType));
168+
return;
169+
}
170+
171+
foreach (var field in typeDef.Fields)
172+
{
173+
if (field.IsIntrospectionField)
174+
{
175+
continue;
176+
}
177+
178+
var fieldMember = field.ResolverMember ?? field.Member;
179+
180+
if (fieldMember is not null)
181+
{
182+
var attributesOnResolver = fieldMember.GetCustomAttributes().ToArray();
183+
184+
if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAuthorizeAttributeName))
185+
{
186+
completionContext.ReportError(
187+
UnsupportedAspNetCoreAttributeError(
188+
AspNetCoreAuthorizeAttributeName,
189+
s_authorizeAttributeName,
190+
fieldMember));
191+
return;
192+
}
193+
194+
if (ContainsNamedAttribute(attributesOnResolver, AspNetCoreAllowAnonymousAttributeName))
195+
{
196+
completionContext.ReportError(
197+
UnsupportedAspNetCoreAttributeError(
198+
AspNetCoreAllowAnonymousAttributeName,
199+
s_allowAnonymousAttributeName,
200+
fieldMember));
201+
return;
202+
}
203+
}
204+
}
205+
}
132206
}
133207

134208
public override void OnAfterMakeExecutable()
@@ -620,6 +694,36 @@ private static bool IsAuthorizedType<T>(T definition)
620694

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

625729
file static class AuthorizationTypeInterceptorExtensions

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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,10 @@ public interface IReadOnlySchemaOptions
207207
/// to the DataLoader promise cache.
208208
/// </summary>
209209
bool PublishRootFieldPagesToPromiseCache { get; }
210+
211+
/// <summary>
212+
/// Errors if either an ASP.NET Core [Authorize] or [AllowAnonymous] attribute
213+
/// is used on a Hot Chocolate resolver or type definition.
214+
/// </summary>
215+
bool ErrorOnAspNetCoreAuthorizationAttributes { get; }
210216
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ public FieldBindingFlags DefaultFieldBindingFlags
137137
/// <inheritdoc cref="IReadOnlySchemaOptions.PublishRootFieldPagesToPromiseCache"/>
138138
public bool PublishRootFieldPagesToPromiseCache { get; set; } = true;
139139

140+
/// <inheritdoc cref="IReadOnlySchemaOptions.ErrorOnAspNetCoreAuthorizationAttributes"/>
141+
public bool ErrorOnAspNetCoreAuthorizationAttributes { get; set; } = true;
142+
140143
/// <summary>
141144
/// Creates a mutable options object from a read-only options object.
142145
/// </summary>
@@ -175,7 +178,8 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
175178
StripLeadingIFromInterface = options.StripLeadingIFromInterface,
176179
EnableTag = options.EnableTag,
177180
DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope,
178-
DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope
181+
DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope,
182+
ErrorOnAspNetCoreAuthorizationAttributes = options.ErrorOnAspNetCoreAuthorizationAttributes
179183
};
180184
}
181185
}

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,78 @@ public async Task Skip_After_Validation_For_Null()
955955
""");
956956
}
957957

958+
[Fact]
959+
public async Task Microsoft_AuthorizeAttribute_On_Method_Produces_Error()
960+
{
961+
var builder = new ServiceCollection()
962+
.AddGraphQL()
963+
.AddQueryType<QueryWithMicrosoftAuthorizeAttributeOnMethod>()
964+
.AddAuthorizationCore();
965+
966+
var act = async () => await builder.BuildSchemaAsync();
967+
968+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
969+
var error = exception.Errors.First();
970+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
971+
Assert.Equal(
972+
"Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
973+
error.Message);
974+
}
975+
976+
[Fact]
977+
public async Task Microsoft_AllowAnonymousAttribute_On_Method_Produces_Error()
978+
{
979+
var builder = new ServiceCollection()
980+
.AddGraphQL()
981+
.AddQueryType<QueryWithMicrosoftAllowAnonymousAttributeOnMethod>()
982+
.AddAuthorizationCore();
983+
984+
var act = async () => await builder.BuildSchemaAsync();
985+
986+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
987+
var error = exception.Errors.First();
988+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
989+
Assert.Equal(
990+
"Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttributeOnMethod.Field`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
991+
error.Message);
992+
}
993+
994+
[Fact]
995+
public async Task Microsoft_AuthorizeAttribute_On_Type_Produces_Error()
996+
{
997+
var builder = new ServiceCollection()
998+
.AddGraphQL()
999+
.AddQueryType<QueryWithMicrosoftAuthorizeAttribute>()
1000+
.AddAuthorizationCore();
1001+
1002+
var act = async () => await builder.BuildSchemaAsync();
1003+
1004+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
1005+
var error = exception.Errors.First();
1006+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
1007+
Assert.Equal(
1008+
"Found unsupported `Microsoft.AspNetCore.Authorization.AuthorizeAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAuthorizeAttribute`. Use `HotChocolate.Authorization.AuthorizeAttribute` instead.",
1009+
error.Message);
1010+
}
1011+
1012+
[Fact]
1013+
public async Task Microsoft_AllowAnonymousAttribute_On_Type_Produces_Error()
1014+
{
1015+
var builder = new ServiceCollection()
1016+
.AddGraphQL()
1017+
.AddQueryType<QueryWithMicrosoftAllowAnonymousAttribute>()
1018+
.AddAuthorizationCore();
1019+
1020+
var act = async () => await builder.BuildSchemaAsync();
1021+
1022+
var exception = await Assert.ThrowsAsync<SchemaException>(act);
1023+
var error = exception.Errors.First();
1024+
Assert.Equal(ErrorCodes.Schema.UnsupportedAspNetCoreAuthorizationAttribute, error.Code);
1025+
Assert.Equal(
1026+
"Found unsupported `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` on `HotChocolate.Authorization.AnnotationBasedAuthorizationTests+QueryWithMicrosoftAllowAnonymousAttribute`. Use `HotChocolate.Authorization.AllowAnonymousAttribute` instead.",
1027+
error.Message);
1028+
}
1029+
9581030
private static IServiceProvider CreateServices(
9591031
IAuthorizationHandler handler,
9601032
Action<AuthorizationOptions>? configure = null)
@@ -971,6 +1043,30 @@ private static IServiceProvider CreateServices(
9711043
.Services
9721044
.BuildServiceProvider();
9731045

1046+
public class QueryWithMicrosoftAuthorizeAttributeOnMethod
1047+
{
1048+
[Microsoft.AspNetCore.Authorization.Authorize]
1049+
public string Field() => "foo";
1050+
}
1051+
1052+
public class QueryWithMicrosoftAllowAnonymousAttributeOnMethod
1053+
{
1054+
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
1055+
public string Field() => "foo";
1056+
}
1057+
1058+
[Microsoft.AspNetCore.Authorization.Authorize]
1059+
public class QueryWithMicrosoftAuthorizeAttribute
1060+
{
1061+
public string Field() => "foo";
1062+
}
1063+
1064+
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
1065+
public class QueryWithMicrosoftAllowAnonymousAttribute
1066+
{
1067+
public string Field() => "foo";
1068+
}
1069+
9741070
[FooDirective]
9751071
[Authorize("QUERY", ApplyPolicy.Validation)]
9761072
[Authorize("QUERY2", ApplyPolicy.BeforeResolver)]

src/HotChocolate/Primitives/src/Primitives/ErrorCodes.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ public static class Schema
253253
/// The specified directive argument does not exist.
254254
/// </summary>
255255
public const string UnknownDirectiveArgument = "HC0072";
256+
257+
/// <summary>
258+
/// An underlying schema runtime type / member is annotated with a
259+
/// Microsoft.AspNetCore.Authorization.* attribute that is not supported by Hot Chocolate.
260+
/// </summary>
261+
public const string UnsupportedAspNetCoreAuthorizationAttribute = "HC0090";
256262
}
257263

258264
public static class Scalars
@@ -264,7 +270,7 @@ public static class Scalars
264270

265271
/// <summary>
266272
/// Either the syntax node is invalid when parsing the literal or the syntax
267-
/// node value has an invalid format.
273+
/// node value has an invalid format.`
268274
/// </summary>
269275
public const string InvalidSyntaxFormat = "HC0002";
270276
}

0 commit comments

Comments
 (0)