Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added error limits to the document validation. #4655

Merged
merged 8 commits into from
Jan 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public static IRequestExecutorBuilder AddValidationRule<T>(
}

/// <summary>
/// Adds a validation rule that inspects if a GraphQL query document
/// Adds a validation rule that inspects if a GraphQL query document
/// exceeds the maximum allowed operation depth.
/// </summary>
public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
Expand All @@ -155,7 +155,7 @@ public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
}

/// <summary>
/// Adds a validation rule that inspects if a GraphQL query document
/// Adds a validation rule that inspects if a GraphQL query document
/// exceeds the maximum allowed operation depth.
/// </summary>
public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
Expand Down Expand Up @@ -185,6 +185,9 @@ public static IRequestExecutorBuilder AddIntrospectionAllowedRule(
/// <summary>
/// Toggle whether introspection is allow or not.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="allow">
/// If `true` introspection is allowed.
/// If `false` introspection is disallowed, except for requests
Expand All @@ -202,6 +205,36 @@ public static IRequestExecutorBuilder AllowIntrospection(
return builder;
}

/// <summary>
/// Sets the max allowed document validation errors.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="maxAllowedValidationErrors"></param>
/// <returns>
/// Returns an <see cref="IRequestExecutorBuilder"/> that can be used to chain
/// configuration.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="builder"/> is <c>null</c>.
/// </exception>
public static IRequestExecutorBuilder SetMaxAllowedValidationErrors(
this IRequestExecutorBuilder builder,
int maxAllowedValidationErrors)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

ConfigureValidation(
builder,
b => b.ConfigureValidation(
c => c.Modifiers.Add(o => o.MaxAllowedErrors = maxAllowedValidationErrors)));
return builder;
}

private static IRequestExecutorBuilder ConfigureValidation(
IRequestExecutorBuilder builder,
Action<IValidationBuilder> configure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using HotChocolate.Execution.Instrumentation;
using HotChocolate.Validation;
using static HotChocolate.WellKnownContextData;
using static HotChocolate.Execution.ErrorHelper;

namespace HotChocolate.Execution.Pipeline;
Expand Down Expand Up @@ -53,10 +54,7 @@ public async ValueTask InvokeAsync(IRequestContext context)

context.Result = QueryResultBuilder.CreateError(
validationResult.Errors,
new Dictionary<string, object?>
{
{ WellKnownContextData.ValidationErrors, true }
});
new Dictionary<string, object?> { { ValidationErrors, true } });

_diagnosticEvents.ValidationErrors(context, validationResult.Errors);
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using HotChocolate.Execution.Instrumentation;
using HotChocolate.Execution.Processing;
using static HotChocolate.Execution.Pipeline.PipelineTools;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

namespace HotChocolate.Validation;

internal sealed class DefaultDocumentValidatorFactory
: IDocumentValidatorFactory
internal sealed class DefaultDocumentValidatorFactory : IDocumentValidatorFactory
{
private readonly DocumentValidatorContextPool _contextPool;
private readonly IValidationConfiguration _configuration;
Expand All @@ -19,9 +18,7 @@ public DefaultDocumentValidatorFactory(
public IDocumentValidator CreateValidator(NameString schemaName = default)
{
schemaName = schemaName.HasValue ? schemaName : Schema.DefaultName;

return new DocumentValidator(
_contextPool,
_configuration.GetRules(schemaName));
ValidationOptions options = _configuration.GetOptions(schemaName);
return new DocumentValidator(_contextPool, options.Rules, options);
}
}
18 changes: 17 additions & 1 deletion src/HotChocolate/Core/src/Validation/DocumentValidator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using HotChocolate.Language;
using HotChocolate.Validation.Options;

namespace HotChocolate.Validation;

Expand All @@ -13,6 +15,7 @@ public sealed class DocumentValidator : IDocumentValidator
private readonly DocumentValidatorContextPool _contextPool;
private readonly IDocumentValidatorRule[] _allRules;
private readonly IDocumentValidatorRule[] _nonCacheableRules;
private readonly int _maxAllowedErrors;

/// <summary>
/// Creates a new instance of <see cref="DocumentValidator"/>.
Expand All @@ -26,16 +29,23 @@ public sealed class DocumentValidator : IDocumentValidator
/// <exception cref="ArgumentNullException"></exception>
public DocumentValidator(
DocumentValidatorContextPool contextPool,
IEnumerable<IDocumentValidatorRule> rules)
IEnumerable<IDocumentValidatorRule> rules,
IErrorOptionsAccessor errorOptionsAccessor)
{
if (rules is null)
{
throw new ArgumentNullException(nameof(rules));
}

if (errorOptionsAccessor is null)
{
throw new ArgumentNullException(nameof(errorOptionsAccessor));
}

_contextPool = contextPool ?? throw new ArgumentNullException(nameof(contextPool));
_allRules = rules.ToArray();
_nonCacheableRules = _allRules.Where(t => !t.IsCacheable).ToArray();
_maxAllowedErrors = errorOptionsAccessor.MaxAllowedErrors;
}

/// <inheritdoc />
Expand Down Expand Up @@ -85,6 +95,11 @@ public DocumentValidatorResult Validate(
? new DocumentValidatorResult(context.Errors)
: DocumentValidatorResult.Ok;
}
catch (MaxValidationErrorsException)
{
Debug.Assert(context.Errors.Count > 0, "There must be at least 1 validation error.");
return new DocumentValidatorResult(context.Errors);
}
finally
{
_contextPool.Return(context);
Expand All @@ -109,6 +124,7 @@ private void PrepareContext(
}
}

