Added ability to strip claims and forward to downstream service as headers

This commit is contained in:
TomPallister
2016-10-18 15:51:56 +01:00
parent 279aae3151
commit 84256e7bac
32 changed files with 1467 additions and 94 deletions

View File

@ -1,4 +1,6 @@
namespace Ocelot.Library.Builder
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Builder
{
using System.Collections.Generic;
using Configuration;
@ -16,6 +18,7 @@
private List<string> _additionalScopes;
private bool _requireHttps;
private string _scopeSecret;
private List<ConfigurationHeaderExtractorProperties> _configHeaderExtractorProperties;
public ReRouteBuilder()
{
@ -86,9 +89,15 @@
return this;
}
public ReRouteBuilder WithConfigurationHeaderExtractorProperties(List<ConfigurationHeaderExtractorProperties> input)
{
_configHeaderExtractorProperties = input;
return this;
}
public ReRoute Build()
{
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret));
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties);
}
}
}

View File

@ -1,3 +1,8 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Configuration
{
using System.Collections.Generic;
@ -11,11 +16,18 @@ namespace Ocelot.Library.Configuration
private readonly List<ReRoute> _reRoutes;
private const string RegExMatchEverything = ".*";
private const string RegExMatchEndString = "$";
private readonly IConfigurationHeaderExtrator _configurationHeaderExtrator;
private readonly ILogger<OcelotConfiguration> _logger;
public OcelotConfiguration(IOptions<YamlConfiguration> options, IConfigurationValidator configurationValidator)
public OcelotConfiguration(IOptions<YamlConfiguration> options,
IConfigurationValidator configurationValidator,
IConfigurationHeaderExtrator configurationHeaderExtrator,
ILogger<OcelotConfiguration> logger)
{
_options = options;
_configurationValidator = configurationValidator;
_configurationHeaderExtrator = configurationHeaderExtrator;
_logger = logger;
_reRoutes = new List<ReRoute>();
SetUpConfiguration();
}
@ -43,7 +55,7 @@ namespace Ocelot.Library.Configuration
var placeholders = new List<string>();
for (int i = 0; i < upstreamTemplate.Length; i++)
for (var i = 0; i < upstreamTemplate.Length; i++)
{
if (IsPlaceHolder(upstreamTemplate, i))
{
@ -70,17 +82,41 @@ namespace Ocelot.Library.Configuration
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
reRoute.AuthenticationOptions.ScopeSecret);
var configHeaders = GetHeadersToAddToRequest(reRoute);
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, authOptionsForRoute
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, configHeaders
));
}
else
{
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod,
upstreamTemplate, isAuthenticated, null));
upstreamTemplate, isAuthenticated, null, new List<ConfigurationHeaderExtractorProperties>()));
}
}
private List<ConfigurationHeaderExtractorProperties> GetHeadersToAddToRequest(YamlReRoute reRoute)
{
var configHeaders = new List<ConfigurationHeaderExtractorProperties>();
foreach (var add in reRoute.AddHeadersToRequest)
{
var configurationHeader = _configurationHeaderExtrator.Extract(add.Key, add.Value);
if (configurationHeader.IsError)
{
_logger.LogCritical(new EventId(1, "Application Failed to start"),
$"Unable to extract configuration for key: {add.Key} and value: {add.Value} your configuration file is incorrect");
throw new Exception(configurationHeader.Errors[0].Message);
}
configHeaders.Add(configurationHeader.Data);
}
return configHeaders;
}
private bool IsPlaceHolder(string upstreamTemplate, int i)
{
return upstreamTemplate[i] == '{';

View File

@ -1,8 +1,11 @@
namespace Ocelot.Library.Configuration
using System.Collections.Generic;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Configuration
{
public class ReRoute
{
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions)
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties)
{
DownstreamTemplate = downstreamTemplate;
UpstreamTemplate = upstreamTemplate;
@ -10,6 +13,8 @@
UpstreamTemplatePattern = upstreamTemplatePattern;
IsAuthenticated = isAuthenticated;
AuthenticationOptions = authenticationOptions;
ConfigurationHeaderExtractorProperties = configurationHeaderExtractorProperties
?? new List<ConfigurationHeaderExtractorProperties>();
}
public string DownstreamTemplate { get; private set; }
@ -18,5 +23,6 @@
public string UpstreamHttpMethod { get; private set; }
public bool IsAuthenticated { get; private set; }
public AuthenticationOptions AuthenticationOptions { get; private set; }
public List<ConfigurationHeaderExtractorProperties> ConfigurationHeaderExtractorProperties { get; private set; }
}
}

