add file configuration fluent validation and change default configura… (#168)

* add file configuration fluent validation and change default configuration validator to fluent validator

* add file validation failed error code

* change authentication schemes check to async

* beautify the code ^_^

* clean file validation and fix test failure.
This commit is contained in:
Eilyyyy 2017-12-05 12:29:44 -06:00 committed by Tom Pallister
parent 31fe6af614
commit 4f27a50503
18 changed files with 160 additions and 298 deletions

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text;
namespace Ocelot.Configuration.File
{
@ -11,5 +12,14 @@ namespace Ocelot.Configuration.File
public string AuthenticationProviderKey {get; set;}
public List<string> AllowedScopes { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"{nameof(AuthenticationProviderKey)}:{AuthenticationProviderKey},{nameof(AllowedScopes)}:[");
sb.AppendJoin(',', AllowedScopes);
sb.Append("]");
return sb.ToString();
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ocelot.Configuration.File
@ -30,5 +31,20 @@ namespace Ocelot.Configuration.File
/// Maximum number of requests that a client can make in a defined period
/// </summary>
public long Limit { get; set; }
public override string ToString()
{
if (!EnableRateLimiting)
{
return string.Empty;
}
var sb = new StringBuilder();
sb.Append(
$"{nameof(Period)}:{Period},{nameof(PeriodTimespan)}:{PeriodTimespan:F},{nameof(Limit)}:{Limit},{nameof(ClientWhitelist)}:[");
sb.AppendJoin(',', ClientWhitelist);
sb.Append(']');
return sb.ToString();
}
}
}

View File

@ -1,11 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateAlreadyUsedError : Error
{
public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)
{
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateContainsSchemeError : Error
{
public DownstreamPathTemplateContainsSchemeError(string message)
: base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)
{
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class PathTemplateDoesntStartWithForwardSlash : Error
{
public PathTemplateDoesntStartWithForwardSlash(string message)
: base(message, OcelotErrorCode.PathTemplateDoesntStartWithForwardSlash)
{
}
}
}

View File

@ -0,0 +1,50 @@
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
{
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)
{
RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider));
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"duplicate downstreampath {reRoute.UpstreamPathTemplate}");
}
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 IsNotDuplicateIn(FileReRoute reRoute, List<FileReRoute> routes)
{
var reRoutesWithUpstreamPathTemplate = routes.Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate).ToList();
var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count == 0);
var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Count(x => x.UpstreamHttpMethod.Count == 0) > 1;
var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Any(x => x.UpstreamHttpMethod.Count != 0);
var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs))
{
return false;
}
return true;
}
}
}

View File