context.MaxAllowedErrors = _maxAllowedErrors;
context.ContextData = contextData;
}
}
18 changes: 16 additions & 2 deletions src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed class DocumentValidatorContext : IDocumentValidatorContext
{
private static readonly FieldInfoListBufferPool _fieldInfoPool = new();
private readonly List<FieldInfoListBuffer> _buffers = new() { new FieldInfoListBuffer() };
private readonly List<IError> _errors = new();

private ISchema? _schema;
private IOutputType? _nonNullString;
Expand Down Expand Up @@ -47,6 +48,8 @@ public IOutputType NonNullString
private set => _nonNullString = value;
}

public int MaxAllowedErrors { get; set; }

public IList<ISyntaxNode> Path { get; } = new List<ISyntaxNode>();

public IList<SelectionSetNode> SelectionSets { get; } = new List<SelectionSetNode>();
Expand Down Expand Up @@ -85,7 +88,7 @@ public IOutputType NonNullString

public IList<IInputField> InputFields { get; } = new List<IInputField>();

public ICollection<IError> Errors { get; } = new List<IError>();
public IReadOnlyCollection<IError> Errors => _errors;

public IList<object?> List { get; } = new List<object?>();

Expand All @@ -111,6 +114,16 @@ public IList<FieldInfo> RentFieldInfoList()
return list;
}

public void ReportError(IError error)
{
if (_errors.Count == MaxAllowedErrors)
{
throw new MaxValidationErrorsException();
}

_errors.Add(error);
}

public void Clear()
{
ClearBuffers();
Expand All @@ -135,11 +148,12 @@ public void Clear()
OutputFields.Clear();
Fields.Clear();
InputFields.Clear();
Errors.Clear();
_errors.Clear();
List.Clear();
UnexpectedErrorsDetected = false;
Count = 0;
Max = 0;
MaxAllowedErrors = 0;
}

private void ClearBuffers()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static IServiceCollection AddValidationCore(
{
services.AddOptions();
services.TryAddSingleton<IValidationConfiguration, ValidationConfiguration>();
services.TryAddSingleton(sp => new DocumentValidatorContextPool(8));
services.TryAddSingleton(_ => new DocumentValidatorContextPool());
services.TryAddSingleton<IDocumentValidatorFactory, DefaultDocumentValidatorFactory>();
return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
/// </summary>
IOutputType NonNullString { get; }

/// <summary>
/// Specifies the max allowed validation errors.
/// </summary>
int MaxAllowedErrors { get;}

/// <summary>
/// The current visitation path of syntax nodes.
/// </summary>
Expand Down Expand Up @@ -110,7 +115,7 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
/// <summary>
/// A list to track validation errors that occurred during the visitation.
/// </summary>
ICollection<IError> Errors { get; }
IReadOnlyCollection<IError> Errors { get; }

/// <summary>
/// Gets ors sets a single counter.
Expand Down Expand Up @@ -144,4 +149,12 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
/// Rents a list of field infos.
/// </summary>
IList<FieldInfo> RentFieldInfoList();

/// <summary>
/// Reports an error.
/// </summary>
/// <param name="error">
/// The validation error that shall be reported.
/// </param>
void ReportError(IError error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Runtime.Serialization;

namespace HotChocolate.Validation;

/// <summary>
/// The exception is thrown whenever the max validation error is exceeded.
/// </summary>
[Serializable]
public class MaxValidationErrorsException : Exception
{
public MaxValidationErrorsException() { }

public MaxValidationErrorsException(string message)
: base(message) { }

public MaxValidationErrorsException(string message, Exception inner)
: base(message, inner) { }

protected MaxValidationErrorsException(
SerializationInfo info,
StreamingContext context)
: base(info, context) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace HotChocolate.Validation.Options;

/// <summary>
/// Represents the validation error options.
/// </summary>
public interface IErrorOptionsAccessor
{
/// <summary>
/// Specifies how many errors are allowed before the validation is aborted.
/// </summary>
int MaxAllowedErrors { get; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace HotChocolate.Validation.Options;

/// <summary>
/// Represents the options for the max execution depth analyzer.
/// </summary>
public interface IMaxExecutionDepthOptionsAccessor
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ namespace HotChocolate.Validation.Options;
public interface IValidationConfiguration
{
IEnumerable<IDocumentValidatorRule> GetRules(string schemaName);

ValidationOptions GetOptions(string schemaName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@

namespace HotChocolate.Validation.Options;

public class ValidationConfiguration
: IValidationConfiguration
public class ValidationConfiguration : IValidationConfiguration
{
private readonly ConcurrentDictionary<string, ValidationOptions> _optionsCache =
new ConcurrentDictionary<string, ValidationOptions>();
private readonly ConcurrentDictionary<string, ValidationOptions> _optionsCache = new();
private readonly IOptionsMonitor<ValidationOptionsModifiers> _optionsMonitor;

public ValidationConfiguration(IOptionsMonitor<ValidationOptionsModifiers> optionsMonitor)
Expand All @@ -19,17 +17,17 @@ public ValidationConfiguration(IOptionsMonitor<ValidationOptionsModifiers> optio
}

public IEnumerable<IDocumentValidatorRule> GetRules(string schemaName)
{
ValidationOptions options = _optionsCache.GetOrAdd(schemaName, CreateOptions);
return options.Rules;
}
=> GetOptions(schemaName).Rules;

public ValidationOptions GetOptions(string schemaName)
=> _optionsCache.GetOrAdd(schemaName, CreateOptions);

private ValidationOptions CreateOptions(string schemaName)
{
ValidationOptionsModifiers modifiers = _optionsMonitor.Get(schemaName);
var options = new ValidationOptions();

for (int i = 0; i < modifiers.Modifiers.Count; i++)
for (var i = 0; i < modifiers.Modifiers.Count; i++)
{
modifiers.Modifiers[i](options);
}
Expand Down
Loading