Skip to content

Commit

Permalink
Added allowAnnonymous directive (#6134)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed May 9, 2023
1 parent 21e0228 commit ff14574
Show file tree
Hide file tree
Showing 20 changed files with 204 additions and 15 deletions.
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

0 comments on commit ff14574

Please sign in to comment.