#568 worked out how to check if qos handler present and kill Ocelot i… (#578)

* #568 worked out how to check if qos handler present and kill Ocelot if options specified but no handler, need to refactor this into fluent validation style

* #568 acceptance tests to make sure Ocelot won't start if the user specifies QoSOptions but doesnt have a QoSHandler registered
This commit is contained in:
Tom Pallister
2018-08-28 18:57:21 +01:00
committed by GitHub
parent 8db5570840
commit 29a7af9486
7 changed files with 1694 additions and 1377 deletions

View File

@ -1,24 +1,30 @@
using System.Collections.Generic;
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class ConfigurationValidationResult
{
public ConfigurationValidationResult(bool isError)
{
IsError = isError;
Errors = new List<Error>();
}
public ConfigurationValidationResult(bool isError, List<Error> errors)
{
IsError = isError;
Errors = errors;
}
public bool IsError { get; }
public List<Error> Errors { get; }
}
}
using System.Collections.Generic;
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class ConfigurationValidationResult
{
public ConfigurationValidationResult(bool isError)
{
IsError = isError;
Errors = new List<Error>();
}
public ConfigurationValidationResult(bool isError, Error error)
{
IsError = isError;
Errors = new List<Error> { error };
}
public ConfigurationValidationResult(bool isError, List<Error> errors)
{
IsError = isError;
Errors = errors;
}
public bool IsError { get; }
public List<Error> Errors { get; }
}
}

View File

@ -1,129 +1,133 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Errors;
using Ocelot.Responses;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration.Validator
{
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{
private readonly IServiceProvider _provider;
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
{
_provider = provider;
RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, provider));
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate");
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.Aggregates))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => IsNotDuplicateIn(aggregateReRoute, config.Aggregates))
.WithMessage((config, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => AllReRoutesForAggregateExist(aggregateReRoute, config.ReRoutes))
.WithMessage((config, aggregateReRoute) => $"ReRoutes for {nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => DoesNotContainReRoutesWithSpecificRequestIdKeys(aggregateReRoute, config.ReRoutes))
.WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates");
}
private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count;
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)
{
var validateResult = await ValidateAsync(configuration);
if (validateResult.IsValid)
{
return new OkResponse<ConfigurationValidationResult>(new ConfigurationValidationResult(false));
}
var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage));
var result = new ConfigurationValidationResult(true, errors.Cast<Error>().ToList());
return new OkResponse<ConfigurationValidationResult>(result);
}
private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute,
List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey));
}
private static bool IsNotDuplicateIn(FileReRoute reRoute,
List<FileReRoute> reRoutes)
{
var matchingReRoutes = reRoutes
.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null))
.ToList();
if(matchingReRoutes.Count == 1)
{
return true;
}
var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0);
var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1;
var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0);
var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs))
{
return false;
}
return true;
}
private static bool IsNotDuplicateIn(FileReRoute reRoute,
List<FileAggregateReRoute> aggregateReRoutes)
{
var duplicate = aggregateReRoutes
.Any(a => a.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& a.UpstreamHost == reRoute.UpstreamHost
&& reRoute.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get"));
return !duplicate;
}
private static bool IsNotDuplicateIn(FileAggregateReRoute reRoute,
List<FileAggregateReRoute> aggregateReRoutes)
{
var matchingReRoutes = aggregateReRoutes
.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& r.UpstreamHost == reRoute.UpstreamHost)
.ToList();
return matchingReRoutes.Count <= 1;
}
}
}
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Errors;
using Ocelot.Responses;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration.Validator
{
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
{
_qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();
RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate));
RuleFor(configuration => configuration.GlobalConfiguration)
.SetValidator(new FileGlobalConfigurationFluentValidator(_qosDelegatingHandlerDelegate));
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate");
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.Aggregates))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => IsNotDuplicateIn(aggregateReRoute, config.Aggregates))
.WithMessage((config, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => AllReRoutesForAggregateExist(aggregateReRoute, config.ReRoutes))
.WithMessage((config, aggregateReRoute) => $"ReRoutes for {nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property");
RuleForEach(configuration => configuration.Aggregates)
.Must((config, aggregateReRoute) => DoesNotContainReRoutesWithSpecificRequestIdKeys(aggregateReRoute, config.ReRoutes))
.WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates");
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)
{
var validateResult = await ValidateAsync(configuration);
if (validateResult.IsValid)
{
return new OkResponse<ConfigurationValidationResult>(new ConfigurationValidationResult(false));
}
var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage));
var result = new ConfigurationValidationResult(true, errors.Cast<Error>().ToList());
return new OkResponse<ConfigurationValidationResult>(result);
}
private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count;
}
private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute,
List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey));
}
private static bool IsNotDuplicateIn(FileReRoute reRoute,
List<FileReRoute> reRoutes)
{
var matchingReRoutes = reRoutes
.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null))
.ToList();
if (matchingReRoutes.Count == 1)
{
return true;
}
var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0);
var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1;
var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0);
var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs))
{
return false;
}
return true;
}
private static bool IsNotDuplicateIn(FileReRoute reRoute,
List<FileAggregateReRoute> aggregateReRoutes)
{
var duplicate = aggregateReRoutes
.Any(a => a.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& a.UpstreamHost == reRoute.UpstreamHost
&& reRoute.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get"));
return !duplicate;
}
private static bool IsNotDuplicateIn(FileAggregateReRoute reRoute,
List<FileAggregateReRoute> aggregateReRoutes)
{
var matchingReRoutes = aggregateReRoutes
.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate
&& r.UpstreamHost == reRoute.UpstreamHost)
.ToList();
return matchingReRoutes.Count <= 1;
}
}
}

