Skip to content

Commit 2161872

Browse files
authored
Update middleware with options and new design (#774)
1 parent 6bba2cb commit 2161872

27 files changed

+1219
-389
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22

33
<PropertyGroup>
4-
<VersionPrefix>6.0.0-preview</VersionPrefix>
4+
<VersionPrefix>7.0.0-preview</VersionPrefix>
55
<LangVersion>latest</LangVersion>
66
<PackageLicenseExpression>MIT</PackageLicenseExpression>
77
<PackageIcon>logo.64x64.png</PackageIcon>
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#nullable enable
2+
3+
using System.Diagnostics;
14
using GraphQL.Server.Transports.AspNetCore;
5+
using GraphQL.Transport;
26
using GraphQL.Types;
37

48
namespace GraphQL.Samples.Server;
@@ -10,32 +14,28 @@ public class GraphQLHttpMiddlewareWithLogs<TSchema> : GraphQLHttpMiddleware<TSch
1014
private readonly ILogger _logger;
1115

1216
public GraphQLHttpMiddlewareWithLogs(
13-
ILogger<GraphQLHttpMiddleware<TSchema>> logger,
14-
IGraphQLTextSerializer requestDeserializer)
15-
: base(requestDeserializer)
17+
RequestDelegate next,
18+
IGraphQLTextSerializer serializer,
19+
IDocumentExecuter<TSchema> documentExecuter,
20+
IServiceScopeFactory serviceScopeFactory,
21+
GraphQLHttpMiddlewareOptions options,
22+
ILogger<GraphQLHttpMiddleware<TSchema>> logger)
23+
: base(next, serializer, documentExecuter, serviceScopeFactory, options)
1624
{
1725
_logger = logger;
1826
}
1927

20-
protected override Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult)
28+
protected override async Task<ExecutionResult> ExecuteRequestAsync(HttpContext context, GraphQLRequest? request, IServiceProvider serviceProvider, IDictionary<string, object?> userContext)
2129
{
22-
if (requestExecutionResult.Result.Errors != null)
30+
var timer = Stopwatch.StartNew();
31+
var ret = await base.ExecuteRequestAsync(context, request, serviceProvider, userContext);
32+
if (ret.Errors != null)
2333
{
24-
if (requestExecutionResult.IndexInBatch.HasValue)
25-
_logger.LogError("GraphQL execution completed in {Elapsed} with error(s) in batch [{Index}]: {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.IndexInBatch, requestExecutionResult.Result.Errors);
26-
else
27-
_logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.Result.Errors);
34+
_logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", timer.Elapsed, ret.Errors);
2835
}
2936
else
30-
_logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", requestExecutionResult.Elapsed);
31-
32-
return base.RequestExecutedAsync(requestExecutionResult);
33-
}
37+
_logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", timer.Elapsed);
3438

35-
protected override CancellationToken GetCancellationToken(HttpContext context)
36-
{
37-
// custom CancellationToken example
38-
var cts = CancellationTokenSource.CreateLinkedTokenSource(base.GetCancellationToken(context), new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
39-
return cts.Token;
39+
return ret;
4040
}
4141
}

samples/Samples.Server/Startup.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using GraphQL.Samples.Schemas.Chat;
55
using GraphQL.Server;
66
using GraphQL.Server.Authorization.AspNetCore;
7+
using GraphQL.Server.Transports.AspNetCore;
78
using GraphQL.Server.Ui.Altair;
89
using GraphQL.Server.Ui.GraphiQL;
910
using GraphQL.Server.Ui.Playground;
@@ -34,7 +35,6 @@ public void ConfigureServices(IServiceCollection services)
3435

