diff --git a/Directory.Build.props b/Directory.Build.props index 05577647..0f134a74 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@ - 7.8.0-preview - 8.0.0 + 8.0.0-preview + 9.0.0 latest MIT logo.64x64.png @@ -32,7 +32,8 @@ GraphQL.Server.$(MSBuildProjectName) GraphQL.Server.$(MSBuildProjectName) - [7.6.0,8.0.0) + + 8.0.0-preview-1053 true <_FriendAssembliesPublicKey>PublicKey=0024000004800000940000000602000000240000525341310004000001000100352162dbf27be78fc45136884b8f324aa9f1dfc928c96c24704bf1df1a8779b2f26c760ed8321eca5b95ea6bd9bb60cd025b300f73bd1f4ae1ee6e281f85c527fa013ab5cb2c3fc7a1cbef7f9bf0c9014152e6a21f6e0ac6a371f8b45c6d7139c9119df9eeecf1cf59063545bb7c07437b1bc12be2c57d108d72d6c27176fbb8 diff --git a/GraphQL.Server.sln b/GraphQL.Server.sln index 02e5081d..e876bbd5 100644 --- a/GraphQL.Server.sln +++ b/GraphQL.Server.sln @@ -96,15 +96,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Net48.Tests", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Pages.Tests", "tests\Samples.Pages.Tests\Samples.Pages.Tests.csproj", "{BF4ED814-A21D-45F5-8F71-54148751AE81}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authorization.AspNetCore", "src\Authorization.AspNetCore\Authorization.AspNetCore.csproj", "{A4875E2D-62AF-4D86-9D8A-19A310365FE8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authorization.AspNetCore.Tests", "tests\Authorization.AspNetCore.Tests\Authorization.AspNetCore.Tests.csproj", "{9AEB9941-618B-430A-A14C-37F39AFFC0C4}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{AB155A1E-CB5E-465E-BC29-A1A4A5D9D2B0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migration", "Migration", "{FF0810A0-3372-40ED-B3E3-D703BEF0F118}" ProjectSection(SolutionItems) = preProject docs\migration\migration7.md = docs\migration\migration7.md + docs\migration\migration8.md = docs\migration\migration8.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Jwt", "samples\Samples.Jwt\Samples.Jwt.csproj", "{5A16B117-0FAD-4F91-A97C-E72B733B4E57}" @@ -120,7 +117,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.AzureFunctions", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.AzureFunctions.Tests", "tests\Samples.AzureFunctions.Tests\Samples.AzureFunctions.Tests.csproj", "{A204E359-05E8-4CEE-891C-4CCA6570FA52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.Upload", "samples\Samples.Upload\Samples.Upload.csproj", "{33E2CDF5-F854-4F1A-80D5-DBF0BDF8EEA8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Upload", "samples\Samples.Upload\Samples.Upload.csproj", "{33E2CDF5-F854-4F1A-80D5-DBF0BDF8EEA8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Upload.Tests", "tests\Samples.Upload.Tests\Samples.Upload.Tests.csproj", "{DE3059F4-B548-4091-BFC0-5879246A2DF9}" EndProject @@ -242,14 +239,6 @@ Global {BF4ED814-A21D-45F5-8F71-54148751AE81}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF4ED814-A21D-45F5-8F71-54148751AE81}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF4ED814-A21D-45F5-8F71-54148751AE81}.Release|Any CPU.Build.0 = Release|Any CPU - {A4875E2D-62AF-4D86-9D8A-19A310365FE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4875E2D-62AF-4D86-9D8A-19A310365FE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4875E2D-62AF-4D86-9D8A-19A310365FE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4875E2D-62AF-4D86-9D8A-19A310365FE8}.Release|Any CPU.Build.0 = Release|Any CPU - {9AEB9941-618B-430A-A14C-37F39AFFC0C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AEB9941-618B-430A-A14C-37F39AFFC0C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AEB9941-618B-430A-A14C-37F39AFFC0C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AEB9941-618B-430A-A14C-37F39AFFC0C4}.Release|Any CPU.Build.0 = Release|Any CPU {5A16B117-0FAD-4F91-A97C-E72B733B4E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A16B117-0FAD-4F91-A97C-E72B733B4E57}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A16B117-0FAD-4F91-A97C-E72B733B4E57}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -306,7 +295,6 @@ Global {15043767-3620-4505-9DA9-78E3E5C05E18} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43} {08E0FF4C-9131-4523-A6EF-4C1FCE7EFA05} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43} {BF4ED814-A21D-45F5-8F71-54148751AE81} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43} - {9AEB9941-618B-430A-A14C-37F39AFFC0C4} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43} {FF0810A0-3372-40ED-B3E3-D703BEF0F118} = {AB155A1E-CB5E-465E-BC29-A1A4A5D9D2B0} {5A16B117-0FAD-4F91-A97C-E72B733B4E57} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B} {2B5A39E8-098F-458E-981C-BC9470CB94B0} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43} diff --git a/README.md b/README.md index 96d277e6..98e76aab 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ Provides the following packages: | Package | Downloads | Version | Description | |------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| -| GraphQL.Server.All | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.All)](https://www.nuget.org/packages/GraphQL.Server.All) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.All)](https://www.nuget.org/packages/GraphQL.Server.All) | Includes all the packages below, excluding the legacy authorization package, plus the `GraphQL.DataLoader` and `GraphQL.MemoryCache` packages | -| GraphQL.Server.Authorization.AspNetCore (deprecated) | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.Authorization.AspNetCore)](https://www.nuget.org/packages/GraphQL.Server.Authorization.AspNetCore) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.Authorization.AspNetCore)](https://www.nuget.org/packages/GraphQL.Server.Authorization.AspNetCore) | Provides legacy authorization rule support (deprecated; please use GraphQL.Server.Transports.AspNetCore) | +| GraphQL.Server.All | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.All)](https://www.nuget.org/packages/GraphQL.Server.All) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.All)](https://www.nuget.org/packages/GraphQL.Server.All) | Includes all the packages below, plus the `GraphQL.DataLoader` and `GraphQL.MemoryCache` packages | | GraphQL.Server.Transports.AspNetCore | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.Transports.AspNetCore)](https://www.nuget.org/packages/GraphQL.Server.Transports.AspNetCore) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.Transports.AspNetCore)](https://www.nuget.org/packages/GraphQL.Server.Transports.AspNetCore) | Provides GraphQL over HTTP/WebSocket server support on top of ASP.NET Core, plus authorization rule support | | GraphQL.Server.Ui.Altair | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.Ui.Altair)](https://www.nuget.org/packages/GraphQL.Server.Ui.Altair) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.Ui.Altair)](https://www.nuget.org/packages/GraphQL.Server.Ui.Altair) | Provides Altair UI middleware | | GraphQL.Server.Ui.Playground | [![Nuget](https://img.shields.io/nuget/dt/GraphQL.Server.Ui.Playground)](https://www.nuget.org/packages/GraphQL.Server.Ui.Playground) | [![Nuget](https://img.shields.io/nuget/v/GraphQL.Server.Ui.Playground)](https://www.nuget.org/packages/GraphQL.Server.Ui.Playground) | Provides Playground UI middleware | @@ -33,6 +32,7 @@ Note that GitHub requires authentication to consume the feed. See more informati | :warning: When upgrading from prior versions, please remove references to these old packages :warning: | |-| | GraphQL.Server.Core | +| GraphQL.Server.Authentication.AspNetCore | | GraphQL.Server.Transports.AspNetCore.NewtonsoftJson | | GraphQL.Server.Transports.AspNetCore.SystemTextJson | | GraphQL.Server.Transports.Subscriptions.Abstractions | @@ -64,7 +64,10 @@ any policies or roles specified for input graph types, fields of input graph typ directives. It skips validations for fields or fragments that are marked with the `@skip` or `@include` directives. -See [migration notes](docs/migration/migration7.md) for changes from version 6.x. +### Migration from older version + +- [v7 to v8 migration notes](docs/migration/migration8.md) +- [v6 to v7 migration notes](docs/migration/migration7.md) ## Configuration diff --git a/docs/migration/migration8.md b/docs/migration/migration8.md new file mode 100644 index 00000000..7bb64bff --- /dev/null +++ b/docs/migration/migration8.md @@ -0,0 +1,12 @@ +# Migrating from v7 to v8 + +## Major changes and new features + +None + +## Breaking changes + +- The validation rules' signatures have changed slightly due to the underlying changes to the + GraphQL.NET library. Please see the GraphQL.NET v8 migration document for more information. +- The obsolete (v6 and prior) authorization validation rule has been removed. See the v7 migration + document for more information on how to migrate to the v7/v8 authorization validation rule. diff --git a/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj b/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj deleted file mode 100644 index 7a1440c8..00000000 --- a/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net8.0 - Integration of GraphQL.NET validation subsystem into ASP.NET Core - GraphQL;authentication;authorization;validation - true - - - - - - - diff --git a/src/Authorization.AspNetCore/AuthorizationError.cs b/src/Authorization.AspNetCore/AuthorizationError.cs deleted file mode 100644 index 9fa2e0ac..00000000 --- a/src/Authorization.AspNetCore/AuthorizationError.cs +++ /dev/null @@ -1,33 +0,0 @@ -using GraphQL.Validation; -using GraphQLParser.AST; -using Microsoft.AspNetCore.Authorization; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// An error that represents an authorization failure while parsing the document. -/// -[Obsolete("This class has been replaced by GraphQL.Server.Transports.AspNetCore.Errors.AccessDeniedError and will be removed in v8.")] -public class AuthorizationError : ValidationError -{ - /// - /// Initializes a new instance of the class for a specified authorization result with a specific error message. - /// - public AuthorizationError(ASTNode? node, ValidationContext context, string message, AuthorizationResult result, OperationType? operationType = null) - : base(context.Document.Source, "6.1.1", message, node == null ? Array.Empty() : new ASTNode[] { node }) - { - Code = "authorization"; - AuthorizationResult = result; - OperationType = operationType; - } - - /// - /// Returns the result of the ASP.NET Core authorization request. - /// - public virtual AuthorizationResult AuthorizationResult { get; } - - /// - /// The GraphQL operation type. - /// - public OperationType? OperationType { get; } -} diff --git a/src/Authorization.AspNetCore/AuthorizationValidationRule.cs b/src/Authorization.AspNetCore/AuthorizationValidationRule.cs deleted file mode 100644 index 21911b2b..00000000 --- a/src/Authorization.AspNetCore/AuthorizationValidationRule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Security.Claims; -using GraphQL.Validation; -using Microsoft.AspNetCore.Authorization; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// GraphQL authorization validation rule which integrates to ASP.NET Core authorization mechanism. -/// For more information see https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction. -/// -[Obsolete("This class has been replaced by GraphQL.Server.Transports.AspNetCore.AuthorizationValidationRule and will be removed in v8.")] -public partial class AuthorizationValidationRule : IValidationRule -{ - private readonly IAuthorizationService _authorizationService; - private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor; - private readonly IAuthorizationErrorMessageBuilder _messageBuilder; - - /// - /// Creates an instance of . - /// - /// ASP.NET Core to authorize against. - /// The which provides the for authorization. - /// The which is used to generate the message for an . - public AuthorizationValidationRule( - IAuthorizationService authorizationService, - IClaimsPrincipalAccessor claimsPrincipalAccessor, - IAuthorizationErrorMessageBuilder messageBuilder) - { - _authorizationService = authorizationService; - _claimsPrincipalAccessor = claimsPrincipalAccessor; - _messageBuilder = messageBuilder; - } - - /// - public async ValueTask ValidateAsync(ValidationContext context) - { - var visitor = new AuthorizationVisitor(context, _claimsPrincipalAccessor.GetClaimsPrincipal(context), _authorizationService, _messageBuilder, this); - - // if the schema fails authentication, report the error and do not perform any additional authorization checks. - return await visitor.ValidateSchemaAsync(context) ? visitor : null; - } - - /// - /// Adds an authorization failure error to the document response - /// - protected virtual void AddValidationError(GraphQLParser.AST.ASTNode? node, ValidationContext context, GraphQLParser.AST.OperationType? operationType, AuthorizationResult result) - { - string message = _messageBuilder.GenerateMessage(operationType, result); - context.ReportError(new AuthorizationError(node, context, message, result, operationType)); - } -} diff --git a/src/Authorization.AspNetCore/AuthorizationVisitor.cs b/src/Authorization.AspNetCore/AuthorizationVisitor.cs deleted file mode 100644 index 5754e4d9..00000000 --- a/src/Authorization.AspNetCore/AuthorizationVisitor.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Security.Claims; -using GraphQL.Validation; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Infrastructure; - -namespace GraphQL.Server.Authorization.AspNetCore; - -public partial class AuthorizationValidationRule -{ - /// - [Obsolete("This class has been replaced by GraphQL.Server.Transports.AspNetCore.AuthorizationVisitor and will be removed in v8.")] - public class AuthorizationVisitor : Transports.AspNetCore.AuthorizationVisitor - { - private readonly IAuthorizationErrorMessageBuilder _messageBuilder; - private readonly AuthorizationValidationRule _authorizationValidationRule; - - /// - public AuthorizationVisitor(ValidationContext context, ClaimsPrincipal claimsPrincipal, IAuthorizationService authorizationService, IAuthorizationErrorMessageBuilder authorizationErrorMessageBuilder, AuthorizationValidationRule authorizationValidationRule) - : base(context, claimsPrincipal, authorizationService) - { - _messageBuilder = authorizationErrorMessageBuilder; - _authorizationValidationRule = authorizationValidationRule; - } - - /// - protected override void HandleNodeNotAuthorized(ValidationInfo info) - => ReportError(info, new DenyAnonymousAuthorizationRequirement()); - - /// - protected override void HandleNodeNotInRoles(ValidationInfo info, List roles) - => ReportError(info, new RolesAuthorizationRequirement(roles)); - - /// - protected override void HandleNodeNotInPolicy(ValidationInfo info, string policy, AuthorizationResult authorizationResult) - => ReportError(info, authorizationResult); - - private void ReportError(ValidationInfo info, IAuthorizationRequirement authorizationRequirement) - => ReportError(info, AuthorizationResult.Failed(AuthorizationFailure.Failed(new[] { authorizationRequirement }))); - - private void ReportError(ValidationInfo info, AuthorizationResult authorizationResult) - { - _authorizationValidationRule.AddValidationError(info.Node, info.Context, info.Context.Operation.Operation, authorizationResult); - } - } -} diff --git a/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs b/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs deleted file mode 100644 index caaf04de..00000000 --- a/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Text; -using GraphQLParser.AST; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Infrastructure; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// Default error message builder implementation. -/// -[Obsolete("This class will be removed in v8 as revealing authorization requirements may be a security risk; please use ErrorInfoProvider if you require detailed access-denied error messages.")] -public class DefaultAuthorizationErrorMessageBuilder : IAuthorizationErrorMessageBuilder -{ - /// - public virtual string GenerateMessage(OperationType? operationType, AuthorizationResult result) - { - if (result.Succeeded) - return "Success!"; - - var error = new StringBuilder(); - AppendFailureHeader(error, operationType); - - if (result.Failure != null) - { - foreach (var requirement in result.Failure.FailedRequirements) - { - AppendFailureLine(error, requirement); - } - } - - return error.ToString(); - } - - private string GetOperationType(OperationType? operationType) - { - return operationType switch - { - OperationType.Query => "query", - OperationType.Mutation => "mutation", - OperationType.Subscription => "subscription", - _ => "operation", - }; - } - - /// - public virtual void AppendFailureHeader(StringBuilder errorBuilder, OperationType? operationType) - { - errorBuilder - .Append("You are not authorized to run this ") - .Append(GetOperationType(operationType)) - .Append('.'); - } - - /// - public virtual void AppendFailureLine(StringBuilder errorBuilder, IAuthorizationRequirement authorizationRequirement) - { - errorBuilder.AppendLine(); - - switch (authorizationRequirement) - { - case ClaimsAuthorizationRequirement claimsAuthorizationRequirement: - errorBuilder.Append("Required claim '"); - errorBuilder.Append(claimsAuthorizationRequirement.ClaimType); - if (claimsAuthorizationRequirement.AllowedValues == null || !claimsAuthorizationRequirement.AllowedValues.Any()) - { - errorBuilder.Append("' is not present."); - } - else - { - errorBuilder.Append("' with any value of '"); - errorBuilder.Append(string.Join(", ", claimsAuthorizationRequirement.AllowedValues)); - errorBuilder.Append("' is not present."); - } - break; - - case DenyAnonymousAuthorizationRequirement _: - errorBuilder.Append("The current user must be authenticated."); - break; - - case NameAuthorizationRequirement nameAuthorizationRequirement: - errorBuilder.Append("The current user name must match the name '"); - errorBuilder.Append(nameAuthorizationRequirement.RequiredName); - errorBuilder.Append("'."); - break; - - case OperationAuthorizationRequirement operationAuthorizationRequirement: - errorBuilder.Append("Required operation '"); - errorBuilder.Append(operationAuthorizationRequirement.Name); - errorBuilder.Append("' was not present."); - break; - - case RolesAuthorizationRequirement rolesAuthorizationRequirement: - if (rolesAuthorizationRequirement.AllowedRoles == null || !rolesAuthorizationRequirement.AllowedRoles.Any()) - { - // This should never happen. - errorBuilder.Append("Required roles are not present."); - } - else - { - errorBuilder.Append("Required roles '"); - errorBuilder.Append(string.Join(", ", rolesAuthorizationRequirement.AllowedRoles)); - errorBuilder.Append("' are not present."); - } - break; - - case AssertionRequirement _: - default: - errorBuilder.Append("Requirement '"); - errorBuilder.Append(authorizationRequirement.GetType().Name); - errorBuilder.Append("' was not satisfied."); - break; - } - } -} diff --git a/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs b/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs deleted file mode 100644 index edcef9b4..00000000 --- a/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Security.Claims; -using GraphQL.Validation; -using Microsoft.AspNetCore.Http; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// The default claims principal accessor. -/// -[Obsolete("This class will be removed in v8; please override GraphQLHttpMiddleware.HandleAuthorizeAsync and set HttpContext.User if needed.")] -public class DefaultClaimsPrincipalAccessor : IClaimsPrincipalAccessor -{ - private readonly IHttpContextAccessor _contextAccessor; - - /// - /// Creates an instance of . - /// - /// ASP.NET Core to take claims principal () from. - public DefaultClaimsPrincipalAccessor(IHttpContextAccessor contextAccessor) - { - _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); - } - - /// - /// Returns the . - /// - /// - /// - public ClaimsPrincipal GetClaimsPrincipal(ValidationContext context) - { - return _contextAccessor.HttpContext?.User!; - } -} diff --git a/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs b/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs deleted file mode 100644 index 7fb9393c..00000000 --- a/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using GraphQL.DI; -using GraphQL.Server.Authorization.AspNetCore; -#if NETCOREAPP3_1_OR_GREATER -using Microsoft.AspNetCore.Authorization; -#endif -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace GraphQL.Server; - -/// -/// Extension methods for interface. -/// -public static class GraphQLBuilderAuthorizationExtensions -{ - /// - /// Adds the GraphQL authorization. - /// - /// The GraphQL builder. - /// Reference to the passed . - [Obsolete("This extension method has been replaced with AddAuthorization and will be removed in v8.")] - public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder) -#if NETCOREAPP3_1_OR_GREATER - => builder.AddGraphQLAuthorization(_ => { }); - - /// - /// Adds the GraphQL authorization. - /// - /// The GraphQL builder. - /// An action delegate to configure the provided . - /// Reference to the passed . - [Obsolete("This extension method has been replaced with AddAuthorization and will be removed in v8.")] - public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder, Action? configure) -#endif - { - if (builder.Services is not IServiceCollection services) - throw new NotSupportedException("This method only supports the MicrosoftDI implementation of IGraphQLBuilder."); - - services.TryAddTransient(); - services.TryAddTransient(); - services.AddHttpContextAccessor(); - -#if NETCOREAPP3_1_OR_GREATER - if (configure != null) - services.AddAuthorizationCore(configure); - else - services.AddAuthorizationCore(); -#endif - - builder.AddValidationRule(true); - - return builder; - } -} diff --git a/src/Authorization.AspNetCore/IAuthorizationErrorMessageBuilder.cs b/src/Authorization.AspNetCore/IAuthorizationErrorMessageBuilder.cs deleted file mode 100644 index 0b43f1c1..00000000 --- a/src/Authorization.AspNetCore/IAuthorizationErrorMessageBuilder.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text; -using GraphQLParser.AST; -using Microsoft.AspNetCore.Authorization; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// Error message builder for authorization errors. -/// -[Obsolete("This class will be removed in v8 as revealing authorization requirements may be a security risk; please use ErrorInfoProvider if you require detailed access-denied error messages.")] -public interface IAuthorizationErrorMessageBuilder -{ - /// - /// Generates an authorization error message based on the provided - /// - /// The GraphQL operation type. - /// The which is used to generate the message. - /// The generated error message. - string GenerateMessage(OperationType? operationType, AuthorizationResult result); - - /// - /// Appends the error message header to the provided . - /// - /// The error message . - /// The GraphQL operation type. - void AppendFailureHeader(StringBuilder errorBuilder, OperationType? operationType); - - /// - /// Appends a description of the failed to the supplied . - /// - /// The which is used to compose the error message. - /// The failed . - void AppendFailureLine(StringBuilder errorBuilder, IAuthorizationRequirement authorizationRequirement); -} diff --git a/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs b/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs deleted file mode 100644 index 525b381f..00000000 --- a/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Security.Claims; -using GraphQL.Validation; - -namespace GraphQL.Server.Authorization.AspNetCore; - -/// -/// Provides access to the used for GraphQL operation authorization. -/// -[Obsolete("This class will be removed in v8; please override GraphQLHttpMiddleware.HandleAuthorizeAsync and set HttpContext.User if needed.")] -public interface IClaimsPrincipalAccessor -{ - /// - /// Provides the for the current - /// - /// The of the current operation - /// - ClaimsPrincipal GetClaimsPrincipal(ValidationContext context); -} diff --git a/src/Transports.AspNetCore/AuthorizationValidationRule.cs b/src/Transports.AspNetCore/AuthorizationValidationRule.cs index 59a33d38..96f7b619 100644 --- a/src/Transports.AspNetCore/AuthorizationValidationRule.cs +++ b/src/Transports.AspNetCore/AuthorizationValidationRule.cs @@ -3,10 +3,10 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// Validates a document against the configured set of policy and role requirements. /// -public class AuthorizationValidationRule : IValidationRule +public class AuthorizationValidationRule : ValidationRuleBase { /// - public virtual async ValueTask ValidateAsync(ValidationContext context) + public override async ValueTask GetPreNodeVisitorAsync(ValidationContext context) { var user = context.User ?? throw new InvalidOperationException("User could not be retrieved from ValidationContext. Please be sure it is set in ExecutionOptions.User."); diff --git a/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs b/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs index cef523c5..43a834a2 100644 --- a/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs +++ b/src/Transports.AspNetCore/AuthorizationVisitorBase.Validation.cs @@ -12,7 +12,7 @@ public virtual ValueTask ValidateSchemaAsync(ValidationContext context) /// /// Validate a node that is current within the context. /// - private ValueTask ValidateAsync(IProvideMetadata obj, ASTNode? node, ValidationContext context) + private ValueTask ValidateAsync(IMetadataReader obj, ASTNode? node, ValidationContext context) => ValidateAsync(BuildValidationInfo(node, obj, context)); /// @@ -21,7 +21,7 @@ private ValueTask ValidateAsync(IProvideMetadata obj, ASTNode? node, Valid /// The specified . /// The , or which has been matched to the node specified in . /// The validation context. - private static ValidationInfo BuildValidationInfo(ASTNode? node, IProvideMetadata obj, ValidationContext context) + private static ValidationInfo BuildValidationInfo(ASTNode? node, IMetadataReader obj, ValidationContext context) { IFieldType? parentFieldType = null; IGraphType? parentGraphType = null; @@ -52,7 +52,7 @@ private static ValidationInfo BuildValidationInfo(ASTNode? node, IProvideMetadat /// For graph types other than operations, the field where this type was referenced; for query arguments, the field to which this argument belongs. /// For graph types, the graph type for the field where this type was referenced; for field types, the graph type to which this field belongs; for query arguments, the graph type for the field to which this argument belongs. public readonly record struct ValidationInfo( - IProvideMetadata Obj, + IMetadataReader Obj, ASTNode? Node, IFieldType? ParentFieldType, IGraphType? ParentGraphType, @@ -65,7 +65,7 @@ public readonly record struct ValidationInfo( /// /// Validates authorization rules for the specified schema, graph, field or query argument. - /// Does not consider + /// Does not consider /// as this is handled elsewhere. /// Returns a value indicating if validation was successful for this node. /// diff --git a/src/Transports.AspNetCore/HttpGetValidationRule.cs b/src/Transports.AspNetCore/HttpGetValidationRule.cs index 2a098cb0..78d5048f 100644 --- a/src/Transports.AspNetCore/HttpGetValidationRule.cs +++ b/src/Transports.AspNetCore/HttpGetValidationRule.cs @@ -3,10 +3,10 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// Validates that HTTP GET requests only execute queries; not mutations or subscriptions. /// -public sealed class HttpGetValidationRule : IValidationRule +public sealed class HttpGetValidationRule : ValidationRuleBase { /// - public ValueTask ValidateAsync(ValidationContext context) + public override ValueTask GetPreNodeVisitorAsync(ValidationContext context) { if (context.Operation.Operation != OperationType.Query) { diff --git a/src/Transports.AspNetCore/HttpPostValidationRule.cs b/src/Transports.AspNetCore/HttpPostValidationRule.cs index 9138a9ea..e8b2e0a3 100644 --- a/src/Transports.AspNetCore/HttpPostValidationRule.cs +++ b/src/Transports.AspNetCore/HttpPostValidationRule.cs @@ -3,10 +3,10 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// Validates that HTTP POST requests do not execute subscriptions. /// -public sealed class HttpPostValidationRule : IValidationRule +public sealed class HttpPostValidationRule : ValidationRuleBase { /// - public ValueTask ValidateAsync(ValidationContext context) + public override ValueTask GetPreNodeVisitorAsync(ValidationContext context) { if (context.Operation.Operation == OperationType.Subscription) { diff --git a/tests/ApiApprovalTests/ApiApprovalTests.cs b/tests/ApiApprovalTests/ApiApprovalTests.cs index e450ea7c..08978814 100644 --- a/tests/ApiApprovalTests/ApiApprovalTests.cs +++ b/tests/ApiApprovalTests/ApiApprovalTests.cs @@ -14,9 +14,6 @@ public class ApiApprovalTests [InlineData(typeof(Server.Ui.Playground.PlaygroundMiddleware))] [InlineData(typeof(Server.Ui.Voyager.VoyagerMiddleware))] [InlineData(typeof(Server.Transports.AspNetCore.GraphQLHttpMiddleware<>))] -#pragma warning disable CS0618 // Type or member is obsolete - [InlineData(typeof(Server.Authorization.AspNetCore.AuthorizationValidationRule))] -#pragma warning restore CS0618 // Type or member is obsolete public void public_api_should_not_change_unintentionally(Type type) { string baseDir = AppDomain.CurrentDomain.BaseDirectory; diff --git a/tests/ApiApprovalTests/ApiApprovalTests.csproj b/tests/ApiApprovalTests/ApiApprovalTests.csproj index 70589ab0..12c490ee 100644 --- a/tests/ApiApprovalTests/ApiApprovalTests.csproj +++ b/tests/ApiApprovalTests/ApiApprovalTests.csproj @@ -10,7 +10,6 @@ - diff --git a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt index d3c12fde..693815b1 100644 --- a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -15,10 +15,10 @@ namespace GraphQL.Server.Transports.AspNetCore public System.Func? OnNotAuthorizedPolicy { get; } public System.Func? OnNotAuthorizedRole { get; } } - public class AuthorizationValidationRule : GraphQL.Validation.IValidationRule + public class AuthorizationValidationRule : GraphQL.Validation.ValidationRuleBase { public AuthorizationValidationRule() { } - public virtual System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public class AuthorizationVisitor : GraphQL.Server.Transports.AspNetCore.AuthorizationVisitorBase { @@ -47,10 +47,10 @@ namespace GraphQL.Server.Transports.AspNetCore 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) { } + public ValidationInfo(GraphQL.Types.IMetadataReader Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { } public GraphQL.Validation.ValidationContext Context { get; init; } public GraphQLParser.AST.ASTNode? Node { get; init; } - public GraphQL.Types.IProvideMetadata Obj { get; init; } + public GraphQL.Types.IMetadataReader Obj { get; init; } public GraphQL.Types.IFieldType? ParentFieldType { get; init; } public GraphQL.Types.IGraphType? ParentGraphType { get; init; } } @@ -147,15 +147,15 @@ namespace GraphQL.Server.Transports.AspNetCore public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options, Microsoft.Extensions.Hosting.IHostApplicationLifetime hostApplicationLifetime) { } protected override System.Threading.Tasks.ValueTask?> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload) { } } - public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpGetValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpGetValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } - public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpPostValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpPostValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public interface IAuthorizationOptions { diff --git a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt index d2d58630..9167836f 100644 --- a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -22,10 +22,10 @@ namespace GraphQL.Server.Transports.AspNetCore public System.Func? OnNotAuthorizedPolicy { get; } public System.Func? OnNotAuthorizedRole { get; } } - public class AuthorizationValidationRule : GraphQL.Validation.IValidationRule + public class AuthorizationValidationRule : GraphQL.Validation.ValidationRuleBase { public AuthorizationValidationRule() { } - public virtual System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public class AuthorizationVisitor : GraphQL.Server.Transports.AspNetCore.AuthorizationVisitorBase { @@ -54,10 +54,10 @@ namespace GraphQL.Server.Transports.AspNetCore 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) { } + public ValidationInfo(GraphQL.Types.IMetadataReader Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { } public GraphQL.Validation.ValidationContext Context { get; init; } public GraphQLParser.AST.ASTNode? Node { get; init; } - public GraphQL.Types.IProvideMetadata Obj { get; init; } + public GraphQL.Types.IMetadataReader Obj { get; init; } public GraphQL.Types.IFieldType? ParentFieldType { get; init; } public GraphQL.Types.IGraphType? ParentGraphType { get; init; } } @@ -161,15 +161,15 @@ namespace GraphQL.Server.Transports.AspNetCore public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) { } public System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { } } - public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpGetValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpGetValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } - public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpPostValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpPostValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public interface IAuthorizationOptions { diff --git a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt index 4fdc10f2..246ddb6c 100644 --- a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -15,10 +15,10 @@ namespace GraphQL.Server.Transports.AspNetCore public System.Func? OnNotAuthorizedPolicy { get; } public System.Func? OnNotAuthorizedRole { get; } } - public class AuthorizationValidationRule : GraphQL.Validation.IValidationRule + public class AuthorizationValidationRule : GraphQL.Validation.ValidationRuleBase { public AuthorizationValidationRule() { } - public virtual System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public class AuthorizationVisitor : GraphQL.Server.Transports.AspNetCore.AuthorizationVisitorBase { @@ -47,10 +47,10 @@ namespace GraphQL.Server.Transports.AspNetCore 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) { } + public ValidationInfo(GraphQL.Types.IMetadataReader Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { } public GraphQL.Validation.ValidationContext Context { get; init; } public GraphQLParser.AST.ASTNode? Node { get; init; } - public GraphQL.Types.IProvideMetadata Obj { get; init; } + public GraphQL.Types.IMetadataReader Obj { get; init; } public GraphQL.Types.IFieldType? ParentFieldType { get; init; } public GraphQL.Types.IGraphType? ParentGraphType { get; init; } } @@ -147,15 +147,15 @@ namespace GraphQL.Server.Transports.AspNetCore public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options, Microsoft.Extensions.Hosting.IHostApplicationLifetime hostApplicationLifetime) { } protected override System.Threading.Tasks.ValueTask?> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload) { } } - public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpGetValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpGetValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } - public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule + public sealed class HttpPostValidationRule : GraphQL.Validation.ValidationRuleBase { public HttpPostValidationRule() { } - public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + public override System.Threading.Tasks.ValueTask GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { } } public interface IAuthorizationOptions { diff --git a/tests/Authorization.AspNetCore.Tests/Authorization.AspNetCore.Tests.csproj b/tests/Authorization.AspNetCore.Tests/Authorization.AspNetCore.Tests.csproj deleted file mode 100644 index 70a9eb18..00000000 --- a/tests/Authorization.AspNetCore.Tests/Authorization.AspNetCore.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 - disable - $(NoWarn);CS0618 - - - - - - - - - - - - - diff --git a/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs b/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs deleted file mode 100644 index bc84d099..00000000 --- a/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs +++ /dev/null @@ -1,426 +0,0 @@ -using GraphQL.Types; -using GraphQL.Types.Relay.DataObjects; - -namespace GraphQL.Server.Authorization.AspNetCore.Tests; - -public class AuthorizationValidationRuleTests : ValidationTestBase -{ - // https://github.com/graphql-dotnet/server/issues/463 - [Fact] - public void policy_on_schema_success() - { - ConfigureAuthorizationOptions(options => - { - options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); - options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); - }); - - ShouldPassRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" }, - { "some", "abcdef" } - }); - }); - } - - // https://github.com/graphql-dotnet/server/issues/463 - [Fact] - public void policy_on_schema_fail() - { - ConfigureAuthorizationOptions(options => - { - options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); - options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); - }); - - ShouldFailRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" }, - }); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'some' is not present. - """); - }; - }); - } - - [Fact] - public void class_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void class_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void method_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void property_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void method_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void property_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void nested_type_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = "query { post }"; - config.Schema = NestedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void nested_type_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { post }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void passes_with_claim_on_input_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = """query { author(input: { name: "Quinn" }) }"""; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void nested_type_list_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { posts }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void nested_type_list_non_null_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { postsNonNull }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact(Skip = "Not supported in v7")] - public void fails_on_missing_claim_on_input_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = """query { author(input: { name: "Quinn" }) }"""; - config.Schema = TypedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - [Fact] - public void passes_with_policy_on_connection_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); - - ShouldPassRule(config => - { - config.Query = "query { posts { items { id } } }"; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); - }); - } - - [Fact] - public void fails_on_missing_claim_on_connection_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); - - ShouldFailRule(config => - { - config.Query = "query { posts { items { id } } }"; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(""" - You are not authorized to run this query. - Required claim 'admin' is not present. - """); - }; - }); - } - - private static Schema BasicSchema() - { - string defs = """ - type Query { - post(id: ID!): String - } - """; - - return Schema.For(defs, _ => _.Types.Include()); - } - - [GraphQLMetadata("Query")] - [Authorize("ClassPolicy")] - public class BasicQueryWithAttributesAndClassPolicy - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public string Post(string id) => ""; - } - - [GraphQLMetadata("Query")] - public class BasicQueryWithAttributesAndMethodPolicy - { - [Authorize("FieldPolicy")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public string Post(string id) => ""; - } - - [GraphQLMetadata("Query")] - public class BasicQueryWithAttributesAndPropertyPolicy - { - [Authorize("FieldPolicy")] - public string Post { get; set; } = ""; - } - - private Schema NestedSchema() - { - string defs = """ - type Query { - post(id: ID!): Post - posts: [Post] - postsNonNull: [Post!]! - } - - type Post { - id: ID! - } - """; - - return Schema.For(defs, _ => - { - _.Types.Include(); - _.Types.Include(); - }); - } - - [GraphQLMetadata("Query")] - public class NestedQueryWithAttributes - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public Post Post(string id) => null; - - public IEnumerable Posts() => null; - - public IEnumerable PostsNonNull() => null; - } - - [Authorize("PostPolicy")] - public class Post - { - public string Id { get; set; } - } - - public class PostGraphType : ObjectGraphType - { - public PostGraphType() - { - Field(p => p.Id); - } - } - - public class Author - { - public string Name { get; set; } - } - - private Schema TypedSchema() - { - var query = new ObjectGraphType(); - - query.Field( - "author", - arguments: new QueryArguments(new QueryArgument { Name = "input" }), - resolve: context => "testing" - ); - - query.Connection() - .Name("posts") - .AuthorizeWithPolicy("ConnectionPolicy") - .Resolve(ctx => new Connection()); - - return new Schema { Query = query }; - } - - public class AuthorInputType : InputObjectGraphType - { - public AuthorInputType() - { - Field(x => x.Name).AuthorizeWithPolicy("FieldPolicy"); - } - } -} diff --git a/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs b/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs deleted file mode 100644 index 2e717a83..00000000 --- a/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Security.Claims; -using GraphQL.Execution; -using GraphQL.Validation; -using GraphQLParser.AST; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; - -namespace GraphQL.Server.Authorization.AspNetCore.Tests; - -public class ValidationTestBase : IDisposable -{ - protected ServiceProvider ServiceProvider { get; private set; } - - protected HttpContext HttpContext { get; private set; } - - protected AuthorizationValidationRule Rule { get; private set; } - - protected void ConfigureAuthorizationOptions(Action setupOptions) - { - var (authorizationService, httpContextAccessor) = BuildServices(setupOptions); - HttpContext = httpContextAccessor.HttpContext; - Rule = new AuthorizationValidationRule(authorizationService, new DefaultClaimsPrincipalAccessor(httpContextAccessor), new DefaultAuthorizationErrorMessageBuilder()); - } - - protected void ShouldPassRule(Action configure) - { - var config = new ValidationTestConfig(); - config.Rules.Add(Rule); - configure(config); - - config.Rules.ShouldNotBeEmpty("Must provide at least one rule to validate against."); - - config.Schema.Initialize(); - - var result = Validate(config); - - string message = ""; - if (result.Errors?.Count > 0) - { - message = string.Join(", ", result.Errors.Select(x => x.Message)); - } - result.IsValid.ShouldBeTrue(message); - config.ValidateResult(result); - } - - protected void ShouldFailRule(Action configure) - { - var config = new ValidationTestConfig(); - config.Rules.Add(Rule); - config.User = CreatePrincipal(); - configure(config); - - config.Rules.ShouldNotBeEmpty("Must provide at least one rule to validate against."); - - config.Schema.Initialize(); - - var result = Validate(config); - - result.IsValid.ShouldBeFalse("Expected validation errors though there were none."); - config.ValidateResult(result); - } - - private (IAuthorizationService, IHttpContextAccessor) BuildServices(Action setupOptions) - { - if (ServiceProvider != null) - throw new InvalidOperationException("BuildServices has been already called"); - - var services = new ServiceCollection() - .AddAuthorization(setupOptions) - .AddLogging() - .AddOptions() - .AddHttpContextAccessor(); - - ServiceProvider = services.BuildServiceProvider(); - - var authorizationService = ServiceProvider.GetRequiredService(); - var httpContextAccessor = ServiceProvider.GetRequiredService(); - - httpContextAccessor.HttpContext = new DefaultHttpContext(); - return (authorizationService, httpContextAccessor); - } - - private IValidationResult Validate(ValidationTestConfig config) - { - HttpContext.User = config.User; - var documentBuilder = new GraphQLDocumentBuilder(); - var document = documentBuilder.Build(config.Query); - var validator = new DocumentValidator(); - return validator.ValidateAsync(new ValidationOptions - { - Schema = config.Schema, - Document = document, - Operation = document.Definitions.OfType().First(), - Rules = config.Rules, - Variables = config.Variables - }).GetAwaiter().GetResult().validationResult; - } - - protected ClaimsPrincipal CreatePrincipal(string authenticationType = "Bearer", IDictionary claims = null) - { - var claimsList = new List(); - - if (claims != null) - { - foreach (var c in claims) - claimsList.Add(new Claim(c.Key, c.Value)); - } - - return new ClaimsPrincipal(new ClaimsIdentity(claimsList, authenticationType)); - } - - public void Dispose() - { - ServiceProvider.Dispose(); - } -} diff --git a/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs b/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs deleted file mode 100644 index a9bbe644..00000000 --- a/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Security.Claims; -using GraphQL.Types; -using GraphQL.Validation; - -namespace GraphQL.Server.Authorization.AspNetCore.Tests; - -public class ValidationTestConfig -{ - public string Query { get; set; } - - public ISchema Schema { get; set; } - - public List Rules { get; set; } = new List(); - - public ClaimsPrincipal User { get; set; } - - public Inputs Variables { get; set; } - - public Action ValidateResult { get; set; } = _ => { }; -} diff --git a/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs b/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs index 617f9fbc..bc83b03f 100644 --- a/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs +++ b/tests/Transports.AspNetCore.Tests/AuthorizationTests.cs @@ -57,7 +57,7 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true var inputs = new GraphQLSerializer().Deserialize(variables) ?? Inputs.Empty; var validator = new DocumentValidator(); - var (coreRulesResult, _) = validator.ValidateAsync(new ValidationOptions + var validationResult = validator.ValidateAsync(new ValidationOptions { Document = document, Extensions = Inputs.Empty, @@ -68,9 +68,9 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true RequestServices = mockServices.Object, User = _principal, }).GetAwaiter().GetResult(); // there is no async code being tested - coreRulesResult.IsValid.ShouldBe(shouldPassCoreRules); + validationResult.IsValid.ShouldBe(shouldPassCoreRules); - var (result, _) = validator.ValidateAsync(new ValidationOptions + var result = validator.ValidateAsync(new ValidationOptions { Document = document, Extensions = Inputs.Empty, @@ -507,7 +507,7 @@ public void UnusedFragmentsAreIgnored() ret.IsValid.ShouldBeFalse(); } - private void Apply(IProvideMetadata obj, Mode mode) + private void Apply(IMetadataWriter obj, Mode mode) { switch (mode) { @@ -600,7 +600,7 @@ public async Task NullIdentity() var validator = new DocumentValidator(); _schema.Authorize(); - var (result, _) = await validator.ValidateAsync(new ValidationOptions + var result = await validator.ValidateAsync(new ValidationOptions { Document = document, Extensions = Inputs.Empty,