View File

@ -0,0 +1,21 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
using Requester;
public class FileGlobalConfigurationFluentValidator : AbstractValidator<FileGlobalConfiguration>
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileGlobalConfigurationFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(configuration => configuration.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate));
}
}
}

View File

@ -0,0 +1,28 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
using Requester;
public class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions>
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileQoSOptionsFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => {
RuleFor(qosOptions => qosOptions)
.Must(HaveQosHandlerRegistered)
.WithMessage("Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?");
});
}
private bool HaveQosHandlerRegistered(FileQoSOptions arg)
{
return _qosDelegatingHandlerDelegate != null;
}
}
}

View File

@ -1,92 +1,95 @@
namespace Ocelot.Configuration.Validator
{
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly IServiceProvider _serviceProvider;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider serviceProvider)
{
_authenticationSchemeProvider = authenticationSchemeProvider;
_serviceProvider = serviceProvider;
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.RateLimitOptions)
.Must(IsValidPeriod)
.WithMessage("RateLimitOptions.Period does not contains (s,m,h,d)");
RuleFor(reRoute => reRoute.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider");
When(reRoute => reRoute.UseServiceDiscovery, () => {
RuleFor(r => r.ServiceName).NotEmpty()
.WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!");
});
When(reRoute => !reRoute.UseServiceDiscovery, () => {
RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
.WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!");
});
When(reRoute => !reRoute.UseServiceDiscovery, () => {
RuleFor(reRoute => reRoute.DownstreamHostAndPorts)
.SetCollectionValidator(new HostAndPortValidator());
});
}
private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey))
{
return true;
}
var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey);
}
private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
{
string period = rateLimitOptions.Period;
return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d");
}
}
}
namespace Ocelot.Configuration.Validator
{
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{
_authenticationSchemeProvider = authenticationSchemeProvider;
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(reRoute => reRoute.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate));
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.RateLimitOptions)
.Must(IsValidPeriod)
.WithMessage("RateLimitOptions.Period does not contains (s,m,h,d)");
RuleFor(reRoute => reRoute.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider");
When(reRoute => reRoute.UseServiceDiscovery, () => {
RuleFor(r => r.ServiceName).NotEmpty()
.WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!");
});
When(reRoute => !reRoute.UseServiceDiscovery, () => {
RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
.WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!");
});
When(reRoute => !reRoute.UseServiceDiscovery, () => {
RuleFor(reRoute => reRoute.DownstreamHostAndPorts)
.SetCollectionValidator(new HostAndPortValidator());
});
}
private async Task<bool> IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey))
{
return true;
}
var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey);
}
private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
{
string period = rateLimitOptions.Period;
return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d");
}
}
}