@ -1,223 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Errors;
using Ocelot.Responses;
namespace Ocelot.Configuration.Validator
{
public class FileConfigurationValidator : IConfigurationValidator
{
private readonly IAuthenticationSchemeProvider _provider;
public FileConfigurationValidator(IAuthenticationSchemeProvider provider)
{
_provider = provider;
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)
{
var result = CheckForDuplicateReRoutes(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
result = CheckDownstreamTemplatePathBeingsWithForwardSlash(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
result = CheckUpstreamTemplatePathBeingsWithForwardSlash(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
result = await CheckForUnsupportedAuthenticationProviders(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
result = CheckForReRoutesRateLimitOptions(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
return new OkResponse<ConfigurationValidationResult>(result);
}
private ConfigurationValidationResult CheckDownstreamTemplatePathBeingsWithForwardSlash(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach(var reRoute in configuration.ReRoutes)
{
if(!reRoute.DownstreamPathTemplate.StartsWith("/"))
{
errors.Add(new PathTemplateDoesntStartWithForwardSlash($"{reRoute.DownstreamPathTemplate} doesnt start with forward slash"));
}
}
if(errors.Any())
{
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(false, errors);
}
private ConfigurationValidationResult CheckUpstreamTemplatePathBeingsWithForwardSlash(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach(var reRoute in configuration.ReRoutes)
{
if(!reRoute.UpstreamPathTemplate.StartsWith("/"))
{
errors.Add(new PathTemplateDoesntStartWithForwardSlash($"{reRoute.DownstreamPathTemplate} doesnt start with forward slash"));
}
}
if(errors.Any())
{
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(false, errors);
}
private async Task<ConfigurationValidationResult> CheckForUnsupportedAuthenticationProviders(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach (var reRoute in configuration.ReRoutes)
{
var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions.AuthenticationProviderKey);
if (!isAuthenticated)
{
continue;
}
var data = await _provider.GetAllSchemesAsync();
var schemes = data.ToList();
if (schemes.Any(x => x.Name == reRoute.AuthenticationOptions.AuthenticationProviderKey))
{
continue;
}
var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions.AuthenticationProviderKey} is unsupported authentication provider, upstream template is {reRoute.UpstreamPathTemplate}, upstream method is {reRoute.UpstreamHttpMethod}");
errors.Add(error);
}
return errors.Count > 0
? new ConfigurationValidationResult(true, errors)
: new ConfigurationValidationResult(false);
}
private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach(var reRoute in configuration.ReRoutes)
{
if(reRoute.DownstreamPathTemplate.Contains("https://")
|| reRoute.DownstreamPathTemplate.Contains("http://"))
{
errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme"));
}
}
if(errors.Any())
{
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(false, errors);
}
private ConfigurationValidationResult CheckForDuplicateReRoutes(FileConfiguration configuration)
{
var duplicatedUpstreamPathTemplates = new List<string>();
var distinctUpstreamPathTemplates = configuration.ReRoutes.Select(x => x.UpstreamPathTemplate).Distinct();
foreach (string upstreamPathTemplate in distinctUpstreamPathTemplates)
{
var reRoutesWithUpstreamPathTemplate = configuration.ReRoutes.Where(x => x.UpstreamPathTemplate == upstreamPathTemplate);
var hasEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Any();
var hasDuplicateEmptyListToAllowAllHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() == 0).Count() > 1;
var hasSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.Where(x => x.UpstreamHttpMethod.Count() > 0).Any();
var hasDuplicateSpecificHttpVerbs = reRoutesWithUpstreamPathTemplate.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any();
if (hasDuplicateEmptyListToAllowAllHttpVerbs || hasDuplicateSpecificHttpVerbs || (hasEmptyListToAllowAllHttpVerbs && hasSpecificHttpVerbs))
{
duplicatedUpstreamPathTemplates.Add(upstreamPathTemplate);
}
}
if (duplicatedUpstreamPathTemplates.Count() == 0)
{
return new ConfigurationValidationResult(false);
}
else
{
var errors = duplicatedUpstreamPathTemplates
.Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d)))
.Cast<Error>()
.ToList();
return new ConfigurationValidationResult(true, errors);
}
}
private ConfigurationValidationResult CheckForReRoutesRateLimitOptions(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach (var reRoute in configuration.ReRoutes)
{
if (reRoute.RateLimitOptions.EnableRateLimiting)
{
if (!IsValidPeriod(reRoute))
{
errors.Add(new RateLimitOptionsValidationError($"{reRoute.RateLimitOptions.Period} not contains scheme"));
}
}
}
if (errors.Any())
{
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(false, errors);
}
private static bool IsValidPeriod(FileReRoute reRoute)
{
string period = reRoute.RateLimitOptions.Period;
return period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d");
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class FileValidationFailedError : Error
{
public FileValidationFailedError(string message) : base(message, OcelotErrorCode.FileValidationFailedError)
{
}
}
}

View File

@ -1,15 +0,0 @@
using Ocelot.Errors;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ocelot.Configuration.Validator
{
public class RateLimitOptionsValidationError : Error
{
public RateLimitOptionsValidationError(string message)
: base(message, OcelotErrorCode.RateLimitOptionsError)
{
}
}
}

View File

@ -0,0 +1,54 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ocelot.Configuration.Validator
{
public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider)
{
_authenticationSchemeProvider = authenticationSchemeProvider;
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("downstream path {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("upstream path {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("downstream path {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.RateLimitOptions)
.Must(IsValidPeriod)
.WithMessage("rate limit period {PropertyValue} not contains (s,m,h,d)");
RuleFor(reRoute => reRoute.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider");
}
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");
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class UnsupportedAuthenticationProviderError : Error
{
public UnsupportedAuthenticationProviderError(string message)
: base(message, OcelotErrorCode.UnsupportedAuthenticationProviderError)
{
}
}
}

View File

@ -72,7 +72,7 @@ namespace Ocelot.DependencyInjection
_services.Configure<FileConfiguration>(configurationRoot);
_services.TryAddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
_services.TryAddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
_services.TryAddSingleton<IConfigurationValidator, FileConfigurationValidator>();
_services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>();
_services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();
_services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();
_services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();

View File

@ -32,6 +32,7 @@
UnableToSetConfigInConsulError,
UnmappableRequestError,
RateLimitOptionsError,
PathTemplateDoesntStartWithForwardSlash
PathTemplateDoesntStartWithForwardSlash,
FileValidationFailedError
}
}

View File

@ -26,6 +26,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="7.2.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />

View File

@ -15,17 +15,17 @@ using Xunit;
namespace Ocelot.UnitTests.Configuration
{
public class ConfigurationValidationTests
public class ConfigurationFluentValidationTests
{
private readonly IConfigurationValidator _configurationValidator;
private FileConfiguration _fileConfiguration;
private Response<ConfigurationValidationResult> _result;
private Mock<IAuthenticationSchemeProvider> _provider;
public ConfigurationValidationTests()
public ConfigurationFluentValidationTests()
{
_provider = new Mock<IAuthenticationSchemeProvider>();
_configurationValidator = new FileConfigurationValidator(_provider.Object);
_configurationValidator = new FileConfigurationFluentValidator(_provider.Object);
}
[Fact]
@ -44,6 +44,7 @@ namespace Ocelot.UnitTests.Configuration
}))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.Then(x => x.ThenTheErrorIs<FileValidationFailedError>())
.BDDfy();
}
@ -147,7 +148,6 @@ namespace Ocelot.UnitTests.Configuration
}))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<UnsupportedAuthenticationProviderError>())
.BDDfy();
}
@ -172,10 +172,10 @@ namespace Ocelot.UnitTests.Configuration
}))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<DownstreamPathTemplateAlreadyUsedError>())
.BDDfy();
}
private void GivenAConfiguration(FileConfiguration fileConfiguration)
{
_fileConfiguration = fileConfiguration;
@ -225,6 +225,5 @@ namespace Ocelot.UnitTests.Configuration
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
}
}
}
}

