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

Best practices for regular expressions versus Regex performance review #1348

Merged
merged 20 commits into from
Nov 16, 2024
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
120 changes: 65 additions & 55 deletions src/Ocelot/Authorization/ClaimsAuthorizer.cs
Original file line number Diff line number Diff line change
@@ -1,87 +1,97 @@
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Infrastructure;
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Security.Claims;

namespace Ocelot.Authorization
namespace Ocelot.Authorization;

/// <summary>
/// Default authorizer by claims.
/// </summary>
public partial class ClaimsAuthorizer : IClaimsAuthorizer
{
public class ClaimsAuthorizer : IClaimsAuthorizer
private readonly IClaimsParser _claimsParser;

public ClaimsAuthorizer(IClaimsParser claimsParser)
{
private readonly IClaimsParser _claimsParser;
_claimsParser = claimsParser;
}

public ClaimsAuthorizer(IClaimsParser claimsParser)
#if NET7_0_OR_GREATER
[GeneratedRegex(@"^{(?<variable>.+)}$", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
private static partial Regex RegexAuthorize();
#else
private static readonly Regex _regexAuthorize = RegexGlobal.New(@"^{(?<variable>.+)}$");
private static Regex RegexAuthorize() => _regexAuthorize;
#endif
public Response<bool> Authorize(
ClaimsPrincipal claimsPrincipal,
Dictionary<string, string> routeClaimsRequirement,
List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues
)
{
foreach (var required in routeClaimsRequirement)
{
_claimsParser = claimsParser;
}
var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);

public Response<bool> Authorize(
ClaimsPrincipal claimsPrincipal,
Dictionary<string, string> routeClaimsRequirement,
List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues
)
{
foreach (var required in routeClaimsRequirement)
if (values.IsError)
{
var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key);
return new ErrorResponse<bool>(values.Errors);
}

if (values.IsError)
if (values.Data != null)
{
// dynamic claim
var match = RegexAuthorize().Match(required.Value);
if (match.Success)
{
return new ErrorResponse<bool>(values.Errors);
}
var variableName = match.Captures[0].Value;

if (values.Data != null)
{
// dynamic claim
var match = Regex.Match(required.Value, @"^{(?<variable>.+)}$");
if (match.Success)
var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray();
if (matchingPlaceholders.Length == 1)
{
var variableName = match.Captures[0].Value;

var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray();
if (matchingPlaceholders.Length == 1)
{
// match
var actualValue = matchingPlaceholders[0].Value;
var authorized = values.Data.Contains(actualValue);
if (!authorized)
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}"));
}
}
else
// match
var actualValue = matchingPlaceholders[0].Value;
var authorized = values.Data.Contains(actualValue);
if (!authorized)
{
// config error
if (matchingPlaceholders.Length == 0)
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}"));
}
else
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}"));
}
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}"));
}
}
else
{
// static claim
var authorized = values.Data.Contains(required.Value);
if (!authorized)
// config error
if (matchingPlaceholders.Length == 0)
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}"));
}
else
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}"));
$"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}"));
}
}
}
else
{
return new ErrorResponse<bool>(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}"));
// static claim
var authorized = values.Data.Contains(required.Value);
if (!authorized)
{
return new ErrorResponse<bool>(new ClaimValueNotAuthorizedError(
$"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}"));
}
}
}

return new OkResponse<bool>(true);
else
{
return new ErrorResponse<bool>(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}"));
}
}

return new OkResponse<bool>(true);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Ocelot.Configuration.File;
using Ocelot.Infrastructure;
using Ocelot.Values;