3536
services.AddGraphQL(builder => builder
3637
.AddApolloTracing()
37-
.AddHttpMiddleware<ChatSchema, GraphQLHttpMiddlewareWithLogs<ChatSchema>>()
3838
.AddWebSocketsHttpMiddleware<ChatSchema>()
3939
.AddSchema<ChatSchema>()
4040
.ConfigureExecutionOptions(options =>
@@ -63,7 +63,7 @@ public void Configure(IApplicationBuilder app)
6363
app.UseWebSockets();
6464

6565
app.UseGraphQLWebSockets<ChatSchema>();
66-
app.UseGraphQL<ChatSchema, GraphQLHttpMiddlewareWithLogs<ChatSchema>>();
66+
app.UseGraphQL<GraphQLHttpMiddlewareWithLogs<ChatSchema>>("/graphql", new GraphQLHttpMiddlewareOptions());
6767

6868
app.UseGraphQLPlayground(new PlaygroundOptions
6969
{

samples/Samples.Server/StartupWithRouting.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public void ConfigureServices(IServiceCollection services)
3535

3636
services.AddGraphQL(builder => builder
3737
.AddApolloTracing()
38-
.AddHttpMiddleware<ChatSchema, GraphQLHttpMiddlewareWithLogs<ChatSchema>>()
3938
.AddWebSocketsHttpMiddleware<ChatSchema>()
4039
.AddSchema<ChatSchema>()
4140
.ConfigureExecutionOptions(options =>
@@ -69,7 +68,7 @@ public void Configure(IApplicationBuilder app)
6968
app.UseEndpoints(endpoints =>
7069
{
7170
endpoints.MapGraphQLWebSockets<ChatSchema>();
72-
endpoints.MapGraphQL<ChatSchema, GraphQLHttpMiddlewareWithLogs<ChatSchema>>();
71+
endpoints.MapGraphQL<GraphQLHttpMiddlewareWithLogs<ChatSchema>>();
7372

7473
endpoints.MapGraphQLPlayground(new PlaygroundOptions
7574
{
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#nullable enable
2+
3+
using System.Security.Claims;
4+
using System.Security.Principal;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace GraphQL.Server.Transports.AspNetCore;
9+
10+
/// <summary>
11+
/// Helper methods for performing connection authorization.
12+
/// </summary>
13+
public static class AuthorizationHelper
14+
{
15+
/// <summary>
16+
/// Performs connection authorization according to the options set within
17+
/// <see cref="AuthorizationParameters{TState}"/>. Returns <see langword="true"/>
18+
/// if authorization was successful or not required.
19+
/// </summary>
20+
public static async ValueTask<bool> AuthorizeAsync<TState>(AuthorizationParameters<TState> options, TState state)
21+
{
22+
if (options.AuthorizationRequired)
23+
{
24+
if (!((options.HttpContext.User ?? NoUser()).Identity ?? NoIdentity()).IsAuthenticated)
25+
{
26+
if (options.OnNotAuthenticated != null)
27+
await options.OnNotAuthenticated(state);
28+
return false;
29+
}
30+
}
31+
32+
if (options.AuthorizedRoles?.Count > 0)
33+
{
34+
var user = options.HttpContext.User ?? NoUser();
35+
foreach (var role in options.AuthorizedRoles)
36+
{
37+
if (user.IsInRole(role))
38+
goto PassRoleCheck;
39+
}
40+
if (options.OnNotAuthorizedRole != null)
41+
await options.OnNotAuthorizedRole(state);
42+
return false;
43+
}
44+
PassRoleCheck:
45+
46+
if (options.AuthorizedPolicy != null)
47+
{
48+
var authorizationService = options.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
49+
var authResult = await authorizationService.AuthorizeAsync(options.HttpContext.User ?? NoUser(), null, options.AuthorizedPolicy);
50+
if (!authResult.Succeeded)
51+
{
52+
if (options.OnNotAuthorizedPolicy != null)
53+
await options.OnNotAuthorizedPolicy(state, authResult);
54+
return false;
55+
}
56+
}
57+
58+
return true;
59+
}
60+
61+
private static IIdentity NoIdentity()
62+
=> throw new InvalidOperationException($"IIdentity could not be retrieved from HttpContext.User.Identity.");
63+
64+
private static ClaimsPrincipal NoUser()
65+
=> throw new InvalidOperationException("ClaimsPrincipal could not be retrieved from HttpContext.User.");
66+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#nullable enable
2+
3+
using System.Security.Claims;
4+
using System.Security.Principal;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace GraphQL.Server.Transports.AspNetCore;
9+
10+
/// <summary>
11+
/// Authorization parameters.
12+
/// This struct is used to group all necessary parameters together and perform arbitrary
13+
/// actions based on provided authentication properties/attributes/etc.
14+
/// It is not intended to be called from user code.
15+
/// </summary>
16+
public readonly struct AuthorizationParameters<TState>
17+
{
18+
/// <summary>
19+
/// Initializes an instance with a specified <see cref="Microsoft.AspNetCore.Http.HttpContext"/>
20+
/// and parameters copied from the specified instance of <see cref="GraphQLHttpMiddlewareOptions"/>.
21+
/// </summary>
22+
public AuthorizationParameters(
23+
HttpContext httpContext,
24+
GraphQLHttpMiddlewareOptions middlewareOptions,
25+
Func<TState, Task>? onNotAuthenticated,
26+
Func<TState, Task>? onNotAuthorizedRole,
27+
Func<TState, AuthorizationResult, Task>? onNotAuthorizedPolicy)
28+
{
29+
HttpContext = httpContext;
30+
AuthorizationRequired = middlewareOptions.AuthorizationRequired;
31+
AuthorizedRoles = middlewareOptions.AuthorizedRoles;
32+
AuthorizedPolicy = middlewareOptions.AuthorizedPolicy;
33+
OnNotAuthenticated = onNotAuthenticated;
34+
OnNotAuthorizedRole = onNotAuthorizedRole;
35+
OnNotAuthorizedPolicy = onNotAuthorizedPolicy;
36+
}
37+
38+
/// <summary>
39+
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.HttpContext"/> for the request.
40+
/// </summary>
41+
public HttpContext HttpContext { get; }
42+
43+
/// <inheritdoc cref="GraphQLHttpMiddlewareOptions.AuthorizationRequired"/>
44+
public bool AuthorizationRequired { get; }
45+
46+
/// <inheritdoc cref="GraphQLHttpMiddlewareOptions.AuthorizedRoles"/>
47+
public List<string>? AuthorizedRoles { get; }
48+
49+
/// <inheritdoc cref="GraphQLHttpMiddlewareOptions.AuthorizedPolicy"/>
50+
public string? AuthorizedPolicy { get; }
51+
52+
/// <summary>
53+
/// A delegate which executes if <see cref="AuthorizationRequired"/> is set
54+
/// but <see cref="IIdentity.IsAuthenticated"/> returns <see langword="false"/>.
55+
/// </summary>
56+
public Func<TState, Task>? OnNotAuthenticated { get; }
57+
58+
/// <summary>
59+
/// A delegate which executes if <see cref="AuthorizedRoles"/> is set but
60+
/// <see cref="ClaimsPrincipal.IsInRole(string)"/> returns <see langword="false"/>
61+
/// for all roles.
62+
/// </summary>
63+
public Func<TState, Task>? OnNotAuthorizedRole { get; }
64+
65+
/// <summary>
66+
/// A delegate which executes if <see cref="AuthorizedPolicy"/> is set but
67+
/// <see cref="IAuthorizationService.AuthorizeAsync(ClaimsPrincipal, object, string)"/>
68+
/// returns an unsuccessful <see cref="AuthorizationResult"/> for the specified policy.
69+
/// </summary>
70+
public Func<TState, AuthorizationResult, Task>? OnNotAuthorizedPolicy { get; }
71+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#nullable enable
2+
3+
using GraphQL.Validation;
4+
using GraphQLParser.AST;
5+
using Microsoft.AspNetCore.Authorization;
6+
7+
namespace GraphQL.Server.Transports.AspNetCore.Errors;
8+
9+
/// <summary>
10+
/// Represents an error indicating that the user is not allowed access to the specified resource.
11+
/// </summary>
12+
public class AccessDeniedError : ValidationError
13+
{
14+
/// <inheritdoc cref="AccessDeniedError"/>
15+
public AccessDeniedError(string resource)
16+
: base($"Access denied for {resource}.")
17+
{
18+
}
19+
20+
/// <inheritdoc cref="AccessDeniedError"/>
21+
public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params ASTNode[] nodes)
22+
: base(originalQuery, null!, $"Access denied for {resource}.", nodes)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// Returns the policy that would allow access to these node(s).
28+
/// </summary>
29+
public string? PolicyRequired { get; set; }
30+
31+
/// <inheritdoc cref="AuthorizationResult"/>
32+
public AuthorizationResult? PolicyAuthorizationResult { get; set; }
33+
34+
/// <summary>
35+
/// Returns the list of role memberships that would allow access to these node(s).
36+
/// </summary>
37+
public List<string>? RolesRequired { get; set; }
38+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#nullable enable
2+
3+
using GraphQL.Execution;
4+
5+
namespace GraphQL.Server.Transports.AspNetCore.Errors;
6+
7+
/// <summary>
8+
/// Represents an error indicating that batched requests are not supported.
9+
/// </summary>
10+
public class BatchedRequestsNotSupportedError : RequestError
11+
{
12+
/// <inheritdoc cref="BatchedRequestsNotSupportedError"/>
13+
public BatchedRequestsNotSupportedError() : base("Batched requests are not supported.") { }
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#nullable enable
2+
3+
using GraphQL.Validation;
4+
using GraphQLParser.AST;
5+
6+
namespace GraphQL.Server.Transports.AspNetCore.Errors;
7+
8+
/// <summary>
9+
/// Represents a validation error indicating that the requested operation is not valid
10+
/// for the type of HTTP request.
11+
/// </summary>
12+
public class HttpMethodValidationError : ValidationError
13+
{
14+
/// <inheritdoc cref="HttpMethodValidationError"/>
15+
public HttpMethodValidationError(GraphQLParser.ROM originalQuery, ASTNode node, string message)
16+
: base(originalQuery, null!, message, node)
17+
{
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#nullable enable
2+
3+
using GraphQL.Execution;
4+
5+
namespace GraphQL.Server.Transports.AspNetCore.Errors;
6+
7+
/// <summary>
8+
/// Represents an error indicating that the content-type is invalid, for example, could not be parsed or is not supported.
9+
/// </summary>
10+
public class InvalidContentTypeError : RequestError
11+
{
12+
/// <inheritdoc cref="InvalidContentTypeError"/>
13+
public InvalidContentTypeError() : base("Invalid 'Content-Type' header.") { }
14+
15+
/// <inheritdoc cref="InvalidContentTypeError"/>
16+
public InvalidContentTypeError(string message) : base("Invalid 'Content-Type' header: " + message) { }
17+
}

0 commit comments

Comments
 (0)