Skip to content
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 @@ -2,6 +2,7 @@
using System.Runtime.InteropServices;
using HotChocolate.Configuration;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Configurations;
Expand Down Expand Up @@ -133,6 +134,8 @@ public override void OnBeforeCompleteMetadata(

public override void OnAfterMakeExecutable()
{
var queryType = (ObjectType)_queryContext.Type;

foreach (var type in _objectTypes)
{
var objectType = (ObjectType)type.TypeReg.Type;
Expand All @@ -142,26 +145,40 @@ public override void OnAfterMakeExecutable()
continue;
}

var pipeline = nodeResolverInfo.Pipeline;
var directives = objectType.Directives;
var length = directives.Count;
ref var start = ref directives.GetReference();
var pipeline = ApplyAuthMiddleware(nodeResolverInfo.Pipeline, objectType.Directives);

for (var i = length - 1; i >= 0; i--)
// a query field that doubles as node resolver already
// applies the authorization of the query type.
if (!nodeResolverInfo.IsQueryFieldResolver)
{
var directive = Unsafe.Add(ref start, i);

if (directive.Type.Name.EqualsOrdinal(Authorize))
{
var authDir = directive.ToValue<AuthorizeDirective>();
pipeline = CreateAuthMiddleware(authDir).Middleware.Invoke(pipeline);
}
pipeline = ApplyAuthMiddleware(pipeline, queryType.Directives);
}

objectType.SetNodeResolver(new NodeResolverInfo(nodeResolverInfo.QueryField, pipeline));
}
}

private static FieldDelegate ApplyAuthMiddleware(
FieldDelegate pipeline,
DirectiveCollection directives)
{
var length = directives.Count;
ref var start = ref directives.GetReference();

for (var i = length - 1; i >= 0; i--)
{
var directive = Unsafe.Add(ref start, i);

if (directive.Type.Name.EqualsOrdinal(Authorize))
{
var authDir = directive.ToValue<AuthorizeDirective>();
pipeline = CreateAuthMiddleware(authDir).Middleware.Invoke(pipeline);
}
}

return pipeline;
}

private void InspectObjectTypesForAuthDirective(State state)
{
foreach (var type in _objectTypes)
Expand All @@ -181,7 +198,8 @@ private void InspectObjectTypesForAuthDirective(State state)
{
foreach (var fieldDef in type.TypeDef.Fields)
{
// we are not interested in introspection fields or the node fields.
// we are not interested in introspection fields. the node fields are
// authorized through the node resolver pipelines.
if (fieldDef.IsIntrospectionField || fieldDef.IsNodeField())
{
continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Security.Claims;
using System.Text;
using HotChocolate.Execution;
using HotChocolate.Resolvers;
using HotChocolate.Types;
Expand Down Expand Up @@ -774,7 +775,9 @@ public async Task Authorize_Nodes_Field_Different_Ids_BeforeResolver()
{
// arrange
var handler = new AuthHandler(
resolver: (_, _) => AuthorizeResult.NotAllowed,
resolver: (_, d) => d.Policy.EqualsOrdinal("QUERY2")
? AuthorizeResult.Allowed
: AuthorizeResult.NotAllowed,
validation: (_, _) => AuthorizeResult.Allowed);
var services = CreateServices(handler);
var executor = await services.GetRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);
Expand Down Expand Up @@ -839,6 +842,81 @@ public async Task Authorize_Nodes_Field_Different_Ids_BeforeResolver()
""");
}

[Theory]
[InlineData("query($id: ID!) { node(id: $id) { __typename } }", false, "node")]
[InlineData("query($id: [ID!]!) { nodes(ids: $id) { __typename } }", true, "nodes[0]")]
public async Task Node_Should_ApplyQueryTypePolicy_When_NodeTypeHasNoPolicy(
string document,
bool isList,
string expectedPath)
{
// arrange
var handler = new AuthHandler(
resolver: (_, d) => d.Policy.EqualsOrdinal("QUERY2")
? AuthorizeResult.NotAllowed
: AuthorizeResult.Allowed,
validation: (_, _) => AuthorizeResult.Allowed);
var services = CreateServices(handler);
var executor = await services.GetRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);
var id = Convert.ToBase64String("BlogPage:1"u8);

// act
var result = await executor.ExecuteAsync(
builder =>
builder
.SetDocument(document)
.SetVariableValues(new Dictionary<string, object?>
{
{ "id", isList ? new List<string> { id } : id }
}),
TestContext.Current.CancellationToken);

// assert
var error = Assert.Single(result.ExpectOperationResult().Errors);
Assert.Equal(ErrorCodes.Authentication.NotAuthorized, error.Code);
Assert.Equal(expectedPath, error.Path?.ToString());
}

[Theory]
[InlineData("query($id: ID!) { person(id: $id) { __typename } }", "Person:abc")]
[InlineData("query($id: ID!) { node(id: $id) { __typename } }", "Person:abc")]
[InlineData("query($id: ID!) { node(id: $id) { __typename } }", "BlogPage:1")]
public async Task Node_Should_ApplyQueryTypePolicyOnce_When_NodeIsResolved(
string document,
string id)
{
// arrange
var count = 0;
var handler = new AuthHandler(
resolver: (_, d) =>
{
if (d.Policy.EqualsOrdinal("QUERY2"))
{
Interlocked.Increment(ref count);
}

return AuthorizeResult.Allowed;
},
validation: (_, _) => AuthorizeResult.Allowed);
var services = CreateServices(handler);
var executor = await services.GetRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await executor.ExecuteAsync(
builder =>
builder
.SetDocument(document)
.SetVariableValues(new Dictionary<string, object?>
{
{ "id", Convert.ToBase64String(Encoding.UTF8.GetBytes(id)) }
}),
TestContext.Current.CancellationToken);

// assert
Assert.Empty(result.ExpectOperationResult().Errors);
Assert.Equal(1, count);
}

[Fact]
public async Task Skip_Authorize_On_Node_Field()
{
Expand Down
Loading