View File

@ -13,6 +13,6 @@
public string UpstreamTemplate { get; set; }
public string UpstreamHttpMethod { get; set; }
public YamlAuthenticationOptions AuthenticationOptions { get; set; }
public Dictionary<string,string> AddHeadersToRequest { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; }
}
}

View File

@ -21,6 +21,9 @@
services.Configure<YamlConfiguration>(configurationRoot);
// Add framework services.
services.AddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
services.AddSingleton<IClaimsParser, ClaimsParser>();
services.AddSingleton<IConfigurationHeaderExtrator, ConfigurationHeaderExtrator>();
services.AddSingleton<IConfigurationValidator, ConfigurationValidator>();
services.AddSingleton<IOcelotConfiguration, OcelotConfiguration>();
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();

View File

@ -10,6 +10,10 @@
CannotFindDataError,
UnableToCompleteRequestError,
UnableToCreateAuthenticationHandlerError,
UnsupportedAuthenticationProviderError
UnsupportedAuthenticationProviderError,
CannotFindClaimError,
ParsingConfigurationHeaderError,
NoInstructionsError,
InstructionNotForClaimsError
}
}

View File

@ -1,23 +0,0 @@
namespace Ocelot.Library.Middleware
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Repository;
public class ClaimsParserMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
public ClaimsParserMiddleware(RequestDelegate next, IScopedRequestDataRepository scopedRequestDataRepository)
: base(scopedRequestDataRepository)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
}
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Ocelot.Library.DownstreamRouteFinder;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Middleware
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Repository;
public class HttpRequestHeadersBuilderMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IAddHeadersToRequest _addHeadersToRequest;
private readonly IScopedRequestDataRepository _scopedRequestDataRepository;
public HttpRequestHeadersBuilderMiddleware(RequestDelegate next,
IScopedRequestDataRepository scopedRequestDataRepository,
IAddHeadersToRequest addHeadersToRequest)
: base(scopedRequestDataRepository)
{
_next = next;
_addHeadersToRequest = addHeadersToRequest;
_scopedRequestDataRepository = scopedRequestDataRepository;
}
public async Task Invoke(HttpContext context)
{
var downstreamRoute = _scopedRequestDataRepository.Get<DownstreamRoute>("DownstreamRoute");
if (downstreamRoute.Data.ReRoute.ConfigurationHeaderExtractorProperties.Any())
{
_addHeadersToRequest.SetHeadersOnContext(downstreamRoute.Data.ReRoute.ConfigurationHeaderExtractorProperties, context);
}
await _next.Invoke(context);
}
}
}

View File

@ -0,0 +1,12 @@
namespace Ocelot.Library.Middleware
{
using Microsoft.AspNetCore.Builder;
public static class HttpRequestHeadersBuilderMiddlewareExtensions
{
public static IApplicationBuilder UseHttpRequestHeadersBuilderMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpRequestHeadersBuilderMiddleware>();
}
}
}

View File

@ -12,6 +12,8 @@
builder.UseAuthenticationMiddleware();
builder.UseHttpRequestHeadersBuilderMiddleware();
builder.UseDownstreamUrlCreatorMiddleware();
builder.UseHttpRequestBuilderMiddleware();

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class AddHeadersToRequest : IAddHeadersToRequest
{
private readonly IClaimsParser _claimsParser;
public AddHeadersToRequest(IClaimsParser claimsParser)
{
_claimsParser = claimsParser;
}
public Response SetHeadersOnContext(List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties, HttpContext context)
{
foreach (var config in configurationHeaderExtractorProperties)
{
var value = _claimsParser.GetValue(context.User.Claims, config.ClaimKey, config.Delimiter, config.Index);
if (value.IsError)
{
return new ErrorResponse(value.Errors);
}
var exists = context.Request.Headers.FirstOrDefault(x => x.Key == config.HeaderKey);
if (!string.IsNullOrEmpty(exists.Key))
{
context.Request.Headers.Remove(exists);
}
context.Request.Headers.Add(config.HeaderKey, new StringValues(value.Data));
}
return new OkResponse();
}
}
}