namespace Ocelot.Configuration.Creator;
Expand All @@ -11,11 +12,11 @@ public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTempl
{
private const string PlaceHolderPattern = @"(\{header:.*?\})";
#if NET7_0_OR_GREATER
[GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
private static partial Regex RegExPlaceholders();
[GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
private static partial Regex RegexPlaceholders();
#else
private static readonly Regex RegExPlaceholdersVar = new(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
private static Regex RegExPlaceholders() => RegExPlaceholdersVar;
private static readonly Regex _regexPlaceholders = RegexGlobal.New(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static Regex RegexPlaceholders() => _regexPlaceholders;
#endif

public IDictionary<string, UpstreamHeaderTemplate> Create(IRoute route)
Expand All @@ -25,7 +26,7 @@ public IDictionary<string, UpstreamHeaderTemplate> Create(IRoute route)
foreach (var headerTemplate in route.UpstreamHeaderTemplates)
{
var headerTemplateValue = headerTemplate.Value;
var matches = RegExPlaceholders().Matches(headerTemplateValue);
var matches = RegexPlaceholders().Matches(headerTemplateValue);

if (matches.Count > 0)
{
Expand Down
96 changes: 54 additions & 42 deletions src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,69 @@
using Ocelot.Responses;
using Ocelot.Infrastructure;
using Ocelot.Responses;

namespace Ocelot.Configuration.Parser
namespace Ocelot.Configuration.Parser;

/// <summary>
/// Default implementation of the <see cref="IClaimToThingConfigurationParser"/> interface.
/// </summary>
public partial class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser
{
public class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser
{
private readonly Regex _claimRegex = new("Claims\\[.*\\]");
private readonly Regex _indexRegex = new("value\\[.*\\]");
private const char SplitToken = '>';
private const char SplitToken = '>';
#if NET7_0_OR_GREATER
[GeneratedRegex("Claims\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
private static partial Regex ClaimRegex();
[GeneratedRegex("value\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)]
private static partial Regex IndexRegex();
#else
private static readonly Regex _claimRegex = RegexGlobal.New("Claims\\[.*\\]");
private static readonly Regex _indexRegex = RegexGlobal.New("value\\[.*\\]");
private static Regex ClaimRegex() => _claimRegex;
private static Regex IndexRegex() => _indexRegex;
#endif

public Response<ClaimToThing> Extract(string existingKey, string value)
public Response<ClaimToThing> Extract(string existingKey, string value)
{
try
{
try
{
var instructions = value.Split(SplitToken);
var instructions = value.Split(SplitToken);

if (instructions.Length <= 1)
{
return new ErrorResponse<ClaimToThing>(new NoInstructionsError(SplitToken.ToString()));
}

var claimMatch = _claimRegex.IsMatch(instructions[0]);
if (instructions.Length <= 1)
{
return new ErrorResponse<ClaimToThing>(new NoInstructionsError(SplitToken.ToString()));
}

if (!claimMatch)
{
return new ErrorResponse<ClaimToThing>(new InstructionNotForClaimsError());
}
var claimMatch = ClaimRegex().IsMatch(instructions[0]);

var newKey = GetIndexValue(instructions[0]);
var index = 0;
var delimiter = string.Empty;
if (!claimMatch)
{
return new ErrorResponse<ClaimToThing>(new InstructionNotForClaimsError());
}

if (instructions.Length > 2 && _indexRegex.IsMatch(instructions[1]))
{
index = int.Parse(GetIndexValue(instructions[1]));
delimiter = instructions[2].Trim();
}
var newKey = GetIndexValue(instructions[0]);
var index = 0;
var delimiter = string.Empty;

return new OkResponse<ClaimToThing>(
new ClaimToThing(existingKey, newKey, delimiter, index));
}
catch (Exception exception)
if (instructions.Length > 2 && IndexRegex().IsMatch(instructions[1]))
{
return new ErrorResponse<ClaimToThing>(new ParsingConfigurationHeaderError(exception));
index = int.Parse(GetIndexValue(instructions[1]));
delimiter = instructions[2].Trim();
}
}

private static string GetIndexValue(string instruction)
return new OkResponse<ClaimToThing>(
new ClaimToThing(existingKey, newKey, delimiter, index));
}
catch (Exception exception)
{
var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal);
var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal);
var length = lastIndexer - firstIndexer;
var claimKey = instruction.Substring(firstIndexer + 1, length - 1);
return claimKey;
return new ErrorResponse<ClaimToThing>(new ParsingConfigurationHeaderError(exception));
}
}
}

private static string GetIndexValue(string instruction)
{
var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal);
var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal);
var length = lastIndexer - firstIndexer;
var claimKey = instruction.Substring(firstIndexer + 1, length - 1);
return claimKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration.File;
using Ocelot.Errors;
using Ocelot.Infrastructure;
using Ocelot.Responses;
using Ocelot.ServiceDiscovery;

namespace Ocelot.Configuration.Validator
{
/// <summary>
/// Validation of a <see cref="FileConfiguration"/> objects.
/// </summary>
/// <summary>Validation of a <see cref="FileConfiguration"/> objects.</summary>
public partial class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{
private const string Servicefabric = "servicefabric";
Expand Down Expand Up @@ -101,11 +100,11 @@ private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateR
}

#if NET7_0_OR_GREATER
[GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")]
[GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
private static partial Regex PlaceholderRegex();
#else
private static readonly Regex PlaceholderRegexVar = new(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000));
private static Regex PlaceholderRegex() => PlaceholderRegexVar;
private static readonly Regex _placeholderRegex = RegexGlobal.New(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static Regex PlaceholderRegex() => _placeholderRegex;
#endif

private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate)
Expand Down
Loading