diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs index 53dfcda1156..610bae912a5 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs @@ -115,7 +115,7 @@ public static IRequestExecutorBuilder AddGraphQLServer( if (!disableDefaultSecurity) { builder.AddCostAnalyzer(); - builder.AddIntrospectionAllowedRule( + builder.DisableIntrospection( (sp, _) => { var environment = sp.GetService(); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/IntrospectionTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/IntrospectionTests.cs index aa2a3a76e7f..373c57753ed 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/IntrospectionTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/IntrospectionTests.cs @@ -1,5 +1,7 @@ +using System.Net; using CookieCrumble; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Transport; using HotChocolate.Transport.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -77,6 +79,152 @@ public async Task Introspection_Request_With_Rule_Removed_Fail(string environmen } #endif + + [Fact] + public async Task Introspection_OfType_Depth_1_BadRequest() + { + // arrange + var server = CreateStarWarsServer( + configureServices: s => s + .AddGraphQL() + .SetIntrospectionAllowedDepth( + maxAllowedOfTypeDepth: 1, + maxAllowedListRecursiveDepth: 1)); + + var request = new GraphQLHttpRequest( + new OperationRequest( + """ + { + __schema { + types { + ofType { + ofType { + name + } + } + } + } + } + """), + new Uri("http://localhost:5000/graphql")); + + // act + var client = new DefaultGraphQLHttpClient(server.CreateClient()); + using var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Introspection_OfType_Depth_1_Depth_Analysis_Disabled() + { + // arrange + var server = CreateStarWarsServer( + configureServices: s => s + .AddGraphQL() + .SetIntrospectionAllowedDepth( + maxAllowedOfTypeDepth: 1, + maxAllowedListRecursiveDepth: 1) + .Services + .AddValidation() + .ConfigureValidation(b => b.Modifiers.Add(o => o.DisableDepthRule = true))); + + var request = new GraphQLHttpRequest( + new OperationRequest( + """ + { + __schema { + types { + ofType { + ofType { + name + } + } + } + } + } + """), + new Uri("http://localhost:5000/graphql")); + + // act + var client = new DefaultGraphQLHttpClient(server.CreateClient()); + using var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Introspection_Disabled_BadRequest() + { + // arrange + var server = CreateStarWarsServer( + configureServices: s => s + .AddGraphQL() + .DisableIntrospection()); + + var request = new GraphQLHttpRequest( + new OperationRequest( + """ + { + __schema { + types { + ofType { + ofType { + name + } + } + } + } + } + """), + new Uri("http://localhost:5000/graphql")); + + // act + var client = new DefaultGraphQLHttpClient(server.CreateClient()); + using var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Introspection_OfType_Depth_2_OK() + { + // arrange + var server = CreateStarWarsServer( + configureServices: s => s + .AddGraphQL() + .SetIntrospectionAllowedDepth( + maxAllowedOfTypeDepth: 2, + maxAllowedListRecursiveDepth: 1)); + + var request = new GraphQLHttpRequest( + new OperationRequest( + """ + { + __schema { + types { + ofType { + ofType { + name + } + } + } + } + } + """), + new Uri("http://localhost:5000/graphql")); + + // act + var client = new DefaultGraphQLHttpClient(server.CreateClient()); + using var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + private GraphQLHttpClient GetClient(string environment, bool removeRule = false) { var server = CreateStarWarsServer( @@ -85,7 +233,8 @@ private GraphQLHttpClient GetClient(string environment, bool removeRule = false) { if (removeRule) { - s.AddGraphQL().RemoveIntrospectionAllowedRule(); + s.AddGraphQL() + .DisableIntrospection(disable: false); } }); return new DefaultGraphQLHttpClient(server.CreateClient()); diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs index ab5066b6719..699d65bbe02 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs @@ -186,7 +186,7 @@ public static IRequestExecutorBuilder AddValidationResultAggregator( /// Specifies if depth analysis is skipped for introspection queries. /// /// - /// Defines if request depth overrides are allowed on a per request basis. + /// Defines if request depth overrides are allowed on a per-request basis. /// /// /// Defines if the validation rule is enabled. @@ -220,21 +220,13 @@ public static IRequestExecutorBuilder AddMaxExecutionDepthRule( /// Adds a validation rule that only allows requests to use `__schema` or `__type` /// if the request carries an introspection allowed flag. /// + [Obsolete("Use `DisableIntrospection` instead.")] public static IRequestExecutorBuilder AddIntrospectionAllowedRule( - this IRequestExecutorBuilder builder, - Func? isEnabled = null) - => ConfigureValidation(builder, b => b.AddIntrospectionAllowedRule(isEnabled)); - - /// - /// Removes a validation rule that only allows requests to use `__schema` or `__type` - /// if the request carries an introspection allowed flag. - /// - public static IRequestExecutorBuilder RemoveIntrospectionAllowedRule( this IRequestExecutorBuilder builder) - => ConfigureValidation(builder, b => b.RemoveIntrospectionAllowedRule()); + => DisableIntrospection(builder); /// - /// Toggle whether introspection is allow or not. + /// Toggle whether introspection is allowed or not. /// /// /// The . @@ -244,17 +236,49 @@ public static IRequestExecutorBuilder RemoveIntrospectionAllowedRule( /// If `false` introspection is disallowed, except for requests /// that carry an introspection allowed flag. /// + [Obsolete("Use `DisableIntrospection` instead.")] public static IRequestExecutorBuilder AllowIntrospection( this IRequestExecutorBuilder builder, bool allow) - { - if (!allow) - { - builder.AddIntrospectionAllowedRule(); - } + => DisableIntrospection(builder, disable: !allow); - return builder; - } + /// + /// Toggle whether introspection is disabled or not. + /// + /// + /// The . + /// + /// + /// If `true` introspection is disabled, except for requests + /// that carry an introspection allowed flag. + /// If `false` introspection is enabled. + /// + public static IRequestExecutorBuilder DisableIntrospection( + this IRequestExecutorBuilder builder, + bool disable = true) + => ConfigureValidation( + builder, + b => b.ModifyValidationOptions( + o => o.DisableIntrospection = disable)); + + /// + /// Toggle whether introspection is disabled or not. + /// + /// + /// The . + /// + /// + /// If `true` introspection is disabled, except for requests + /// that carry an introspection allowed flag. + /// If `false` introspection is enabled. + /// + public static IRequestExecutorBuilder DisableIntrospection( + this IRequestExecutorBuilder builder, + Func disable) + => ConfigureValidation( + builder, + b => b.ModifyValidationOptions( + (s, o) => o.DisableIntrospection = disable(s, o))); /// /// Sets the max allowed document validation errors. @@ -283,6 +307,47 @@ public static IRequestExecutorBuilder SetMaxAllowedValidationErrors( builder, b => b.ConfigureValidation( c => c.Modifiers.Add(o => o.MaxAllowedErrors = maxAllowedValidationErrors))); + + return builder; + } + + /// + /// Sets the max allowed depth for introspection queries. + /// + /// + /// The . + /// + /// + /// The max allowed ofType depth for introspection queries. + /// + /// + /// The max allowed list recursive depth for introspection queries. + /// + /// + /// Returns an that can be used to chain + /// + /// + /// is null. + /// + public static IRequestExecutorBuilder SetIntrospectionAllowedDepth( + this IRequestExecutorBuilder builder, + ushort maxAllowedOfTypeDepth, + ushort maxAllowedListRecursiveDepth) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + ConfigureValidation( + builder, + b => b.ConfigureValidation( + c => c.Modifiers.Add(o => + { + o.MaxAllowedOfTypeDepth = maxAllowedOfTypeDepth; + o.MaxAllowedListRecursiveDepth = maxAllowedListRecursiveDepth; + }))); + return builder; } diff --git a/src/HotChocolate/Core/src/Execution/Extensions/HotChocolateExecutionSelectionExtensions.cs b/src/HotChocolate/Core/src/Execution/Extensions/HotChocolateExecutionSelectionExtensions.cs index cc26612f1d7..2c7c85881b2 100644 --- a/src/HotChocolate/Core/src/Execution/Extensions/HotChocolateExecutionSelectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/Extensions/HotChocolateExecutionSelectionExtensions.cs @@ -1,3 +1,4 @@ +#if NET6_0_OR_GREATER using System.Buffers.Text; using System.Linq.Expressions; using System.Text; @@ -70,3 +71,4 @@ private static int EstimateIntLength(int value) return length; } } +#endif diff --git a/src/HotChocolate/Core/src/Validation/DefaultDocumentValidatorFactory.cs b/src/HotChocolate/Core/src/Validation/DefaultDocumentValidatorFactory.cs index 2f8677f4207..76fdca737ee 100644 --- a/src/HotChocolate/Core/src/Validation/DefaultDocumentValidatorFactory.cs +++ b/src/HotChocolate/Core/src/Validation/DefaultDocumentValidatorFactory.cs @@ -19,6 +19,8 @@ public IDocumentValidator CreateValidator(string? schemaName = default) { schemaName ??= Schema.DefaultName; var options = _configuration.GetOptions(schemaName); - return new DocumentValidator(_contextPool, options.Rules, options.ResultAggregators, options); + var rules = _configuration.GetRules(schemaName); + var aggregators = _configuration.GetResultAggregators(schemaName); + return new DocumentValidator(_contextPool, rules, aggregators, options); } } diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs index c0439f486da..2a4c85e0a7b 100644 --- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs +++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs @@ -93,11 +93,11 @@ public static IValidationBuilder AddDocumentRules( this IValidationBuilder builder) { return builder.ConfigureValidation( - m => m.Modifiers.Add(o => + m => m.RulesModifiers.Add((_, r) => { - if (o.Rules.All(t => t.GetType() != typeof(DocumentRule))) + if (r.Rules.All(t => t.GetType() != typeof(DocumentRule))) { - o.Rules.Add(new DocumentRule()); + r.Rules.Add(new DocumentRule()); } })); } @@ -342,40 +342,27 @@ public static IValidationBuilder AddMaxExecutionDepthRule( }); } - /// - /// Removes a validation rule that restricts the depth of a GraphQL request. - /// - public static IValidationBuilder RemoveMaxExecutionDepthRule( - this IValidationBuilder builder) - => builder.TryRemoveValidationVisitor(); - /// /// Adds a validation rule that only allows requests to use `__schema` or `__type` /// if the request carries an introspection allowed flag. /// public static IValidationBuilder AddIntrospectionAllowedRule( - this IValidationBuilder builder, - Func? isEnabled = null) + this IValidationBuilder builder) => builder.TryAddValidationVisitor( (_, _) => new IntrospectionVisitor(), priority: 0, isCacheable: false, - isEnabled: isEnabled); - - /// - /// Removes a validation rule that only allows requests to use `__schema` or `__type` - /// if the request carries an introspection allowed flag. - /// - public static IValidationBuilder RemoveIntrospectionAllowedRule( - this IValidationBuilder builder) - => builder.TryRemoveValidationVisitor(); + isEnabled: (_, o) => o.DisableIntrospection); /// /// Adds a validation rule that restricts the depth of a GraphQL introspection request. /// public static IValidationBuilder AddIntrospectionDepthRule( this IValidationBuilder builder) - => builder.TryAddValidationVisitor(priority: 1); + => builder.TryAddValidationVisitor( + priority: 1, + factory: (_, o) => new IntrospectionDepthVisitor(o), + isEnabled: (_, o) => !o.DisableDepthRule); /// /// Adds a validation rule that restricts the depth of coordinate cycles in GraphQL operations. diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs index 887fb2b555d..dcc9a0c2dbd 100644 --- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs +++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs @@ -2,6 +2,7 @@ using HotChocolate.Validation.Options; using Microsoft.Extensions.Options; +// ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; /// @@ -94,11 +95,28 @@ public static IValidationBuilder ConfigureValidation( /// /// Returns the validation builder for configuration chaining. /// - internal static IValidationBuilder ModifyValidationOptions( + public static IValidationBuilder ModifyValidationOptions( this IValidationBuilder builder, Action configure) => builder.ConfigureValidation(m => m.Modifiers.Add(configure)); + /// + /// Modifies the validation options object. + /// + /// + /// The validation builder. + /// + /// + /// The delegate to mutate the validation options. + /// + /// + /// Returns the validation builder for configuration chaining. + /// + public static IValidationBuilder ModifyValidationOptions( + this IValidationBuilder builder, + Action configure) + => builder.ConfigureValidation((s, m) => m.Modifiers.Add(o => configure(s, o))); + /// /// Registers the specified validation visitor, /// if the same type of validation visitor was not yet registered. @@ -124,11 +142,11 @@ public static IValidationBuilder TryAddValidationVisitor( where T : DocumentValidatorVisitor, new() { return builder.ConfigureValidation(m => - m.Modifiers.Add(o => + m.RulesModifiers.Add((_, r) => { - if (o.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule))) + if (r.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule))) { - o.Rules.Add(new DocumentValidatorRule(new T(), isCacheable, priority)); + r.Rules.Add(new DocumentValidatorRule(new T(), isCacheable, priority)); } })); } @@ -166,12 +184,12 @@ public static IValidationBuilder TryAddValidationVisitor( where T : DocumentValidatorVisitor { return builder.ConfigureValidation((s, m) => - m.Modifiers.Add(o => + m.RulesModifiers.Add((o, r) => { - if (o.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule)) + if (r.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule)) && (isEnabled?.Invoke(s, o) ?? true)) { - o.Rules.Add(new DocumentValidatorRule(factory(s, o), isCacheable, priority)); + r.Rules.Add(new DocumentValidatorRule(factory(s, o), isCacheable, priority)); } })); } @@ -184,12 +202,12 @@ public static IValidationBuilder TryRemoveValidationVisitor( where T : DocumentValidatorVisitor { return builder.ConfigureValidation((_, m) => - m.Modifiers.Add(o => + m.RulesModifiers.Add((_, r) => { - var entries = o.Rules.Where(t => t.GetType() == typeof(DocumentValidatorRule)).ToList(); + var entries = r.Rules.Where(t => t.GetType() == typeof(DocumentValidatorRule)).ToList(); foreach (var entry in entries) { - o.Rules.Remove(entry); + r.Rules.Remove(entry); } })); } @@ -210,11 +228,11 @@ public static IValidationBuilder TryAddValidationRule( where T : class, IDocumentValidatorRule, new() { return builder.ConfigureValidation(m => - m.Modifiers.Add(o => + m.RulesModifiers.Add((_, r) => { - if (o.Rules.All(t => t.GetType() != typeof(T))) + if (r.Rules.All(t => t.GetType() != typeof(T))) { - o.Rules.Add(new T()); + r.Rules.Add(new T()); } })); } @@ -239,12 +257,12 @@ public static IValidationBuilder TryAddValidationRule( where T : class, IDocumentValidatorRule { return builder.ConfigureValidation((s, m) => - m.Modifiers.Add(o => + m.RulesModifiers.Add((o, r) => { var instance = factory(s, o); - if (o.Rules.All(t => t.GetType() != instance.GetType())) + if (r.Rules.All(t => t.GetType() != instance.GetType())) { - o.Rules.Add(instance); + r.Rules.Add(instance); } })); } @@ -269,12 +287,12 @@ public static IValidationBuilder TryAddValidationResultAggregator( where T : class, IValidationResultAggregator { return builder.ConfigureValidation((s, m) => - m.Modifiers.Add(o => + m.RulesModifiers.Add((o, r) => { var instance = factory(s, o); - if (o.ResultAggregators.All(t => t.GetType() != instance.GetType())) + if (r.ResultAggregators.All(t => t.GetType() != instance.GetType())) { - o.ResultAggregators.Add(instance); + r.ResultAggregators.Add(instance); } })); } diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs index 308c8800bea..f45aee12809 100644 --- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ public static IValidationBuilder AddValidation( var builder = new DefaultValidationBuilder(schemaName, services); builder + .AddIntrospectionAllowedRule() .AddIntrospectionDepthRule() .AddDocumentRules() .AddOperationRules() diff --git a/src/HotChocolate/Core/src/Validation/Options/IIntrospectionOptionsAccessor.cs b/src/HotChocolate/Core/src/Validation/Options/IIntrospectionOptionsAccessor.cs new file mode 100644 index 00000000000..44ab26a2e36 --- /dev/null +++ b/src/HotChocolate/Core/src/Validation/Options/IIntrospectionOptionsAccessor.cs @@ -0,0 +1,29 @@ +namespace HotChocolate.Validation.Options; + +/// +/// Represents the options for introspection rules. +/// +public interface IIntrospectionOptionsAccessor +{ + /// + /// Defines if introspection is disabled. + /// + bool DisableIntrospection { get; } + + /// + /// Defines if the introspection depth rule is disabled. + /// + bool DisableDepthRule { get; } + + /// + /// Specifies the maximum allowed `ofType` field depth + /// when running the introspection depth rule. + /// + ushort MaxAllowedOfTypeDepth { get; } + + /// + /// Specifies the maximum allowed list recursive depth + /// when running the introspection depth rule. + /// + ushort MaxAllowedListRecursiveDepth { get; } +} diff --git a/src/HotChocolate/Core/src/Validation/Options/ValidationConfiguration.cs b/src/HotChocolate/Core/src/Validation/Options/ValidationConfiguration.cs index 97730ada474..2040708c1d0 100644 --- a/src/HotChocolate/Core/src/Validation/Options/ValidationConfiguration.cs +++ b/src/HotChocolate/Core/src/Validation/Options/ValidationConfiguration.cs @@ -3,36 +3,42 @@ namespace HotChocolate.Validation.Options; -public class ValidationConfiguration : IValidationConfiguration +public class ValidationConfiguration( + IOptionsMonitor optionsMonitor) + : IValidationConfiguration { - private readonly ConcurrentDictionary _optionsCache = new(); - private readonly IOptionsMonitor _optionsMonitor; - - public ValidationConfiguration(IOptionsMonitor optionsMonitor) - { - _optionsMonitor = optionsMonitor - ?? throw new ArgumentNullException(nameof(optionsMonitor)); - } + private readonly ConcurrentDictionary _optionsCache = new(); + private readonly IOptionsMonitor _optionsMonitor = optionsMonitor + ?? throw new ArgumentNullException(nameof(optionsMonitor)); public IEnumerable GetRules(string schemaName) - => GetOptions(schemaName).Rules; + => GetRulesOptions(schemaName).Rules; public IEnumerable GetResultAggregators(string schemaName) - => GetOptions(schemaName).ResultAggregators; + => GetRulesOptions(schemaName).ResultAggregators; public ValidationOptions GetOptions(string schemaName) - => _optionsCache.GetOrAdd(schemaName, CreateOptions); + => _optionsCache.GetOrAdd(schemaName, CreateOptions).Item1; + + private ValidationRulesOptions GetRulesOptions(string schemaName) + => _optionsCache.GetOrAdd(schemaName, CreateOptions).Item2; - private ValidationOptions CreateOptions(string schemaName) + private (ValidationOptions, ValidationRulesOptions) CreateOptions(string schemaName) { var modifiers = _optionsMonitor.Get(schemaName); var options = new ValidationOptions(); + var rulesOptions = new ValidationRulesOptions(); for (var i = 0; i < modifiers.Modifiers.Count; i++) { modifiers.Modifiers[i](options); } - return options; + for (var i = 0; i < modifiers.RulesModifiers.Count; i++) + { + modifiers.RulesModifiers[i](options, rulesOptions); + } + + return (options, rulesOptions); } } diff --git a/src/HotChocolate/Core/src/Validation/Options/ValidationOptions.cs b/src/HotChocolate/Core/src/Validation/Options/ValidationOptions.cs index c55b6246db7..f9496ae1a84 100644 --- a/src/HotChocolate/Core/src/Validation/Options/ValidationOptions.cs +++ b/src/HotChocolate/Core/src/Validation/Options/ValidationOptions.cs @@ -3,24 +3,15 @@ namespace HotChocolate.Validation.Options; /// /// The validation options. /// -public class ValidationOptions +public sealed class ValidationOptions : IMaxExecutionDepthOptionsAccessor , IErrorOptionsAccessor + , IIntrospectionOptionsAccessor { private int? _maxAllowedExecutionDepth; private int _maxErrors = 5; - - /// - /// Gets the document rules of the validation. - /// - public IList Rules { get; } = - new List(); - - /// - /// Gets the document rules that run async logic after the initial validators have run.. - /// - public IList ResultAggregators { get; } = - new List(); + private ushort _maxAllowedOfTypeDepth = 16; + private ushort _maxAllowedListRecursiveDepth = 1; /// /// Gets the maximum allowed depth of a query. The default value is @@ -54,7 +45,24 @@ public int MaxAllowedErrors { value = 5; } + _maxErrors = value; } } + + public bool DisableIntrospection { get; set; } + + public bool DisableDepthRule { get; set; } + + public ushort MaxAllowedOfTypeDepth + { + get => _maxAllowedOfTypeDepth; + set => _maxAllowedOfTypeDepth = value > 0 ? value : (ushort)1; + } + + public ushort MaxAllowedListRecursiveDepth + { + get => _maxAllowedListRecursiveDepth; + set => _maxAllowedListRecursiveDepth = value > 0 ? value : (ushort)16; + } } diff --git a/src/HotChocolate/Core/src/Validation/Options/ValidationOptionsModifiers.cs b/src/HotChocolate/Core/src/Validation/Options/ValidationOptionsModifiers.cs index c368fb616e8..e2dd55b2d0d 100644 --- a/src/HotChocolate/Core/src/Validation/Options/ValidationOptionsModifiers.cs +++ b/src/HotChocolate/Core/src/Validation/Options/ValidationOptionsModifiers.cs @@ -4,4 +4,7 @@ public class ValidationOptionsModifiers { public IList> Modifiers { get; } = new List>(); + + public IList> RulesModifiers { get; } = + new List>(); } diff --git a/src/HotChocolate/Core/src/Validation/Options/ValidationRulesOptions.cs b/src/HotChocolate/Core/src/Validation/Options/ValidationRulesOptions.cs new file mode 100644 index 00000000000..521c24d18e4 --- /dev/null +++ b/src/HotChocolate/Core/src/Validation/Options/ValidationRulesOptions.cs @@ -0,0 +1,19 @@ +namespace HotChocolate.Validation.Options; + +/// +/// The validation rules options. +/// +public sealed class ValidationRulesOptions +{ + /// + /// Gets the document rules of the validation. + /// + public IList Rules { get; } = + new List(); + + /// + /// Gets the document rules that run async logic after the initial validators have run.. + /// + public IList ResultAggregators { get; } = + new List(); +} diff --git a/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs index c274f1ab743..21a5c294d77 100644 --- a/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs +++ b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs @@ -3,6 +3,7 @@ using HotChocolate.Types; using HotChocolate.Types.Introspection; using HotChocolate.Utilities; +using HotChocolate.Validation.Options; namespace HotChocolate.Validation.Rules; @@ -10,15 +11,17 @@ namespace HotChocolate.Validation.Rules; /// This rules ensures that recursive introspection fields cannot be used /// to create endless cycles. /// -internal sealed class IntrospectionDepthVisitor : TypeDocumentValidatorVisitor +internal sealed class IntrospectionDepthVisitor( + IIntrospectionOptionsAccessor options) + : TypeDocumentValidatorVisitor { private readonly (SchemaCoordinate Coordinate, ushort MaxAllowed)[] _limits = [ - (new SchemaCoordinate("__Type", "fields"), 1), - (new SchemaCoordinate("__Type", "inputFields"), 1), - (new SchemaCoordinate("__Type", "interfaces"), 1), - (new SchemaCoordinate("__Type", "possibleTypes"), 1), - (new SchemaCoordinate("__Type", "ofType"), 8) + (new SchemaCoordinate("__Type", "fields"), options.MaxAllowedListRecursiveDepth), + (new SchemaCoordinate("__Type", "inputFields"), options.MaxAllowedListRecursiveDepth), + (new SchemaCoordinate("__Type", "interfaces"), options.MaxAllowedListRecursiveDepth), + (new SchemaCoordinate("__Type", "possibleTypes"), options.MaxAllowedListRecursiveDepth), + (new SchemaCoordinate("__Type", "ofType"), options.MaxAllowedOfTypeDepth) ]; protected override ISyntaxVisitorAction Enter( diff --git a/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs index 39b96d83efa..5fc5677f833 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs @@ -70,6 +70,7 @@ public void AddValidationRule_2_Factory_Is_Null() } [Fact] + [Obsolete] public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed() { Snapshot.FullName(); @@ -87,6 +88,7 @@ public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed() } [Fact] + [Obsolete] public async Task AllowIntrospection_IntegrationTest_NotAllowed() { Snapshot.FullName(); @@ -104,6 +106,7 @@ public async Task AllowIntrospection_IntegrationTest_NotAllowed() } [Fact] + [Obsolete] public async Task AllowIntrospection_IntegrationTest_Allowed() { Snapshot.FullName(); @@ -121,6 +124,7 @@ public async Task AllowIntrospection_IntegrationTest_Allowed() } [Fact] + [Obsolete] public async Task AllowIntrospection_IntegrationTest_NotAllowed_CustomMessage() { Snapshot.FullName(); @@ -139,6 +143,7 @@ public async Task AllowIntrospection_IntegrationTest_NotAllowed_CustomMessage() } [Fact] + [Obsolete] public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed_CustomMessageFact() { Snapshot.FullName(); @@ -157,6 +162,7 @@ public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed_CustomM } [Fact] + [Obsolete] public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed_CustomMessage() { Snapshot.FullName(); @@ -175,6 +181,7 @@ public async Task AddIntrospectionAllowedRule_IntegrationTest_NotAllowed_CustomM } [Fact] + [Obsolete] public async Task AddIntrospectionAllowedRule_IntegrationTest_Allowed() { Snapshot.FullName(); diff --git a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs index e3fc0c2559b..bb2d2d7ab8b 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs @@ -14,7 +14,7 @@ protected DocumentValidatorVisitorTestBase(Action configure) var builder = serviceCollection .AddValidation() - .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear())) + .ConfigureValidation(c => c.RulesModifiers.Add((_, r) => r.Rules.Clear())) .ModifyValidationOptions(o => o.MaxAllowedErrors = int.MaxValue); configure(builder); diff --git a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs index 4f682209691..4a8e6aa15cf 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs @@ -119,7 +119,7 @@ public void Max_2_Relative_Field_Allowed_Success() var builder = serviceCollection .AddValidation() - .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear())) + .ConfigureValidation(c => c.RulesModifiers.Add((_, r) => r.Rules.Clear())) .ModifyValidationOptions(o => o.MaxAllowedErrors = int.MaxValue); builder.AddMaxAllowedFieldCycleDepthRule( null, @@ -164,7 +164,7 @@ public void Max_1_Relative_Field_Allowed_Fail() var builder = serviceCollection .AddValidation() - .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear())) + .ConfigureValidation(c => c.RulesModifiers.Add((_, r) => r.Rules.Clear())) .ModifyValidationOptions(o => o.MaxAllowedErrors = int.MaxValue); builder.AddMaxAllowedFieldCycleDepthRule( null, diff --git a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionRuleTests.cs index b24019ee484..f5c02993881 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionRuleTests.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionRuleTests.cs @@ -11,11 +11,13 @@ public void IntrospectionNotAllowed_Schema_Field() { ExpectErrors( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __schema - }"); + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ + { + __schema + } + """); } [Fact] @@ -23,15 +25,16 @@ public void IntrospectionNotAllowed_Schema_Field_Custom_MessageFactory() { ExpectErrors( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __schema - }", - new KeyValuePair[] + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ { - new(WellKnownContextData.IntrospectionMessage, new Func(() => "Bar")), - }); + __schema + } + """, + [ + new(WellKnownContextData.IntrospectionMessage, new Func(() => "Bar")) + ]); } [Fact] @@ -39,15 +42,16 @@ public void IntrospectionNotAllowed_Schema_Field_Custom_Message() { ExpectErrors( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __schema - }", - new KeyValuePair[] + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ { - new(WellKnownContextData.IntrospectionMessage, "Baz"), - }); + __schema + } + """, + [ + new(WellKnownContextData.IntrospectionMessage, "Baz") + ]); } [Fact] @@ -55,11 +59,13 @@ public void IntrospectionNotAllowed_Type_Field() { ExpectErrors( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __type(name: ""foo"") - }"); + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ + { + __type(name: "foo") + } + """); } [Fact] @@ -67,11 +73,13 @@ public void IntrospectionAllowed_Typename_Field() { ExpectValid( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __typename - }"); + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ + { + __typename + } + """); } [Fact] @@ -79,16 +87,18 @@ public void IntrospectionAllowed_Schema_Field() { ExpectValid( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @"{ + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ + { __schema { name } - }", - new KeyValuePair[] - { - new(WellKnownContextData.IntrospectionAllowed, null), - }); + } + """, + [ + new(WellKnownContextData.IntrospectionAllowed, null) + ]); } [Fact] @@ -96,22 +106,22 @@ public void IntrospectionAllowed_Type_Field() { ExpectValid( CreateSchema(), - b => b.AddIntrospectionAllowedRule(), - @" - { - __type(name: ""foo"") - }", - new KeyValuePair[] + b => b.AddIntrospectionAllowedRule() + .ModifyValidationOptions(o => o.DisableIntrospection = true), + """ { - new(WellKnownContextData.IntrospectionAllowed, null), - }); + __type(name: "foo") + } + """, + [ + new(WellKnownContextData.IntrospectionAllowed, null) + ]); } - private ISchema CreateSchema() - { - return SchemaBuilder.New() - .AddDocumentFromString(FileResource.Open("IntrospectionSchema.graphql")) + private static ISchema CreateSchema() + => SchemaBuilder.New() + .AddDocumentFromString( + FileResource.Open("IntrospectionSchema.graphql")) .Use(_ => _ => default) .Create(); - } } diff --git a/src/HotChocolate/Core/test/Validation.Tests/TestHelper.cs b/src/HotChocolate/Core/test/Validation.Tests/TestHelper.cs index 3593af31c68..9776202112d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/TestHelper.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/TestHelper.cs @@ -30,7 +30,7 @@ public static void ExpectValid( var builder = serviceCollection .AddValidation() - .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear())); + .ConfigureValidation(c => c.RulesModifiers.Add((_, r) => r.Rules.Clear())); configure(builder); IServiceProvider services = serviceCollection.BuildServiceProvider(); @@ -86,7 +86,7 @@ public static void ExpectErrors( var builder = serviceCollection .AddValidation() - .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear())); + .ConfigureValidation(c => c.RulesModifiers.Add((_, r) => r.Rules.Clear())); configure(builder); IServiceProvider services = serviceCollection.BuildServiceProvider(); diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap index feb6f685f92..fb8497ba68a 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap @@ -1,22 +1,22 @@ -[ +[ { "Message": "Introspection is not allowed for the current request.", "Code": "HC0046", "Path": null, "Locations": [ { - "Line": 3, - "Column": 21 + "Line": 2, + "Column": 5 } ], "Extensions": { "field": { "Kind": "Name", "Location": { - "Start": 39, - "End": 65, - "Line": 3, - "Column": 21 + "Start": 6, + "End": 16, + "Line": 2, + "Column": 5 }, "Value": "__schema" }, diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap index eccb208ea09..66f6cce3e58 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap @@ -1,22 +1,22 @@ -[ +[ { "Message": "Baz", "Code": "HC0046", "Path": null, "Locations": [ { - "Line": 3, - "Column": 21 + "Line": 2, + "Column": 5 } ], "Extensions": { "field": { "Kind": "Name", "Location": { - "Start": 39, - "End": 65, - "Line": 3, - "Column": 21 + "Start": 6, + "End": 16, + "Line": 2, + "Column": 5 }, "Value": "__schema" }, diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_MessageFactory.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_MessageFactory.snap index 1216159f4aa..9037a66d858 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_MessageFactory.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_MessageFactory.snap @@ -1,22 +1,22 @@ -[ +[ { "Message": "Bar", "Code": "HC0046", "Path": null, "Locations": [ { - "Line": 3, - "Column": 21 + "Line": 2, + "Column": 5 } ], "Extensions": { "field": { "Kind": "Name", "Location": { - "Start": 39, - "End": 65, - "Line": 3, - "Column": 21 + "Start": 6, + "End": 16, + "Line": 2, + "Column": 5 }, "Value": "__schema" }, diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap index 5b9ef2cafe5..9d7d723bbfa 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap @@ -1,22 +1,22 @@ -[ +[ { "Message": "Introspection is not allowed for the current request.", "Code": "HC0046", "Path": null, "Locations": [ { - "Line": 3, - "Column": 21 + "Line": 2, + "Column": 5 } ], "Extensions": { "field": { "Kind": "Name", "Location": { - "Start": 39, - "End": 46, - "Line": 3, - "Column": 21 + "Start": 6, + "End": 13, + "Line": 2, + "Column": 5 }, "Value": "__type" }, diff --git a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs index d37834cfaa3..dcec8476f3c 100644 --- a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs +++ b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs @@ -35,7 +35,7 @@ public static IWebHost CreateServer(Action configure, o builder .AddStarWarsTypes() - .RemoveIntrospectionAllowedRule() + .DisableIntrospection(disable: false) .AddStarWarsRepositories() .AddInMemorySubscriptions() .ModifyOptions(