View File

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

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Ocelot.Library.Errors;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class ClaimsParser : IClaimsParser
{
public Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index)
{
var claimResponse = GetValue(claims, key);
if (claimResponse.IsError)
{
return claimResponse;
}
if (string.IsNullOrEmpty(delimiter))
{
return claimResponse;
}
var splits = claimResponse.Data.Split(delimiter.ToCharArray());
if (splits.Length < index || index < 0)
{
return new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}")
});
}
var value = splits[index];
return new OkResponse<string>(value);
}
private Response<string> GetValue(IEnumerable<Claim> claims, string key)
{
var claim = claims.FirstOrDefault(c => c.Type == key);
if (claim != null)
{
return new OkResponse<string>(claim.Value);
}
return new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {key}")
});
}
}
}

View File

@ -0,0 +1,18 @@
namespace Ocelot.Library.RequestBuilder
{
public class ConfigurationHeaderExtractorProperties
{
public ConfigurationHeaderExtractorProperties(string headerKey, string claimKey, string delimiter, int index)
{
ClaimKey = claimKey;
Delimiter = delimiter;
Index = index;
HeaderKey = headerKey;
}
public string HeaderKey { get; private set; }
public string ClaimKey { get; private set; }
public string Delimiter { get; private set; }
public int Index { get; private set; }
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Ocelot.Library.Errors;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class ConfigurationHeaderExtrator : IConfigurationHeaderExtrator
{
private readonly Regex _claimRegex = new Regex("Claims\\[.*\\]");
private readonly Regex _indexRegex = new Regex("value\\[.*\\]");
private const string SplitToken = ">";
public Response<ConfigurationHeaderExtractorProperties> Extract(string headerKey, string value)
{
try
{
var instructions = value.Split(SplitToken.ToCharArray());
if (instructions.Length <= 1)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new NoInstructionsError(SplitToken)
});
}
var claimMatch = _claimRegex.IsMatch(instructions[0]);
if (!claimMatch)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new InstructionNotForClaimsError()
});
}
var claimKey = GetIndexValue(instructions[0]);
var index = 0;
var delimiter = string.Empty;
if (instructions.Length > 2 && _indexRegex.IsMatch(instructions[1]))
{
index = int.Parse(GetIndexValue(instructions[1]));
delimiter = instructions[2].Trim();
}
return new OkResponse<ConfigurationHeaderExtractorProperties>(
new ConfigurationHeaderExtractorProperties(headerKey, claimKey, delimiter, index));
}
catch (Exception exception)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new ParsingConfigurationHeaderError(exception)
});
}
}
private 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;
}
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IAddHeadersToRequest
{
Response SetHeadersOnContext(List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties,
HttpContext context);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Security.Claims;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IClaimsParser
{
Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index);
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IConfigurationHeaderExtrator
{
Response<ConfigurationHeaderExtractorProperties> Extract(string headerKey, string value);
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class InstructionNotForClaimsError : Error
{
public InstructionNotForClaimsError()
: base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class NoInstructionsError : Error
{
public NoInstructionsError(string splitToken)
: base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError)
{
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class ParsingConfigurationHeaderError : Error
{
public ParsingConfigurationHeaderError(Exception exception)
: base($"error parsing configuration eception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError)
{
}
}
}

View File

@ -12,18 +12,14 @@
{
public async Task<HttpContext> CreateResponse(HttpContext context, HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
context.Response.OnStarting(x =>
{
context.Response.OnStarting(x =>
{
context.Response.StatusCode = (int)response.StatusCode;
return Task.CompletedTask;
}, context);
context.Response.StatusCode = (int)response.StatusCode;
return Task.CompletedTask;
}, context);
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
return context;
}
return context;
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
return context;
}
public async Task<HttpContext> CreateErrorResponse(HttpContext context, int statusCode)