Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added @allowAnonymous directive #6134

Merged
merged 1 commit into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum ApplyPolicy {
VALIDATION
}

directive @allowAnonymous repeatable on FIELD_DEFINITION

directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,9 @@ public static class WellKnownContextData
/// The key to access the authorization handler on the global context.
/// </summary>
public const string AuthorizationHandler = "HotChocolate.Authorization.AuthorizationHandler";

/// <summary>
/// The key to access the authorization allowed flag on the member context.
/// </summary>
public const string AllowAnonymous = "HotChocolate.Authorization.AllowAnonymous";
}
17 changes: 17 additions & 0 deletions src/HotChocolate/Core/src/Authorization/AllowAnonymousAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Reflection;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;

namespace HotChocolate.Authorization;

/// <summary>
/// Allows anonymous access to the annotated field.
/// </summary>
public sealed class AllowAnonymousAttribute : ObjectFieldDescriptorAttribute
{
protected override void OnConfigure(
IDescriptorContext context,
IObjectFieldDescriptor descriptor,
MemberInfo member)
=> descriptor.AllowAnonymous();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
using DirectiveLocation = HotChocolate.Types.DirectiveLocation;

namespace HotChocolate.Authorization;

internal sealed class AllowAnonymousDirectiveType
: DirectiveType
, ISchemaDirective
{
public AllowAnonymousDirectiveType()
{
Name = Names.AllowAnonymous;
}

protected override void Configure(IDirectiveTypeDescriptor descriptor)
{
descriptor
.Name(Names.AllowAnonymous)
.Location(DirectiveLocation.FieldDefinition)
.Repeatable()
.Internal();
}

public void ApplyConfiguration(
IDescriptorContext context,
DirectiveNode directiveNode,
IDefinition definition,
Stack<IDefinition> path)
{
((IHasDirectiveDefinition)definition).Directives.Add(new(directiveNode));

if (definition is ObjectFieldDefinition fieldDef)
{
fieldDef.ContextData[WellKnownContextData.AllowAnonymous] = true;
}
}

public static class Names
{
public const string AllowAnonymous = "allowAnonymous";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,20 @@ private void InspectObjectTypesForAuthDirective(State state)
{
foreach (var fieldDef in type.TypeDef.Fields)
{
// we are not interested in introspection fields or the node fields.
if (fieldDef.IsIntrospectionField || fieldDef.IsNodeField())
{
continue;
}

ApplyAuthMiddleware(
fieldDef,
registration,
false);
// if the field contains the AnonymousAllowed flag we will not
// apply authorization on it.
if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
{
continue;
}

ApplyAuthMiddleware(fieldDef, registration, false);
}
}

Expand Down Expand Up @@ -338,6 +343,13 @@ private void ApplyAuthMiddleware(
ObjectFieldDefinition fieldDef,
State state)
{
// if the field contains the AnonymousAllowed flag we will not apply authorization
// on it.
if(fieldDef.GetContextData().ContainsKey(AllowAnonymous))
{
return;
}

var isNodeField = fieldDef.IsNodeField();

if (fieldDef.Type is not null &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using HotChocolate.Language;
using HotChocolate.Resolvers;
Expand Down Expand Up @@ -51,8 +50,7 @@ protected override void Configure(IDirectiveTypeDescriptor<AuthorizeDirective> d
.Type<NonNullType<ApplyPolicyType>>()
.DefaultValue(ApplyPolicy.BeforeResolver);

var context = descriptor.Extend().Context;
descriptor.Use(CreateMiddleware(context.Services));
descriptor.Use(CreateMiddleware());
}

public void ApplyConfiguration(
Expand Down Expand Up @@ -88,16 +86,13 @@ arg.Value is EnumValueNode value &&
}
}

private static DirectiveMiddleware CreateMiddleware(
IServiceProvider schemaServices)
{
return (next, directive) =>
private static DirectiveMiddleware CreateMiddleware()
=> (next, directive) =>
{
var value = directive.AsValue<AuthorizeDirective>();
var auth = new AuthorizeMiddleware(next, value);
return async context => await auth.InvokeAsync(context).ConfigureAwait(false);
};
}

public static class Names
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,29 @@ public static IObjectFieldDescriptor Authorize(

return descriptor.Directive(new AuthorizeDirective(roles));
}

/// <summary>
/// Allows anonymous access to this field.
/// </summary>
/// <param name="descriptor">
/// The field descriptor.
/// </param>
/// <returns>
/// Returns the <see cref="IObjectFieldDescriptor"/> for configuration chaining.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IObjectFieldDescriptor AllowAnonymous(
this IObjectFieldDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}

descriptor.Directive(AllowAnonymousDirectiveType.Names.AllowAnonymous);
descriptor.Extend().Definition.ContextData[WellKnownContextData.AllowAnonymous] = true;
return descriptor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ public static ISchemaBuilder AddAuthorizeDirectiveType(this ISchemaBuilder build
throw new ArgumentNullException(nameof(builder));
}

var type = new AuthorizeDirectiveType();
var authorize = new AuthorizeDirectiveType();
var allowAnonymous = new AllowAnonymousDirectiveType();

return builder
.AddDirectiveType(type)
.TryAddSchemaDirective(type)
.AddDirectiveType(authorize)
.AddDirectiveType(allowAnonymous)
.TryAddSchemaDirective(authorize)
.TryAddSchemaDirective(allowAnonymous)
.TryAddTypeInterceptor<AuthorizationTypeInterceptor>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,63 @@ public async Task Authorize_Query_NoAccess()
Assert.Equal(401, value);
}

[Fact]
public async Task Authorize_Person_AllowAnonymous()
{
// arrange
var handler = new AuthHandler(
resolver: AuthorizeResult.NotAllowed,
validation: AuthorizeResult.Allowed);
var services = CreateServices(handler);
var executor = await services.GetRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(
"""
{
person(id: "UGVyc29uCmRhYmM=") {
name
}
person2(id: "UGVyc29uCmRhYmM=") {
name
}
}
""");

// assert
Snapshot
.Create()
.Add(result)
.MatchInline(
"""
{
"errors": [
{
"message": "The current user is not authorized to access this resource.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"person"
],
"extensions": {
"code": "AUTH_NOT_AUTHORIZED"
}
}
],
"data": {
"person": null,
"person2": {
"name": "Joe"
}
}
}
""");
}

[Fact]
public async Task Authorize_CityOrStreet_Skip_Auth_When_Street()
{
Expand Down Expand Up @@ -863,12 +920,17 @@ private static IServiceProvider CreateServices(

[FooDirective]
[Authorize("QUERY", ApplyPolicy.Validation)]
[Authorize("QUERY2", ApplyPolicy.BeforeResolver)]
public sealed class Query
{
[NodeResolver]
public Person? GetPerson(string id)
=> new(id, "Joe");

[AllowAnonymous]
public Person? GetPerson2(string id)
=> new(id, "Joe");

public ICityOrStreet? GetCityOrStreet(bool street)
=> street
? new Street("Somewhere")
Expand Down