diff --git a/Directory.Build.props b/Directory.Build.props index ae8c276..a0bfecf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,7 +20,7 @@ true enable false - [5.3.3,6.0.0) + 7.0.0 $(NoWarn);IDE0056;IDE0057 diff --git a/README.md b/README.md index de83a0f..1217735 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,7 @@ for changes from previous versions. ### Typical configuration with HTTP middleware First add the `GraphQL.AspNetCore3` nuget package to your application. It requires -`GraphQL` version 5.3.3 or a later 5.x version. - -Second, install the `GraphQL.SystemTextJson` or `GraphQL.NewtonsoftJson` package within your -application if you have not already done so. For best performance, please use the -`GraphQL.SystemTextJson` package. +`GraphQL` version 7.0.0 or a later. Then update your `Program.cs` or `Startup.cs` to register the schema, the serialization engine, and optionally the HTTP middleware and WebSocket services. Configure WebSockets and GraphQL @@ -58,8 +54,7 @@ Below is a complete sample of a .NET 6 console app that hosts a GraphQL endpoint - - + @@ -264,14 +259,14 @@ if you allow anonymous requests. #### For individual graphs, fields and query arguments To configure ASP.NET Core authorization for GraphQL, add the corresponding -validation rule during GraphQL configuration, typically by calling `.AddAuthorization()` +validation rule during GraphQL configuration, typically by calling `.AddAuthorizationRule()` as shown below: ```csharp builder.Services.AddGraphQL(b => b .AddAutoSchema() .AddSystemTextJson() - .AddAuthorization()); + .AddAuthorizationRule()); ``` Both roles and policies are supported for output graph types, fields on output graph types, @@ -315,7 +310,7 @@ fields are marked with `AllowAnonymous`. This project does not include user interfaces, such as GraphiQL or Playground, but you can include references to the ones provided by the [GraphQL Server](https://github.com/graphql-dotnet/server) -repository which work well with ASP.Net Core 3.1+. Below is a list of the nuget packages offered: +repository which work well with ASP.Net Core 2.1+. Below is a list of the nuget packages offered: | Package | Downloads | NuGet Latest | |------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -408,12 +403,15 @@ and [this](http://www.breachattack.com/#howitworks) for more details. You may choose to use the .NET Core 2.1 runtime or a .NET Framework runtime. This library has been tested with .NET Core 2.1 and .NET Framework 4.8. -The only additional requirement is that you must add this code in your `Startup.cs` file: +One additional requirement is that you must add this code in your `Startup.cs` file: ```csharp services.AddHostApplicationLifetime(); ``` +You will also need to reference a serializer package such as `GraphQL.NewtonsoftJson` +or `GraphQL.SystemTextJson`, as `GraphQL.SystemTextJson` is not included in this case. + Besides that requirement, all features are supported in exactly the same manner as when using ASP.NET Core 3.1+. You may find differences in the ASP.NET Core runtime, such as CORS implementation differences, which are outside the scope of this project. @@ -513,7 +511,7 @@ and Subscription portions of your schema, as shown below: builder.Services.AddGraphQL(b => b .AddSchema() .AddSystemTextJson() - .AddAuthorization()); // add authorization validation rule + .AddAuthorizationRule()); // add authorization validation rule var app = builder.Build(); app.UseDeveloperExceptionPage(); diff --git a/migration.md b/migration.md index 09b29c2..79b56bd 100644 --- a/migration.md +++ b/migration.md @@ -1,5 +1,27 @@ # Version history / migration notes +## 5.0.0 + +GraphQL.AspNetCore3 v5 requires GraphQL.NET v7 or newer. + +`builder.AddAuthorization()` has been renamed to `builder.AddAuthorizationRule()`. +The old method has been marked as deprecated. + +The authorization validation rule and supporting methods have been changed to be +asynchronous, to match the new asynchronous signatures of `IValidationRule` in +GraphQL.NET v7. If you override any methods, they will need to be updated with +the new signature. + +The authorization rule now pulls `ClaimsPrincipal` indirectly from +`ExecutionOptions.User`. This value must be set properly from the ASP.NET middleware. +While the default implementation has this update in place, if you override +`GraphQLHttpMiddleware.ExecuteRequestAsync` or do not use the provided ASP.NET +middleware, you must set the value in your code. Another consequence of this +change is that the constructor of `AuthorizationValidationRule` does not require +`IHttpContextAccessor`, and `IHttpContextAccessor` is not required to be registered +within the dependency injection framework (previously provided automatically by +`builder.AddAuthorization()`). + ## 4.0.0 Remove `AllowEmptyQuery` option, as this error condition is now handled by the diff --git a/src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs b/src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs index f98f045..d06243f 100644 --- a/src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs +++ b/src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs @@ -9,21 +9,11 @@ namespace GraphQL.AspNetCore3; /// public class AuthorizationValidationRule : IValidationRule { - private readonly IHttpContextAccessor _contextAccessor; - - /// - public AuthorizationValidationRule(IHttpContextAccessor httpContextAccessor) - { - _contextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - } - /// - public virtual ValueTask ValidateAsync(ValidationContext context) + public virtual async ValueTask ValidateAsync(ValidationContext context) { - var httpContext = _contextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext could not be retrieved from IHttpContextAccessor."); - var user = httpContext.User - ?? throw new InvalidOperationException("ClaimsPrincipal could not be retrieved from HttpContext."); + var user = context.User + ?? throw new InvalidOperationException("User could not be retrieved from ValidationContext. Please be sure it is set in ExecutionOptions.User."); var provider = context.RequestServices ?? throw new MissingRequestServicesException(); var authService = provider.GetService() @@ -31,7 +21,7 @@ public AuthorizationValidationRule(IHttpContextAccessor httpContextAccessor) var visitor = new AuthorizationVisitor(context, user, authService); // if the schema fails authentication, report the error and do not perform any additional authorization checks. - return visitor.ValidateSchema(context) ? new(visitor) : default; + return await visitor.ValidateSchemaAsync(context) ? visitor : null; } } diff --git a/src/GraphQL.AspNetCore3/AuthorizationVisitor.cs b/src/GraphQL.AspNetCore3/AuthorizationVisitor.cs index 5b5e7a3..53b5135 100644 --- a/src/GraphQL.AspNetCore3/AuthorizationVisitor.cs +++ b/src/GraphQL.AspNetCore3/AuthorizationVisitor.cs @@ -35,6 +35,6 @@ protected override bool IsInRole(string role) => ClaimsPrincipal.IsInRole(role); /// - protected override AuthorizationResult Authorize(string policy) - => AuthorizationService.AuthorizeAsync(ClaimsPrincipal, policy).GetAwaiter().GetResult(); + protected override ValueTask AuthorizeAsync(string policy) + => new(AuthorizationService.AuthorizeAsync(ClaimsPrincipal, policy)); } diff --git a/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.Validation.cs b/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.Validation.cs index 5211aa0..d581c30 100644 --- a/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.Validation.cs +++ b/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.Validation.cs @@ -10,14 +10,14 @@ public partial class AuthorizationVisitorBase /// Validates authorization rules for the schema. /// Returns a value indicating if validation was successful. /// - public virtual bool ValidateSchema(ValidationContext context) - => Validate(context.Schema, null, context); + public virtual ValueTask ValidateSchemaAsync(ValidationContext context) + => ValidateAsync(context.Schema, null, context); /// /// Validate a node that is current within the context. /// - private bool Validate(IProvideMetadata obj, ASTNode? node, ValidationContext context) - => Validate(BuildValidationInfo(node, obj, context)); + private ValueTask ValidateAsync(IProvideMetadata obj, ASTNode? node, ValidationContext context) + => ValidateAsync(BuildValidationInfo(node, obj, context)); /// /// Initializes a new instance for the specified node. @@ -67,7 +67,7 @@ public readonly record struct ValidationInfo( /// as this is handled elsewhere. /// Returns a value indicating if validation was successful for this node. /// - protected virtual bool Validate(ValidationInfo info) + protected virtual async ValueTask ValidateAsync(ValidationInfo info) { bool requiresAuthorization = info.Obj.IsAuthorizationRequired(); if (!requiresAuthorization) @@ -84,7 +84,7 @@ protected virtual bool Validate(ValidationInfo info) _policyResults ??= new Dictionary(); foreach (var policy in policies) { if (!_policyResults.TryGetValue(policy, out var result)) { - result = Authorize(policy); + result = await AuthorizeAsync(policy); _policyResults.Add(policy, result); } if (!result.Succeeded) { @@ -120,7 +120,7 @@ protected virtual bool Validate(ValidationInfo info) protected abstract bool IsInRole(string role); /// - protected abstract AuthorizationResult Authorize(string policy); + protected abstract ValueTask AuthorizeAsync(string policy); /// /// Adds a error to the validation context indicating that the user is not authenticated diff --git a/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.cs b/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.cs index 106913b..63c61d8 100644 --- a/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.cs +++ b/src/GraphQL.AspNetCore3/AuthorizationVisitorBase.cs @@ -19,7 +19,7 @@ public AuthorizationVisitorBase(ValidationContext context) private List? _todos; /// - public virtual void Enter(ASTNode node, ValidationContext context) + public virtual async ValueTask EnterAsync(ASTNode node, ValidationContext context) { // if the node is the selected operation, or if it is a fragment referenced by the current operation, // then enable authorization checks on decendant nodes (_checkTree = true) @@ -58,7 +58,7 @@ public virtual void Enter(ASTNode node, ValidationContext context) // Fields, unlike types, are validated immediately. if (!fieldAnonymousAllowed) { - Validate(field, node, context); + await ValidateAsync(field, node, context); } } @@ -92,7 +92,7 @@ public virtual void Enter(ASTNode node, ValidationContext context) // verify field argument var arg = context.TypeInfo.GetArgument(); if (arg != null) { - Validate(arg, node, context); + await ValidateAsync(arg, node, context); } } } @@ -100,7 +100,7 @@ public virtual void Enter(ASTNode node, ValidationContext context) } /// - public virtual void Leave(ASTNode node, ValidationContext context) + public virtual async ValueTask LeaveAsync(ASTNode node, ValidationContext context) { if (!_checkTree) { // if we are within a field skipped by a directive, resume auth checks at the appropriate time @@ -114,30 +114,30 @@ public virtual void Leave(ASTNode node, ValidationContext context) } if (node == context.Operation) { _checkTree = false; - PopAndProcess(); + await PopAndProcessAsync(); } else if (node is GraphQLFragmentDefinition fragmentDefinition) { // once a fragment is done being processed, apply it to all types waiting on fragment checks, // and process checks for types that are not waiting on any fragments _checkTree = false; var fragmentName = fragmentDefinition.FragmentName.Name.StringValue; var ti = _onlyAnonymousSelected.Pop(); - RecursiveResolve(fragmentName, ti, context); + await RecursiveResolveAsync(fragmentName, ti, context); _fragments ??= new(); _fragments.TryAdd(fragmentName, ti); } else if (_checkTree && node is GraphQLField) { - PopAndProcess(); + await PopAndProcessAsync(); } // pop the current type info, and validate the type if it does not contain only fields marked // with AllowAnonymous (assuming it is not waiting on fragments) - void PopAndProcess() + async ValueTask PopAndProcessAsync() { var info = _onlyAnonymousSelected.Pop(); var type = context.TypeInfo.GetLastType()?.GetNamedType(); if (type == null) return; if (info.AnyAuthenticated || (!info.AnyAnonymous && (info.WaitingOnFragments?.Count ?? 0) == 0)) { - Validate(type, node, context); + await ValidateAsync(type, node, context); } else if (info.WaitingOnFragments?.Count > 0) { _todos ??= new(); _todos.Add(new(BuildValidationInfo(node, type, context), info)); @@ -205,7 +205,7 @@ static bool GetDirectiveValue(GraphQLDirective directive, ValidationContext cont /// Runs when a fragment is added or updated; the fragment might not be waiting on any /// other fragments, or it still might be. /// - private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContext context) + private async ValueTask RecursiveResolveAsync(string fragmentName, TypeInfo ti, ValidationContext context) { // first see if any other fragments are waiting on this fragment if (_fragments != null) { @@ -216,7 +216,7 @@ private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContex ti2.AnyAuthenticated |= ti.AnyAuthenticated; ti2.AnyAnonymous |= ti.AnyAnonymous; _fragments[fragment.Key] = ti2; - RecursiveResolve(fragment.Key, ti2, context); + await RecursiveResolveAsync(fragment.Key, ti2, context); goto Retry; // modifying a collection at runtime is not supported } } @@ -234,7 +234,7 @@ private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContex _todos.RemoveAt(i); count--; if (todo.AnyAuthenticated || !todo.AnyAnonymous) { - Validate(todo.ValidationInfo); + await ValidateAsync(todo.ValidationInfo); } } } diff --git a/src/GraphQL.AspNetCore3/GraphQL.AspNetCore3.csproj b/src/GraphQL.AspNetCore3/GraphQL.AspNetCore3.csproj index 0f5ffd8..00bc212 100644 --- a/src/GraphQL.AspNetCore3/GraphQL.AspNetCore3.csproj +++ b/src/GraphQL.AspNetCore3/GraphQL.AspNetCore3.csproj @@ -14,6 +14,7 @@ + diff --git a/src/GraphQL.AspNetCore3/GraphQLBuilderExtensions.cs b/src/GraphQL.AspNetCore3/GraphQLBuilderExtensions.cs index 6d7fa99..05c37e0 100644 --- a/src/GraphQL.AspNetCore3/GraphQLBuilderExtensions.cs +++ b/src/GraphQL.AspNetCore3/GraphQLBuilderExtensions.cs @@ -11,10 +11,17 @@ public static class GraphQLBuilderExtensions /// Registers with the dependency injection framework /// and configures it to be used when executing a request. /// + [Obsolete("Please use AddAuthorizationRule")] public static IGraphQLBuilder AddAuthorization(this IGraphQLBuilder builder) + => AddAuthorizationRule(builder); + + /// + /// Registers with the dependency injection framework + /// and configures it to be used when executing a request. + /// + public static IGraphQLBuilder AddAuthorizationRule(this IGraphQLBuilder builder) { builder.AddValidationRule(true); - builder.Services.TryRegister(ServiceLifetime.Singleton); return builder; } diff --git a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs index a0ad71b..136d190 100644 --- a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs +++ b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs @@ -354,6 +354,7 @@ protected virtual async Task ExecuteRequestAsync(HttpContext co OperationName = request?.OperationName, RequestServices = serviceProvider, UserContext = userContext, + User = context.User, }; if (!context.WebSockets.IsWebSocketRequest) { if (HttpMethods.IsGet(context.Request.Method)) { diff --git a/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs b/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs index 0e3dd4d..8e75172 100644 --- a/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs +++ b/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs @@ -196,6 +196,7 @@ protected override async Task ExecuteRequestAsync(OperationMess OperationName = request.OperationName, RequestServices = scope.ServiceProvider, CancellationToken = CancellationToken, + User = Connection.HttpContext.User, }; if (UserContext != null) options.UserContext = UserContext; diff --git a/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs b/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs index bbdd9ac..4d41a11 100644 --- a/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs +++ b/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs @@ -176,6 +176,7 @@ protected override async Task ExecuteRequestAsync(OperationMess OperationName = request.OperationName, RequestServices = scope.ServiceProvider, CancellationToken = CancellationToken, + User = Connection.HttpContext.User, }; if (UserContext != null) options.UserContext = UserContext; diff --git a/src/Samples/AuthorizationSample/AuthorizationSample.csproj b/src/Samples/AuthorizationSample/AuthorizationSample.csproj index d23c5c7..ce77539 100644 --- a/src/Samples/AuthorizationSample/AuthorizationSample.csproj +++ b/src/Samples/AuthorizationSample/AuthorizationSample.csproj @@ -18,8 +18,7 @@ - - + diff --git a/src/Samples/AuthorizationSample/Program.cs b/src/Samples/AuthorizationSample/Program.cs index e367c1d..e1c85e9 100644 --- a/src/Samples/AuthorizationSample/Program.cs +++ b/src/Samples/AuthorizationSample/Program.cs @@ -33,7 +33,7 @@ builder.Services.AddGraphQL(b => b .AddAutoSchema(s => s.WithMutation()) .AddSystemTextJson() - .AddAuthorization()); + .AddAuthorizationRule()); // ------------------------------------ var app = builder.Build(); @@ -61,11 +61,11 @@ app.UseGraphQL("/graphql"); // configure Playground at "/ui/graphql" app.UseGraphQLPlayground( + "/ui/graphql", new GraphQL.Server.Ui.Playground.PlaygroundOptions { GraphQLEndPoint = new PathString("/graphql"), SubscriptionsEndPoint = new PathString("/graphql"), - }, - "/ui/graphql"); + }); // ------------------------------------- app.MapRazorPages(); diff --git a/src/Samples/BasicSample/BasicSample.csproj b/src/Samples/BasicSample/BasicSample.csproj index 9630206..9eaa32f 100644 --- a/src/Samples/BasicSample/BasicSample.csproj +++ b/src/Samples/BasicSample/BasicSample.csproj @@ -10,8 +10,7 @@ - - + diff --git a/src/Samples/BasicSample/Program.cs b/src/Samples/BasicSample/Program.cs index 2ee1d64..e21b193 100644 --- a/src/Samples/BasicSample/Program.cs +++ b/src/Samples/BasicSample/Program.cs @@ -19,10 +19,10 @@ app.UseGraphQL("/graphql"); // configure Playground at "/" app.UseGraphQLPlayground( + "/", new GraphQL.Server.Ui.Playground.PlaygroundOptions { GraphQLEndPoint = new PathString("/graphql"), SubscriptionsEndPoint = new PathString("/graphql"), - }, - "/"); + }); await app.RunAsync(); diff --git a/src/Samples/ControllerSample/ControllerSample.csproj b/src/Samples/ControllerSample/ControllerSample.csproj index 5adeefc..e480696 100644 --- a/src/Samples/ControllerSample/ControllerSample.csproj +++ b/src/Samples/ControllerSample/ControllerSample.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Samples/ControllerSample/Controllers/HomeController.cs b/src/Samples/ControllerSample/Controllers/HomeController.cs index 522a3bf..4cd5ca8 100644 --- a/src/Samples/ControllerSample/Controllers/HomeController.cs +++ b/src/Samples/ControllerSample/Controllers/HomeController.cs @@ -67,6 +67,7 @@ private async Task ExecuteGraphQLRequestAsync(GraphQLRequest? req Extensions = request?.Extensions, CancellationToken = HttpContext.RequestAborted, RequestServices = HttpContext.RequestServices, + User = HttpContext.User, }; IValidationRule rule = HttpMethods.IsGet(HttpContext.Request.Method) ? new HttpGetValidationRule() : new HttpPostValidationRule(); opts.ValidationRules = DocumentValidator.CoreRules.Append(rule); diff --git a/src/Samples/CorsSample/CorsSample.csproj b/src/Samples/CorsSample/CorsSample.csproj index 9630206..9eaa32f 100644 --- a/src/Samples/CorsSample/CorsSample.csproj +++ b/src/Samples/CorsSample/CorsSample.csproj @@ -10,8 +10,7 @@ - - + diff --git a/src/Samples/EndpointRoutingSample/EndpointRoutingSample.csproj b/src/Samples/EndpointRoutingSample/EndpointRoutingSample.csproj index 9630206..9eaa32f 100644 --- a/src/Samples/EndpointRoutingSample/EndpointRoutingSample.csproj +++ b/src/Samples/EndpointRoutingSample/EndpointRoutingSample.csproj @@ -10,8 +10,7 @@ - - + diff --git a/src/Samples/MultipleSchema/MultipleSchema.csproj b/src/Samples/MultipleSchema/MultipleSchema.csproj index e70e67e..51b5341 100644 --- a/src/Samples/MultipleSchema/MultipleSchema.csproj +++ b/src/Samples/MultipleSchema/MultipleSchema.csproj @@ -9,8 +9,7 @@ - - + diff --git a/src/Samples/MultipleSchema/Program.cs b/src/Samples/MultipleSchema/Program.cs index 3b758a8..e4a5efe 100644 --- a/src/Samples/MultipleSchema/Program.cs +++ b/src/Samples/MultipleSchema/Program.cs @@ -22,17 +22,17 @@ app.UseGraphQL("/dogs/graphql"); // configure Playground at "/cats" app.UseGraphQLPlayground( + "/cats", new GraphQL.Server.Ui.Playground.PlaygroundOptions { GraphQLEndPoint = new PathString("/cats/graphql"), SubscriptionsEndPoint = new PathString("/cats/graphql"), - }, - "/cats"); + }); // configure Playground at "/dogs" app.UseGraphQLPlayground( + "/dogs", new GraphQL.Server.Ui.Playground.PlaygroundOptions { GraphQLEndPoint = new PathString("/dogs/graphql"), SubscriptionsEndPoint = new PathString("/dogs/graphql"), - }, - "/dogs"); + }); app.MapRazorPages(); await app.RunAsync(); diff --git a/src/Samples/Net48Sample/Net48Sample.csproj b/src/Samples/Net48Sample/Net48Sample.csproj index f788427..1fa74d8 100644 --- a/src/Samples/Net48Sample/Net48Sample.csproj +++ b/src/Samples/Net48Sample/Net48Sample.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Samples/PagesSample/PagesSample.csproj b/src/Samples/PagesSample/PagesSample.csproj index 5adeefc..e480696 100644 --- a/src/Samples/PagesSample/PagesSample.csproj +++ b/src/Samples/PagesSample/PagesSample.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt index d1b8e88..9fc1437 100644 --- a/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt +++ b/src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt @@ -17,7 +17,7 @@ namespace GraphQL.AspNetCore3 } public class AuthorizationValidationRule : GraphQL.Validation.IValidationRule { - public AuthorizationValidationRule(Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor) { } + public AuthorizationValidationRule() { } public virtual System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } } public class AuthorizationVisitor : GraphQL.AspNetCore3.AuthorizationVisitorBase @@ -26,25 +26,25 @@ namespace GraphQL.AspNetCore3 protected Microsoft.AspNetCore.Authorization.IAuthorizationService AuthorizationService { get; } protected System.Security.Claims.ClaimsPrincipal ClaimsPrincipal { get; } protected override bool IsAuthenticated { get; } - protected override Microsoft.AspNetCore.Authorization.AuthorizationResult Authorize(string policy) { } + protected override System.Threading.Tasks.ValueTask AuthorizeAsync(string policy) { } protected override bool IsInRole(string role) { } } public abstract class AuthorizationVisitorBase : GraphQL.Validation.INodeVisitor { public AuthorizationVisitorBase(GraphQL.Validation.ValidationContext context) { } protected abstract bool IsAuthenticated { get; } - protected abstract Microsoft.AspNetCore.Authorization.AuthorizationResult Authorize(string policy); - public virtual void Enter(GraphQLParser.AST.ASTNode node, GraphQL.Validation.ValidationContext context) { } + protected abstract System.Threading.Tasks.ValueTask AuthorizeAsync(string policy); + public virtual System.Threading.Tasks.ValueTask EnterAsync(GraphQLParser.AST.ASTNode node, GraphQL.Validation.ValidationContext context) { } protected virtual string GenerateResourceDescription(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info) { } protected System.Collections.Generic.List? GetRecursivelyReferencedFragments(GraphQL.Validation.ValidationContext validationContext) { } protected virtual void HandleNodeNotAuthorized(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info) { } protected virtual void HandleNodeNotInPolicy(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info, string policy, Microsoft.AspNetCore.Authorization.AuthorizationResult authorizationResult) { } protected virtual void HandleNodeNotInRoles(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info, System.Collections.Generic.List roles) { } protected abstract bool IsInRole(string role); - public virtual void Leave(GraphQLParser.AST.ASTNode node, GraphQL.Validation.ValidationContext context) { } + public virtual System.Threading.Tasks.ValueTask LeaveAsync(GraphQLParser.AST.ASTNode node, GraphQL.Validation.ValidationContext context) { } protected virtual bool SkipNode(GraphQLParser.AST.ASTNode node, GraphQL.Validation.ValidationContext context) { } - protected virtual bool Validate(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info) { } - public virtual bool ValidateSchema(GraphQL.Validation.ValidationContext context) { } + protected virtual System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo info) { } + public virtual System.Threading.Tasks.ValueTask ValidateSchemaAsync(GraphQL.Validation.ValidationContext context) { } public readonly struct ValidationInfo : System.IEquatable { public ValidationInfo(GraphQL.Types.IProvideMetadata Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { } @@ -62,7 +62,9 @@ namespace GraphQL.AspNetCore3 } public static class GraphQLBuilderExtensions { + [System.Obsolete("Please use AddAuthorizationRule")] public static GraphQL.DI.IGraphQLBuilder AddAuthorization(this GraphQL.DI.IGraphQLBuilder builder) { } + public static GraphQL.DI.IGraphQLBuilder AddAuthorizationRule(this GraphQL.DI.IGraphQLBuilder builder) { } public static GraphQL.DI.IGraphQLBuilder AddUserContextBuilder(this GraphQL.DI.IGraphQLBuilder builder, GraphQL.DI.ServiceLifetime serviceLifetime = 0) where TUserContextBuilder : class, GraphQL.AspNetCore3.IUserContextBuilder { } public static GraphQL.DI.IGraphQLBuilder AddUserContextBuilder(this GraphQL.DI.IGraphQLBuilder builder, System.Func> creator) diff --git a/src/Tests/AuthorizationTests.cs b/src/Tests/AuthorizationTests.cs index fcb59df..c6cad3f 100644 --- a/src/Tests/AuthorizationTests.cs +++ b/src/Tests/AuthorizationTests.cs @@ -46,10 +46,6 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true }); var mockServices = new Mock(MockBehavior.Strict); mockServices.Setup(x => x.GetService(typeof(IAuthorizationService))).Returns(mockAuthorizationService.Object); - var mockHttpContext = new Mock(MockBehavior.Strict); - mockHttpContext.Setup(x => x.User).Returns(_principal); - var mockContextAccessor = new Mock(MockBehavior.Strict); - mockContextAccessor.Setup(x => x.HttpContext).Returns(mockHttpContext.Object); var document = GraphQLParser.Parser.Parse(query); var inputs = new GraphQLSerializer().Deserialize(variables) ?? Inputs.Empty; @@ -63,6 +59,7 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true UserContext = new Dictionary(), Variables = inputs, RequestServices = mockServices.Object, + User = _principal, }).GetAwaiter().GetResult(); // there is no async code being tested coreRulesResult.IsValid.ShouldBe(shouldPassCoreRules); @@ -70,11 +67,12 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true Document = document, Extensions = Inputs.Empty, Operation = (GraphQLOperationDefinition)document.Definitions.First(x => x.Kind == ASTNodeKind.OperationDefinition), - Rules = new IValidationRule[] { new AuthorizationValidationRule(mockContextAccessor.Object) }, + Rules = new IValidationRule[] { new AuthorizationValidationRule() }, Schema = _schema, UserContext = new Dictionary(), Variables = inputs, RequestServices = mockServices.Object, + User = _principal, }).GetAwaiter().GetResult(); // there is no async code being tested return result; } @@ -533,18 +531,16 @@ private void Apply(IProvideMetadata obj, Mode mode) [Fact] public void Constructors() { - Should.Throw(() => new AuthorizationValidationRule(null!)); Should.Throw(() => new AuthorizationVisitor(null!, _principal, Mock.Of())); Should.Throw(() => new AuthorizationVisitor(new ValidationContext(), null!, Mock.Of())); Should.Throw(() => new AuthorizationVisitor(new ValidationContext(), _principal, null!)); } [Theory] - [InlineData(true, false, false, false)] - [InlineData(false, true, false, false)] - [InlineData(false, false, true, false)] - [InlineData(false, false, false, true)] - public void MiscErrors(bool noHttpContext, bool noClaimsPrincipal, bool noRequestServices, bool noAuthenticationService) + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + public void MiscErrors(bool noClaimsPrincipal, bool noRequestServices, bool noAuthenticationService) { var mockAuthorizationService = new Mock(MockBehavior.Strict); mockAuthorizationService.Setup(x => x.AuthorizeAsync(_principal, null, It.IsAny())).Returns((_, _, policy) => { @@ -554,10 +550,6 @@ public void MiscErrors(bool noHttpContext, bool noClaimsPrincipal, bool noReques }); var mockServices = new Mock(MockBehavior.Strict); mockServices.Setup(x => x.GetService(typeof(IAuthorizationService))).Returns(noAuthenticationService ? null! : mockAuthorizationService.Object); - var mockHttpContext = new Mock(MockBehavior.Strict); - mockHttpContext.Setup(x => x.User).Returns(noClaimsPrincipal ? null! : _principal); - var mockContextAccessor = new Mock(MockBehavior.Strict); - mockContextAccessor.Setup(x => x.HttpContext).Returns(noHttpContext ? null! : mockHttpContext.Object); var document = GraphQLParser.Parser.Parse("{ __typename }"); var validator = new DocumentValidator(); @@ -565,18 +557,16 @@ public void MiscErrors(bool noHttpContext, bool noClaimsPrincipal, bool noReques Document = document, Extensions = Inputs.Empty, Operation = (GraphQLOperationDefinition)document.Definitions.Single(x => x.Kind == ASTNodeKind.OperationDefinition), - Rules = new IValidationRule[] { new AuthorizationValidationRule(mockContextAccessor.Object) }, + Rules = new IValidationRule[] { new AuthorizationValidationRule() }, Schema = _schema, UserContext = new Dictionary(), Variables = Inputs.Empty, RequestServices = noRequestServices ? null : mockServices.Object, + User = noClaimsPrincipal ? null : _principal, }).GetAwaiter().GetResult()); // there is no async code being tested - if (noHttpContext) - err.ShouldBeOfType().Message.ShouldBe("HttpContext could not be retrieved from IHttpContextAccessor."); - if (noClaimsPrincipal) - err.ShouldBeOfType().Message.ShouldBe("ClaimsPrincipal could not be retrieved from HttpContext."); + err.ShouldBeOfType().Message.ShouldBe("User could not be retrieved from ValidationContext. Please be sure it is set in ExecutionOptions.User."); if (noRequestServices) err.ShouldBeOfType(); @@ -591,10 +581,6 @@ public void NullIdentity() var mockAuthorizationService = new Mock(MockBehavior.Strict); var mockServices = new Mock(MockBehavior.Strict); mockServices.Setup(x => x.GetService(typeof(IAuthorizationService))).Returns(mockAuthorizationService.Object); - var mockHttpContext = new Mock(MockBehavior.Strict); - mockHttpContext.Setup(x => x.User).Returns(new ClaimsPrincipal()); - var mockContextAccessor = new Mock(MockBehavior.Strict); - mockContextAccessor.Setup(x => x.HttpContext).Returns(mockHttpContext.Object); var document = GraphQLParser.Parser.Parse("{ __typename }"); var validator = new DocumentValidator(); _schema.Authorize(); @@ -603,11 +589,12 @@ public void NullIdentity() Document = document, Extensions = Inputs.Empty, Operation = (GraphQLOperationDefinition)document.Definitions.Single(x => x.Kind == ASTNodeKind.OperationDefinition), - Rules = new IValidationRule[] { new AuthorizationValidationRule(mockContextAccessor.Object) }, + Rules = new IValidationRule[] { new AuthorizationValidationRule() }, Schema = _schema, UserContext = new Dictionary(), Variables = Inputs.Empty, RequestServices = mockServices.Object, + User = new ClaimsPrincipal(), }).GetAwaiter().GetResult(); // there is no async code being tested result.Errors.ShouldHaveSingleItem().ShouldBeOfType().Message.ShouldBe("Access denied for schema."); @@ -700,24 +687,17 @@ public async Task TestPipeline(bool authenticated) services.AddGraphQL(b => b .AddSchema(_schema) .AddSystemTextJson() - .AddAuthorization()); + .AddAuthorizationRule()); services.AddSingleton(Mock.Of(MockBehavior.Strict)); - var mockContext = new Mock(MockBehavior.Strict); - mockContext.Setup(x => x.User).Returns(_principal); - - var mockContextAccessor = new Mock(MockBehavior.Strict); - mockContextAccessor.Setup(x => x.HttpContext).Returns(mockContext.Object); - - services.AddSingleton(mockContextAccessor.Object); - using var provider = services.BuildServiceProvider(); var executer = provider.GetRequiredService>(); var ret = await executer.ExecuteAsync(new ExecutionOptions { Query = @"{ parent { child } }", RequestServices = provider, + User = _principal, }); var serializer = provider.GetRequiredService(); @@ -729,6 +709,51 @@ public async Task TestPipeline(bool authenticated) actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for field \u0027parent\u0027 on type \u0027QueryType\u0027."",""locations"":[{""line"":1,""column"":3}],""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task EndToEnd(bool authenticated) + { + _field.Authorize(); + + if (authenticated) + SetAuthorized(); + + var hostBuilder = new WebHostBuilder(); + hostBuilder.ConfigureServices(services => { + services.AddSingleton(); + services.AddGraphQL(b => b + .AddSchema(_schema) + .AddSystemTextJson() + .AddAuthorizationRule()); + services.AddAuthentication(); + services.AddAuthorization(); +#if NETCOREAPP2_1 || NET48 + services.AddHostApplicationLifetime(); +#endif + }); + hostBuilder.Configure(app => { + app.UseWebSockets(); + // simulate app.UseAuthentication() + app.Use(next => context => { + context.User = _principal; + return next(context); + }); + app.UseGraphQL(); + }); + using var server = new TestServer(hostBuilder); + + using var client = server.CreateClient(); + using var response = await client.GetAsync("/graphql?query={ parent { child } }"); + response.StatusCode.ShouldBe(authenticated ? System.Net.HttpStatusCode.OK : System.Net.HttpStatusCode.BadRequest); + var actual = await response.Content.ReadAsStringAsync(); + + if (authenticated) + actual.ShouldBe(@"{""data"":{""parent"":null}}"); + else + actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for field \u0027parent\u0027 on type \u0027QueryType\u0027."",""locations"":[{""line"":1,""column"":3}],""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}"); + } + public enum Mode { None, diff --git a/src/Tests/Middleware/FileUploadTests.cs b/src/Tests/Middleware/FileUploadTests.cs index 4b9e349..ebafdda 100644 --- a/src/Tests/Middleware/FileUploadTests.cs +++ b/src/Tests/Middleware/FileUploadTests.cs @@ -108,13 +108,10 @@ public MySchema() var query = new ObjectGraphType { Name = "Query", }; - query.Field( - "ConvertToBase64", - arguments: new QueryArguments( - new QueryArgument(typeof(StringGraphType)) { Name = "prefix" }, - new QueryArgument(typeof(NonNullGraphType)) { Name = "file" } - ), - resolve: context => { + query.Field("ConvertToBase64") + .Argument("prefix") + .Argument>("file") + .Resolve(context => { var prefix = context.GetArgument("prefix"); var file = context.GetArgument("file"); var memStream = new MemoryStream(); diff --git a/src/Tests/WebSockets/NewSubscriptionServerTests.cs b/src/Tests/WebSockets/NewSubscriptionServerTests.cs index dec5987..6ec1c3f 100644 --- a/src/Tests/WebSockets/NewSubscriptionServerTests.cs +++ b/src/Tests/WebSockets/NewSubscriptionServerTests.cs @@ -1,3 +1,5 @@ +using System.Security.Claims; + namespace Tests.WebSockets; public class NewSubscriptionServerTests : IDisposable @@ -350,6 +352,10 @@ public async Task ExecuteRequestAsync() .Verifiable(); mockScope.Setup(x => x.Dispose()).Verifiable(); var result = Mock.Of(MockBehavior.Strict); + var principal = new ClaimsPrincipal(); + var mockContext = new Mock(MockBehavior.Strict); + mockContext.Setup(x => x.User).Returns(principal).Verifiable(); + _mockStream.Setup(x => x.HttpContext).Returns(mockContext.Object).Verifiable(); var mockUserContext = new Mock>(MockBehavior.Strict); _server.Set_UserContext(mockUserContext.Object); _mockDocumentExecuter.Setup(x => x.ExecuteAsync(It.IsAny())) @@ -361,11 +367,13 @@ public async Task ExecuteRequestAsync() options.OperationName.ShouldBe(request.OperationName); options.UserContext.ShouldBe(mockUserContext.Object); options.RequestServices.ShouldBe(mockServiceProvider.Object); + options.User.ShouldBe(principal); return Task.FromResult(result); }) .Verifiable(); var actual = await _server.Do_ExecuteRequestAsync(message); actual.ShouldBe(result); + mockContext.Verify(); _mockDocumentExecuter.Verify(); _mockSerializer.Verify(); _mockServiceScopeFactory.Verify(); diff --git a/src/Tests/WebSockets/OldSubscriptionServerTests.cs b/src/Tests/WebSockets/OldSubscriptionServerTests.cs index 42c02ff..9b35894 100644 --- a/src/Tests/WebSockets/OldSubscriptionServerTests.cs +++ b/src/Tests/WebSockets/OldSubscriptionServerTests.cs @@ -1,3 +1,5 @@ +using System.Security.Claims; + namespace Tests.WebSockets; public class OldSubscriptionServerTests : IDisposable @@ -305,6 +307,10 @@ public async Task ExecuteRequestAsync() .Verifiable(); mockScope.Setup(x => x.Dispose()).Verifiable(); var result = Mock.Of(MockBehavior.Strict); + var principal = new ClaimsPrincipal(); + var mockContext = new Mock(MockBehavior.Strict); + mockContext.Setup(x => x.User).Returns(principal).Verifiable(); + _mockStream.Setup(x => x.HttpContext).Returns(mockContext.Object).Verifiable(); var mockUserContext = new Mock>(MockBehavior.Strict); _server.Set_UserContext(mockUserContext.Object); _mockDocumentExecuter.Setup(x => x.ExecuteAsync(It.IsAny())) @@ -316,11 +322,13 @@ public async Task ExecuteRequestAsync() options.OperationName.ShouldBe(request.OperationName); options.UserContext.ShouldBe(mockUserContext.Object); options.RequestServices.ShouldBe(mockServiceProvider.Object); + options.User.ShouldBe(principal); return Task.FromResult(result); }) .Verifiable(); var actual = await _server.Do_ExecuteRequestAsync(message); actual.ShouldBe(result); + mockContext.Verify(); _mockDocumentExecuter.Verify(); _mockSerializer.Verify(); _mockServiceScopeFactory.Verify();