View File

@ -503,7 +503,7 @@ namespace Ocelot.UnitTests.Configuration
[Fact]
public void should_return_validation_errors()
{
var errors = new List<Error> {new PathTemplateDoesntStartWithForwardSlash("some message")};
var errors = new List<Error> {new FileValidationFailedError("some message")};
this.Given(x => x.GivenTheConfigIs(new FileConfiguration()))
.And(x => x.GivenTheConfigIsInvalid(errors))

View File

@ -54,6 +54,7 @@ namespace Ocelot.UnitTests.Responder
[InlineData(OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)]
[InlineData(OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)]
[InlineData(OcelotErrorCode.DownstreamSchemeNullOrEmptyError)]
[InlineData(OcelotErrorCode.FileValidationFailedError)]
[InlineData(OcelotErrorCode.InstructionNotForClaimsError)]
[InlineData(OcelotErrorCode.NoInstructionsError)]
[InlineData(OcelotErrorCode.ParsingConfigurationHeaderError)]
@ -120,7 +121,7 @@ namespace Ocelot.UnitTests.Responder
// If this test fails then it's because the number of error codes has changed.
// You should make the appropriate changes to the test cases here to ensure
// they cover all the error codes, and then modify this assertion.
Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(31, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?");
Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(32, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?");
}
private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)