mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:22:50 +08:00
Added ability to strip claims and forward to downstream service as headers
This commit is contained in:
parent
279aae3151
commit
84256e7bac
21
README.md
21
README.md
@ -38,12 +38,25 @@ Priorities
|
|||||||
TBC really but example configuration for a route below.
|
TBC really but example configuration for a route below.
|
||||||
|
|
||||||
ReRoutes:
|
ReRoutes:
|
||||||
- DownstreamTemplate: http://localhost:51876/
|
# the url we are forwarding the request to
|
||||||
|
- DownstreamTemplate: http://localhost:52876/
|
||||||
|
# the path we are listening on for this re route
|
||||||
UpstreamTemplate: /
|
UpstreamTemplate: /
|
||||||
UpstreamHttpMethod: Post
|
# the method we are listening for on this re route
|
||||||
|
UpstreamHttpMethod: Get
|
||||||
|
# only support identity server at the moment
|
||||||
AuthenticationOptions:
|
AuthenticationOptions:
|
||||||
Provider: IdentityServer
|
Provider: IdentityServer
|
||||||
ProviderRootUrl: http://localhost:51888
|
ProviderRootUrl: http://localhost:52888
|
||||||
ScopeName: api
|
ScopeName: api
|
||||||
AdditionalScopes: []
|
AdditionalScopes:
|
||||||
|
- openid
|
||||||
|
- offline_access
|
||||||
|
#require if using reference tokens
|
||||||
ScopeSecret: secret
|
ScopeSecret: secret
|
||||||
|
# WARNING - will overwrite any headers already in the request with these values
|
||||||
|
AddHeadersToRequest:
|
||||||
|
CustomerId: Claims[CustomerId] > value
|
||||||
|
LocationId: Claims[LocationId] > value
|
||||||
|
UserType: Claims[sub] > value[0] > |
|
||||||
|
UserId: Claims[sub] > value[1] > |
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Ocelot.Library.Builder
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
|
||||||
|
namespace Ocelot.Library.Builder
|
||||||
{
|
{
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Configuration;
|
using Configuration;
|
||||||
@ -16,6 +18,7 @@
|
|||||||
private List<string> _additionalScopes;
|
private List<string> _additionalScopes;
|
||||||
private bool _requireHttps;
|
private bool _requireHttps;
|
||||||
private string _scopeSecret;
|
private string _scopeSecret;
|
||||||
|
private List<ConfigurationHeaderExtractorProperties> _configHeaderExtractorProperties;
|
||||||
|
|
||||||
public ReRouteBuilder()
|
public ReRouteBuilder()
|
||||||
{
|
{
|
||||||
@ -86,9 +89,15 @@
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReRouteBuilder WithConfigurationHeaderExtractorProperties(List<ConfigurationHeaderExtractorProperties> input)
|
||||||
|
{
|
||||||
|
_configHeaderExtractorProperties = input;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ReRoute Build()
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
|
||||||
namespace Ocelot.Library.Configuration
|
namespace Ocelot.Library.Configuration
|
||||||
{
|
{
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -11,11 +16,18 @@ namespace Ocelot.Library.Configuration
|
|||||||
private readonly List<ReRoute> _reRoutes;
|
private readonly List<ReRoute> _reRoutes;
|
||||||
private const string RegExMatchEverything = ".*";
|
private const string RegExMatchEverything = ".*";
|
||||||
private const string RegExMatchEndString = "$";
|
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;
|
_options = options;
|
||||||
_configurationValidator = configurationValidator;
|
_configurationValidator = configurationValidator;
|
||||||
|
_configurationHeaderExtrator = configurationHeaderExtrator;
|
||||||
|
_logger = logger;
|
||||||
_reRoutes = new List<ReRoute>();
|
_reRoutes = new List<ReRoute>();
|
||||||
SetUpConfiguration();
|
SetUpConfiguration();
|
||||||
}
|
}
|
||||||
@ -43,7 +55,7 @@ namespace Ocelot.Library.Configuration
|
|||||||
|
|
||||||
var placeholders = new List<string>();
|
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))
|
if (IsPlaceHolder(upstreamTemplate, i))
|
||||||
{
|
{
|
||||||
@ -70,17 +82,41 @@ namespace Ocelot.Library.Configuration
|
|||||||
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
|
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
|
||||||
reRoute.AuthenticationOptions.ScopeSecret);
|
reRoute.AuthenticationOptions.ScopeSecret);
|
||||||
|
|
||||||
|
var configHeaders = GetHeadersToAddToRequest(reRoute);
|
||||||
|
|
||||||
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
|
||||||
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, authOptionsForRoute
|
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
|
||||||
|
authOptionsForRoute, configHeaders
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod,
|
_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)
|
private bool IsPlaceHolder(string upstreamTemplate, int i)
|
||||||
{
|
{
|
||||||
return upstreamTemplate[i] == '{';
|
return upstreamTemplate[i] == '{';
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
namespace Ocelot.Library.Configuration
|
using System.Collections.Generic;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
|
||||||
|
namespace Ocelot.Library.Configuration
|
||||||
{
|
{
|
||||||
public class ReRoute
|
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;
|
DownstreamTemplate = downstreamTemplate;
|
||||||
UpstreamTemplate = upstreamTemplate;
|
UpstreamTemplate = upstreamTemplate;
|
||||||
@ -10,6 +13,8 @@
|
|||||||
UpstreamTemplatePattern = upstreamTemplatePattern;
|
UpstreamTemplatePattern = upstreamTemplatePattern;
|
||||||
IsAuthenticated = isAuthenticated;
|
IsAuthenticated = isAuthenticated;
|
||||||
AuthenticationOptions = authenticationOptions;
|
AuthenticationOptions = authenticationOptions;
|
||||||
|
ConfigurationHeaderExtractorProperties = configurationHeaderExtractorProperties
|
||||||
|
?? new List<ConfigurationHeaderExtractorProperties>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DownstreamTemplate { get; private set; }
|
public string DownstreamTemplate { get; private set; }
|
||||||
@ -18,5 +23,6 @@
|
|||||||
public string UpstreamHttpMethod { get; private set; }
|
public string UpstreamHttpMethod { get; private set; }
|
||||||
public bool IsAuthenticated { get; private set; }
|
public bool IsAuthenticated { get; private set; }
|
||||||
public AuthenticationOptions AuthenticationOptions { get; private set; }
|
public AuthenticationOptions AuthenticationOptions { get; private set; }
|
||||||
|
public List<ConfigurationHeaderExtractorProperties> ConfigurationHeaderExtractorProperties { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,6 +21,9 @@
|
|||||||
services.Configure<YamlConfiguration>(configurationRoot);
|
services.Configure<YamlConfiguration>(configurationRoot);
|
||||||
|
|
||||||
// Add framework services.
|
// Add framework services.
|
||||||
|
services.AddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
|
||||||
|
services.AddSingleton<IClaimsParser, ClaimsParser>();
|
||||||
|
services.AddSingleton<IConfigurationHeaderExtrator, ConfigurationHeaderExtrator>();
|
||||||
services.AddSingleton<IConfigurationValidator, ConfigurationValidator>();
|
services.AddSingleton<IConfigurationValidator, ConfigurationValidator>();
|
||||||
services.AddSingleton<IOcelotConfiguration, OcelotConfiguration>();
|
services.AddSingleton<IOcelotConfiguration, OcelotConfiguration>();
|
||||||
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
|
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
CannotFindDataError,
|
CannotFindDataError,
|
||||||
UnableToCompleteRequestError,
|
UnableToCompleteRequestError,
|
||||||
UnableToCreateAuthenticationHandlerError,
|
UnableToCreateAuthenticationHandlerError,
|
||||||
UnsupportedAuthenticationProviderError
|
UnsupportedAuthenticationProviderError,
|
||||||
|
CannotFindClaimError,
|
||||||
|
ParsingConfigurationHeaderError,
|
||||||
|
NoInstructionsError,
|
||||||
|
InstructionNotForClaimsError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
builder.UseAuthenticationMiddleware();
|
builder.UseAuthenticationMiddleware();
|
||||||
|
|
||||||
|
builder.UseHttpRequestHeadersBuilderMiddleware();
|
||||||
|
|
||||||
builder.UseDownstreamUrlCreatorMiddleware();
|
builder.UseDownstreamUrlCreatorMiddleware();
|
||||||
|
|
||||||
builder.UseHttpRequestBuilderMiddleware();
|
builder.UseHttpRequestBuilderMiddleware();
|
||||||
|
42
src/Ocelot.Library/RequestBuilder/AddHeadersToRequest.cs
Normal file
42
src/Ocelot.Library/RequestBuilder/AddHeadersToRequest.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/Ocelot.Library/RequestBuilder/CannotFindClaimError.cs
Normal file
12
src/Ocelot.Library/RequestBuilder/CannotFindClaimError.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
src/Ocelot.Library/RequestBuilder/ClaimsParser.cs
Normal file
55
src/Ocelot.Library/RequestBuilder/ClaimsParser.cs
Normal 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}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
73
src/Ocelot.Library/RequestBuilder/HeaderExtrator.cs
Normal file
73
src/Ocelot.Library/RequestBuilder/HeaderExtrator.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/Ocelot.Library/RequestBuilder/IAddHeadersToRequest.cs
Normal file
12
src/Ocelot.Library/RequestBuilder/IAddHeadersToRequest.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
11
src/Ocelot.Library/RequestBuilder/IClaimsParser.cs
Normal file
11
src/Ocelot.Library/RequestBuilder/IClaimsParser.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/Ocelot.Library/RequestBuilder/NoInstructionsError.cs
Normal file
12
src/Ocelot.Library/RequestBuilder/NoInstructionsError.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,6 @@
|
|||||||
public class HttpContextResponder : IHttpResponder
|
public class HttpContextResponder : IHttpResponder
|
||||||
{
|
{
|
||||||
public async Task<HttpContext> CreateResponse(HttpContext context, HttpResponseMessage response)
|
public async Task<HttpContext> CreateResponse(HttpContext context, HttpResponseMessage response)
|
||||||
{
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
context.Response.OnStarting(x =>
|
context.Response.OnStarting(x =>
|
||||||
{
|
{
|
||||||
@ -23,8 +21,6 @@
|
|||||||
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
|
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpContext> CreateErrorResponse(HttpContext context, int statusCode)
|
public async Task<HttpContext> CreateErrorResponse(HttpContext context, int statusCode)
|
||||||
{
|
{
|
||||||
|
@ -144,49 +144,6 @@ namespace Ocelot.AcceptanceTests
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void should_return_response_200_and_foward_claim_as_header()
|
|
||||||
{
|
|
||||||
|
|
||||||
this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt))
|
|
||||||
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 200, "Hello from Laura"))
|
|
||||||
.And(x => x.GivenIHaveAToken("http://localhost:51888"))
|
|
||||||
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
|
|
||||||
{
|
|
||||||
ReRoutes = new List<YamlReRoute>
|
|
||||||
{
|
|
||||||
new YamlReRoute
|
|
||||||
{
|
|
||||||
DownstreamTemplate = "http://localhost:51876/",
|
|
||||||
UpstreamTemplate = "/",
|
|
||||||
UpstreamHttpMethod = "Get",
|
|
||||||
AuthenticationOptions = new YamlAuthenticationOptions
|
|
||||||
{
|
|
||||||
AdditionalScopes = new List<string>(),
|
|
||||||
Provider = "IdentityServer",
|
|
||||||
ProviderRootUrl = "http://localhost:51888",
|
|
||||||
RequireHttps = false,
|
|
||||||
ScopeName = "api",
|
|
||||||
ScopeSecret = "secret"
|
|
||||||
},
|
|
||||||
AddHeadersToRequest =
|
|
||||||
{
|
|
||||||
{ "CustomerId", "Claims[CustomerId] -> value" },
|
|
||||||
{ "LocationId", "Claims[LocationId] -> value"},
|
|
||||||
{ "UserId", "Claims[Subject] -> delimiter(|) -> value[0]" },
|
|
||||||
{ "UserId", "Claims[Subject] -> delimiter(|) -> value[1]" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.And(x => x.GivenTheApiGatewayIsRunning())
|
|
||||||
.And(x => x.GivenIHaveAddedATokenToMyRequest())
|
|
||||||
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
|
|
||||||
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
|
||||||
.And(x => x.ThenTheResponseBodyShouldBe("Hello from Laura"))
|
|
||||||
.BDDfy();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void should_return_201_using_identity_server_access_token()
|
public void should_return_201_using_identity_server_access_token()
|
||||||
{
|
{
|
||||||
|
291
test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs
Normal file
291
test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using IdentityServer4.Models;
|
||||||
|
using IdentityServer4.Services.InMemory;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Ocelot.Library.Configuration.Yaml;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
public class ClaimsToHeadersForwardingTests : IDisposable
|
||||||
|
{
|
||||||
|
private TestServer _ocelotServer;
|
||||||
|
private HttpClient _ocelotClient;
|
||||||
|
private HttpResponseMessage _response;
|
||||||
|
private readonly string _configurationPath;
|
||||||
|
private IWebHost _servicebuilder;
|
||||||
|
|
||||||
|
// Sadly we need to change this when we update the netcoreapp version to make the test update the config correctly
|
||||||
|
private double _netCoreAppVersion = 1.4;
|
||||||
|
private BearerToken _token;
|
||||||
|
private IWebHost _identityServerBuilder;
|
||||||
|
|
||||||
|
public ClaimsToHeadersForwardingTests()
|
||||||
|
{
|
||||||
|
_configurationPath = $"./bin/Debug/netcoreapp{_netCoreAppVersion}/configuration.yaml";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_and_foward_claim_as_header()
|
||||||
|
{
|
||||||
|
var user = new InMemoryUser
|
||||||
|
{
|
||||||
|
Username = "test",
|
||||||
|
Password = "test",
|
||||||
|
Enabled = true,
|
||||||
|
Subject = "registered|1231231",
|
||||||
|
Claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim("CustomerId", "123"),
|
||||||
|
new Claim("LocationId", "1")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:52888", "api", AccessTokenType.Jwt, user))
|
||||||
|
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:52876", 200))
|
||||||
|
.And(x => x.GivenIHaveAToken("http://localhost:52888"))
|
||||||
|
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<YamlReRoute>
|
||||||
|
{
|
||||||
|
new YamlReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:52876/",
|
||||||
|
UpstreamTemplate = "/",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
AuthenticationOptions = new YamlAuthenticationOptions
|
||||||
|
{
|
||||||
|
AdditionalScopes = new List<string>
|
||||||
|
{
|
||||||
|
"openid", "offline_access"
|
||||||
|
},
|
||||||
|
Provider = "IdentityServer",
|
||||||
|
ProviderRootUrl = "http://localhost:52888",
|
||||||
|
RequireHttps = false,
|
||||||
|
ScopeName = "api",
|
||||||
|
ScopeSecret = "secret",
|
||||||
|
},
|
||||||
|
AddHeadersToRequest =
|
||||||
|
{
|
||||||
|
{"CustomerId", "Claims[CustomerId] > value"},
|
||||||
|
{"LocationId", "Claims[LocationId] > value"},
|
||||||
|
{"UserType", "Claims[sub] > value[0] > |"},
|
||||||
|
{"UserId", "Claims[sub] > value[1] > |"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheApiGatewayIsRunning())
|
||||||
|
.And(x => x.GivenIHaveAddedATokenToMyRequest())
|
||||||
|
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
|
||||||
|
.And(x => x.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231"))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetUrlOnTheApiGateway(string url)
|
||||||
|
{
|
||||||
|
_response = _ocelotClient.GetAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResponseBodyShouldBe(string expectedBody)
|
||||||
|
{
|
||||||
|
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is annoying cos it should be in the constructor but we need to set up the yaml file before calling startup so its a step.
|
||||||
|
/// </summary>
|
||||||
|
private void GivenTheApiGatewayIsRunning()
|
||||||
|
{
|
||||||
|
_ocelotServer = new TestServer(new WebHostBuilder()
|
||||||
|
.UseStartup<Startup>());
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAConfiguration(YamlConfiguration yamlConfiguration)
|
||||||
|
{
|
||||||
|
var serializer = new Serializer();
|
||||||
|
|
||||||
|
if (File.Exists(_configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(_configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (TextWriter writer = File.CreateText(_configurationPath))
|
||||||
|
{
|
||||||
|
serializer.Serialize(writer, yamlConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url, int statusCode)
|
||||||
|
{
|
||||||
|
_servicebuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
var customerId = context.Request.Headers.First(x => x.Key == "CustomerId").Value.First();
|
||||||
|
var locationId = context.Request.Headers.First(x => x.Key == "LocationId").Value.First();
|
||||||
|
var userType = context.Request.Headers.First(x => x.Key == "UserType").Value.First();
|
||||||
|
var userId = context.Request.Headers.First(x => x.Key == "UserId").Value.First();
|
||||||
|
|
||||||
|
var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}";
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_servicebuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, InMemoryUser user)
|
||||||
|
{
|
||||||
|
_identityServerBuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddDeveloperIdentityServer()
|
||||||
|
.AddInMemoryScopes(new List<Scope>
|
||||||
|
{
|
||||||
|
new Scope
|
||||||
|
{
|
||||||
|
Name = scopeName,
|
||||||
|
Description = "My API",
|
||||||
|
Enabled = true,
|
||||||
|
AllowUnrestrictedIntrospection = true,
|
||||||
|
ScopeSecrets = new List<Secret>()
|
||||||
|
{
|
||||||
|
new Secret
|
||||||
|
{
|
||||||
|
Value = "secret".Sha256()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IncludeAllClaimsForUser = true
|
||||||
|
},
|
||||||
|
|
||||||
|
StandardScopes.OpenId,
|
||||||
|
StandardScopes.OfflineAccess
|
||||||
|
})
|
||||||
|
.AddInMemoryClients(new List<Client>
|
||||||
|
{
|
||||||
|
new Client
|
||||||
|
{
|
||||||
|
ClientId = "client",
|
||||||
|
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
|
||||||
|
ClientSecrets = new List<Secret> {new Secret("secret".Sha256())},
|
||||||
|
AllowedScopes = new List<string> { scopeName, "openid", "offline_access" },
|
||||||
|
AccessTokenType = tokenType,
|
||||||
|
Enabled = true,
|
||||||
|
RequireClientSecret = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.AddInMemoryUsers(new List<InMemoryUser>
|
||||||
|
{
|
||||||
|
user
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseIdentityServer();
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_identityServerBuilder.Start();
|
||||||
|
|
||||||
|
VerifyIdentiryServerStarted(url);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyIdentiryServerStarted(string url)
|
||||||
|
{
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAToken(string url)
|
||||||
|
{
|
||||||
|
var tokenUrl = $"{url}/connect/token";
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("client_id", "client"),
|
||||||
|
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||||
|
new KeyValuePair<string, string>("scope", "api"),
|
||||||
|
new KeyValuePair<string, string>("username", "test"),
|
||||||
|
new KeyValuePair<string, string>("password", "test"),
|
||||||
|
new KeyValuePair<string, string>("grant_type", "password")
|
||||||
|
};
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
using (var httpClient = new HttpClient())
|
||||||
|
{
|
||||||
|
var response = httpClient.PostAsync(tokenUrl, content).Result;
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var responseContent = response.Content.ReadAsStringAsync().Result;
|
||||||
|
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIHaveAddedATokenToMyRequest()
|
||||||
|
{
|
||||||
|
_ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||||
|
{
|
||||||
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_servicebuilder?.Dispose();
|
||||||
|
_ocelotClient?.Dispose();
|
||||||
|
_ocelotServer?.Dispose();
|
||||||
|
_identityServerBuilder?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once ClassNeverInstantiated.Local
|
||||||
|
class BearerToken
|
||||||
|
{
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("token_type")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs
Normal file
123
test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Ocelot.Library.Configuration.Yaml;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
public class ReturnsErrorTests : IDisposable
|
||||||
|
{
|
||||||
|
private TestServer _ocelotServer;
|
||||||
|
private HttpClient _ocelotClient;
|
||||||
|
private HttpResponseMessage _response;
|
||||||
|
private readonly string _configurationPath;
|
||||||
|
private IWebHost _servicebuilder;
|
||||||
|
|
||||||
|
// Sadly we need to change this when we update the netcoreapp version to make the test update the config correctly
|
||||||
|
private double _netCoreAppVersion = 1.4;
|
||||||
|
|
||||||
|
public ReturnsErrorTests()
|
||||||
|
{
|
||||||
|
_configurationPath = $"./bin/Debug/netcoreapp{_netCoreAppVersion}/configuration.yaml";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_response_200_and_foward_claim_as_header()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876"))
|
||||||
|
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<YamlReRoute>
|
||||||
|
{
|
||||||
|
new YamlReRoute
|
||||||
|
{
|
||||||
|
DownstreamTemplate = "http://localhost:53876/",
|
||||||
|
UpstreamTemplate = "/",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheApiGatewayIsRunning())
|
||||||
|
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
|
||||||
|
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIGetUrlOnTheApiGateway(string url)
|
||||||
|
{
|
||||||
|
_response = _ocelotClient.GetAsync(url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResponseBodyShouldBe(string expectedBody)
|
||||||
|
{
|
||||||
|
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is annoying cos it should be in the constructor but we need to set up the yaml file before calling startup so its a step.
|
||||||
|
/// </summary>
|
||||||
|
private void GivenTheApiGatewayIsRunning()
|
||||||
|
{
|
||||||
|
_ocelotServer = new TestServer(new WebHostBuilder()
|
||||||
|
.UseStartup<Startup>());
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAConfiguration(YamlConfiguration yamlConfiguration)
|
||||||
|
{
|
||||||
|
var serializer = new Serializer();
|
||||||
|
|
||||||
|
if (File.Exists(_configurationPath))
|
||||||
|
{
|
||||||
|
File.Delete(_configurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (TextWriter writer = File.CreateText(_configurationPath))
|
||||||
|
{
|
||||||
|
serializer.Serialize(writer, yamlConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url)
|
||||||
|
{
|
||||||
|
_servicebuilder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
throw new Exception("BLAMMMM");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_servicebuilder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
|
||||||
|
{
|
||||||
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_servicebuilder?.Dispose();
|
||||||
|
_ocelotClient?.Dispose();
|
||||||
|
_ocelotServer?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -18,9 +20,13 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
private readonly Mock<IConfigurationValidator> _validator;
|
private readonly Mock<IConfigurationValidator> _validator;
|
||||||
private OcelotConfiguration _config;
|
private OcelotConfiguration _config;
|
||||||
private YamlConfiguration _yamlConfiguration;
|
private YamlConfiguration _yamlConfiguration;
|
||||||
|
private readonly Mock<IConfigurationHeaderExtrator> _configExtractor;
|
||||||
|
private readonly Mock<ILogger<OcelotConfiguration>> _logger;
|
||||||
|
|
||||||
public OcelotConfigurationTests()
|
public OcelotConfigurationTests()
|
||||||
{
|
{
|
||||||
|
_logger = new Mock<ILogger<OcelotConfiguration>>();
|
||||||
|
_configExtractor = new Mock<IConfigurationHeaderExtrator>();
|
||||||
_validator = new Mock<IConfigurationValidator>();
|
_validator = new Mock<IConfigurationValidator>();
|
||||||
_yamlConfig = new Mock<IOptions<YamlConfiguration>>();
|
_yamlConfig = new Mock<IOptions<YamlConfiguration>>();
|
||||||
}
|
}
|
||||||
@ -54,6 +60,114 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_create_with_headers_to_extract()
|
||||||
|
{
|
||||||
|
var expected = new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("/api/products/.*$")
|
||||||
|
.WithAuthenticationProvider("IdentityServer")
|
||||||
|
.WithAuthenticationProviderUrl("http://localhost:51888")
|
||||||
|
.WithRequireHttps(false)
|
||||||
|
.WithScopeSecret("secret")
|
||||||
|
.WithAuthenticationProviderScopeName("api")
|
||||||
|
.WithConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
|
||||||
|
{
|
||||||
|
new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0),
|
||||||
|
})
|
||||||
|
.Build()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenTheYamlConfigIs(new YamlConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<YamlReRoute>
|
||||||
|
{
|
||||||
|
new YamlReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
AuthenticationOptions = new YamlAuthenticationOptions
|
||||||
|
{
|
||||||
|
AdditionalScopes = new List<string>(),
|
||||||
|
Provider = "IdentityServer",
|
||||||
|
ProviderRootUrl = "http://localhost:51888",
|
||||||
|
RequireHttps = false,
|
||||||
|
ScopeName = "api",
|
||||||
|
ScopeSecret = "secret"
|
||||||
|
},
|
||||||
|
AddHeadersToRequest =
|
||||||
|
{
|
||||||
|
{"CustomerId", "Claims[CustomerId] > value"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheYamlConfigIsValid())
|
||||||
|
.And(x => x.GivenTheConfigHeaderExtractorReturns(new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0)))
|
||||||
|
.When(x => x.WhenIInstanciateTheOcelotConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(expected))
|
||||||
|
.And(x => x.ThenTheAuthenticationOptionsAre(expected))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheConfigHeaderExtractorReturns(ConfigurationHeaderExtractorProperties expected)
|
||||||
|
{
|
||||||
|
_configExtractor
|
||||||
|
.Setup(x => x.Extract(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(new OkResponse<ConfigurationHeaderExtractorProperties>(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_create_with_authentication_properties()
|
||||||
|
{
|
||||||
|
var expected = new List<ReRoute>
|
||||||
|
{
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("/products/{productId}")
|
||||||
|
.WithUpstreamTemplate("/api/products/{productId}")
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.WithUpstreamTemplatePattern("/api/products/.*$")
|
||||||
|
.WithAuthenticationProvider("IdentityServer")
|
||||||
|
.WithAuthenticationProviderUrl("http://localhost:51888")
|
||||||
|
.WithRequireHttps(false)
|
||||||
|
.WithScopeSecret("secret")
|
||||||
|
.WithAuthenticationProviderScopeName("api")
|
||||||
|
.Build()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenTheYamlConfigIs(new YamlConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<YamlReRoute>
|
||||||
|
{
|
||||||
|
new YamlReRoute
|
||||||
|
{
|
||||||
|
UpstreamTemplate = "/api/products/{productId}",
|
||||||
|
DownstreamTemplate = "/products/{productId}",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
AuthenticationOptions = new YamlAuthenticationOptions
|
||||||
|
{
|
||||||
|
AdditionalScopes = new List<string>(),
|
||||||
|
Provider = "IdentityServer",
|
||||||
|
ProviderRootUrl = "http://localhost:51888",
|
||||||
|
RequireHttps = false,
|
||||||
|
ScopeName = "api",
|
||||||
|
ScopeSecret = "secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.And(x => x.GivenTheYamlConfigIsValid())
|
||||||
|
.When(x => x.WhenIInstanciateTheOcelotConfig())
|
||||||
|
.Then(x => x.ThenTheReRoutesAre(expected))
|
||||||
|
.And(x => x.ThenTheAuthenticationOptionsAre(expected))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void should_create_template_pattern_that_matches_more_than_one_placeholder()
|
public void should_create_template_pattern_that_matches_more_than_one_placeholder()
|
||||||
{
|
{
|
||||||
@ -158,7 +272,8 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
|
|
||||||
private void WhenIInstanciateTheOcelotConfig()
|
private void WhenIInstanciateTheOcelotConfig()
|
||||||
{
|
{
|
||||||
_config = new OcelotConfiguration(_yamlConfig.Object, _validator.Object);
|
_config = new OcelotConfiguration(_yamlConfig.Object, _validator.Object,
|
||||||
|
_configExtractor.Object, _logger.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes)
|
private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes)
|
||||||
@ -174,5 +289,22 @@ namespace Ocelot.UnitTests.Configuration
|
|||||||
result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern);
|
result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ThenTheAuthenticationOptionsAre(List<ReRoute> expectedReRoutes)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _config.ReRoutes.Count; i++)
|
||||||
|
{
|
||||||
|
var result = _config.ReRoutes[i].AuthenticationOptions;
|
||||||
|
var expected = expectedReRoutes[i].AuthenticationOptions;
|
||||||
|
|
||||||
|
result.AdditionalScopes.ShouldBe(expected.AdditionalScopes);
|
||||||
|
result.Provider.ShouldBe(expected.Provider);
|
||||||
|
result.ProviderRootUrl.ShouldBe(expected.ProviderRootUrl);
|
||||||
|
result.RequireHttps.ShouldBe(expected.RequireHttps);
|
||||||
|
result.ScopeName.ShouldBe(expected.ScopeName);
|
||||||
|
result.ScopeSecret.ShouldBe(expected.ScopeSecret);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Library.Builder;
|
||||||
|
using Ocelot.Library.DownstreamRouteFinder;
|
||||||
|
using Ocelot.Library.Middleware;
|
||||||
|
using Ocelot.Library.Repository;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
using Ocelot.Library.Responses;
|
||||||
|
using Ocelot.Library.UrlMatcher;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.Middleware
|
||||||
|
{
|
||||||
|
public class HttpRequestHeadersBuilderMiddlewareTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Mock<IScopedRequestDataRepository> _scopedRepository;
|
||||||
|
private readonly Mock<IAddHeadersToRequest> _addHeaders;
|
||||||
|
private readonly string _url;
|
||||||
|
private readonly TestServer _server;
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private Response<DownstreamRoute> _downstreamRoute;
|
||||||
|
private HttpResponseMessage _result;
|
||||||
|
|
||||||
|
public HttpRequestHeadersBuilderMiddlewareTests()
|
||||||
|
{
|
||||||
|
_url = "http://localhost:51879";
|
||||||
|
_scopedRepository = new Mock<IScopedRequestDataRepository>();
|
||||||
|
_addHeaders = new Mock<IAddHeadersToRequest>();
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(x =>
|
||||||
|
{
|
||||||
|
x.AddSingleton(_addHeaders.Object);
|
||||||
|
x.AddSingleton(_scopedRepository.Object);
|
||||||
|
})
|
||||||
|
.UseUrls(_url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(_url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHttpRequestHeadersBuilderMiddleware();
|
||||||
|
});
|
||||||
|
|
||||||
|
_server = new TestServer(builder);
|
||||||
|
_client = _server.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void happy_path()
|
||||||
|
{
|
||||||
|
var downstreamRoute = new DownstreamRoute(new List<TemplateVariableNameAndValue>(),
|
||||||
|
new ReRouteBuilder()
|
||||||
|
.WithDownstreamTemplate("any old string")
|
||||||
|
.WithConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
|
||||||
|
{
|
||||||
|
new ConfigurationHeaderExtractorProperties("UserId", "Subject", "", 0)
|
||||||
|
})
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
|
||||||
|
.And(x => x.GivenTheAddHeadersToRequestReturns("123"))
|
||||||
|
.When(x => x.WhenICallTheMiddleware())
|
||||||
|
.Then(x => x.ThenTheAddHeadersToRequestIsCalledCorrectly())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheAddHeadersToRequestReturns(string claimValue)
|
||||||
|
{
|
||||||
|
_addHeaders
|
||||||
|
.Setup(x => x.SetHeadersOnContext(It.IsAny<List<ConfigurationHeaderExtractorProperties>>(),
|
||||||
|
It.IsAny<HttpContext>()))
|
||||||
|
.Returns(new OkResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheAddHeadersToRequestIsCalledCorrectly()
|
||||||
|
{
|
||||||
|
_addHeaders
|
||||||
|
.Verify(x => x.SetHeadersOnContext(It.IsAny<List<ConfigurationHeaderExtractorProperties>>(),
|
||||||
|
It.IsAny<HttpContext>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenICallTheMiddleware()
|
||||||
|
{
|
||||||
|
_result = _client.GetAsync(_url).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
|
||||||
|
{
|
||||||
|
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
|
||||||
|
_scopedRepository
|
||||||
|
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
|
||||||
|
.Returns(_downstreamRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
_server.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
152
test/Ocelot.UnitTests/RequestBuilder/AddHeadersToRequestTests.cs
Normal file
152
test/Ocelot.UnitTests/RequestBuilder/AddHeadersToRequestTests.cs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Moq;
|
||||||
|
using Ocelot.Library.Errors;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
using Ocelot.Library.Responses;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.RequestBuilder
|
||||||
|
{
|
||||||
|
public class AddHeadersToRequestTests
|
||||||
|
{
|
||||||
|
private readonly AddHeadersToRequest _addHeadersToRequest;
|
||||||
|
private readonly Mock<IClaimsParser> _parser;
|
||||||
|
private List<ConfigurationHeaderExtractorProperties> _configuration;
|
||||||
|
private HttpContext _context;
|
||||||
|
private Response _result;
|
||||||
|
private Response<string> _claimValue;
|
||||||
|
|
||||||
|
public AddHeadersToRequestTests()
|
||||||
|
{
|
||||||
|
_parser = new Mock<IClaimsParser>();
|
||||||
|
_addHeadersToRequest = new AddHeadersToRequest(_parser.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_add_headers_to_context()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim("test", "data")
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(
|
||||||
|
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
|
||||||
|
{
|
||||||
|
new ConfigurationHeaderExtractorProperties("header-key", "", "", 0)
|
||||||
|
}))
|
||||||
|
.Given(x => x.GivenHttpContext(context))
|
||||||
|
.And(x => x.GivenTheClaimParserReturns(new OkResponse<string>("value")))
|
||||||
|
.When(x => x.WhenIAddHeadersToTheRequest())
|
||||||
|
.Then(x => x.ThenTheResultIsSuccess())
|
||||||
|
.And(x => x.ThenTheHeaderIsAdded())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void if_header_exists_should_replace_it()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim("test", "data")
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Request.Headers.Add("header-key", new StringValues("initial"));
|
||||||
|
|
||||||
|
this.Given(
|
||||||
|
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
|
||||||
|
{
|
||||||
|
new ConfigurationHeaderExtractorProperties("header-key", "", "", 0)
|
||||||
|
}))
|
||||||
|
.Given(x => x.GivenHttpContext(context))
|
||||||
|
.And(x => x.GivenTheClaimParserReturns(new OkResponse<string>("value")))
|
||||||
|
.When(x => x.WhenIAddHeadersToTheRequest())
|
||||||
|
.Then(x => x.ThenTheResultIsSuccess())
|
||||||
|
.And(x => x.ThenTheHeaderIsAdded())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_error()
|
||||||
|
{
|
||||||
|
this.Given(
|
||||||
|
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
|
||||||
|
{
|
||||||
|
new ConfigurationHeaderExtractorProperties("", "", "", 0)
|
||||||
|
}))
|
||||||
|
.Given(x => x.GivenHttpContext(new DefaultHttpContext()))
|
||||||
|
.And(x => x.GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error>
|
||||||
|
{
|
||||||
|
new AnyError()
|
||||||
|
})))
|
||||||
|
.When(x => x.WhenIAddHeadersToTheRequest())
|
||||||
|
.Then(x => x.ThenTheResultIsError())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheHeaderIsAdded()
|
||||||
|
{
|
||||||
|
var header = _context.Request.Headers.First(x => x.Key == "header-key");
|
||||||
|
header.Value.First().ShouldBe(_claimValue.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenConfigurationHeaderExtractorProperties(List<ConfigurationHeaderExtractorProperties> configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenHttpContext(HttpContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheClaimParserReturns(Response<string> claimValue)
|
||||||
|
{
|
||||||
|
_claimValue = claimValue;
|
||||||
|
_parser
|
||||||
|
.Setup(
|
||||||
|
x =>
|
||||||
|
x.GetValue(It.IsAny<IEnumerable<Claim>>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<int>()))
|
||||||
|
.Returns(_claimValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenIAddHeadersToTheRequest()
|
||||||
|
{
|
||||||
|
_result = _addHeadersToRequest.SetHeadersOnContext(_configuration, _context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResultIsSuccess()
|
||||||
|
{
|
||||||
|
_result.IsError.ShouldBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResultIsError()
|
||||||
|
{
|
||||||
|
|
||||||
|
_result.IsError.ShouldBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnyError : Error
|
||||||
|
{
|
||||||
|
public AnyError()
|
||||||
|
: base("blahh", OcelotErrorCode.UnknownError)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
test/Ocelot.UnitTests/RequestBuilder/ClaimParserTests.cs
Normal file
123
test/Ocelot.UnitTests/RequestBuilder/ClaimParserTests.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Ocelot.Library.Errors;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
using Ocelot.Library.Responses;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.RequestBuilder
|
||||||
|
{
|
||||||
|
public class ClaimParserTests
|
||||||
|
{
|
||||||
|
private readonly IClaimsParser _claimsParser;
|
||||||
|
private readonly List<Claim> _claims;
|
||||||
|
private string _key;
|
||||||
|
private Response<string> _result;
|
||||||
|
private string _delimiter;
|
||||||
|
private int _index;
|
||||||
|
|
||||||
|
public ClaimParserTests()
|
||||||
|
{
|
||||||
|
_claims = new List<Claim>();
|
||||||
|
_claimsParser = new Library.RequestBuilder.ClaimsParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void can_parse_claims_dictionary_access_string_returning_value_to_function()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenAClaimOf(new Claim("CustomerId", "1234")))
|
||||||
|
.And(x => x.GivenTheKeyIs("CustomerId"))
|
||||||
|
.When(x => x.WhenICallTheParser())
|
||||||
|
.Then(x => x.ThenTheResultIs(new OkResponse<string>("1234")))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_error_response_when_cannot_find_requested_claim()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenAClaimOf(new Claim("BallsId", "1234")))
|
||||||
|
.And(x => x.GivenTheKeyIs("CustomerId"))
|
||||||
|
.When(x => x.WhenICallTheParser())
|
||||||
|
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
|
||||||
|
{
|
||||||
|
new CannotFindClaimError($"Cannot find claim for key: {_key}")
|
||||||
|
})))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void can_parse_claims_dictionary_access_string_using_delimiter_and_retuning_at_correct_index()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
|
||||||
|
.And(x => x.GivenTheDelimiterIs("|"))
|
||||||
|
.And(x => x.GivenTheIndexIs(1))
|
||||||
|
.And(x => x.GivenTheKeyIs("Subject"))
|
||||||
|
.When(x => x.WhenICallTheParser())
|
||||||
|
.Then(x => x.ThenTheResultIs(new OkResponse<string>("4321")))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_error_response_if_index_too_large()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
|
||||||
|
.And(x => x.GivenTheDelimiterIs("|"))
|
||||||
|
.And(x => x.GivenTheIndexIs(24))
|
||||||
|
.And(x => x.GivenTheKeyIs("Subject"))
|
||||||
|
.When(x => x.WhenICallTheParser())
|
||||||
|
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
|
||||||
|
{
|
||||||
|
new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}")
|
||||||
|
})))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_error_response_if_index_too_small()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
|
||||||
|
.And(x => x.GivenTheDelimiterIs("|"))
|
||||||
|
.And(x => x.GivenTheIndexIs(-1))
|
||||||
|
.And(x => x.GivenTheKeyIs("Subject"))
|
||||||
|
.When(x => x.WhenICallTheParser())
|
||||||
|
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
|
||||||
|
{
|
||||||
|
new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}")
|
||||||
|
})))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheIndexIs(int index)
|
||||||
|
{
|
||||||
|
_index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheDelimiterIs(string delimiter)
|
||||||
|
{
|
||||||
|
_delimiter = delimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenAClaimOf(Claim claim)
|
||||||
|
{
|
||||||
|
_claims.Add(claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheKeyIs(string key)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenICallTheParser()
|
||||||
|
{
|
||||||
|
_result = _claimsParser.GetValue(_claims, _key, _delimiter, _index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheResultIs(Response<string> expected)
|
||||||
|
{
|
||||||
|
_result.Data.ShouldBe(expected.Data);
|
||||||
|
_result.IsError.ShouldBe(expected.IsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Ocelot.Library.Errors;
|
||||||
|
using Ocelot.Library.RequestBuilder;
|
||||||
|
using Ocelot.Library.Responses;
|
||||||
|
using Shouldly;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.RequestBuilder
|
||||||
|
{
|
||||||
|
public class ConfigurationHeadersExtractorTests
|
||||||
|
{
|
||||||
|
private Dictionary<string, string> _dictionary;
|
||||||
|
private readonly IConfigurationHeaderExtrator _configurationHeaderExtrator;
|
||||||
|
private Response<ConfigurationHeaderExtractorProperties> _result;
|
||||||
|
|
||||||
|
public ConfigurationHeadersExtractorTests()
|
||||||
|
{
|
||||||
|
_configurationHeaderExtrator = new ConfigurationHeaderExtrator();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void returns_no_instructions_error()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"CustomerId", ""},
|
||||||
|
}))
|
||||||
|
.When(x => x.WhenICallTheExtractor())
|
||||||
|
.Then(
|
||||||
|
x =>
|
||||||
|
x.ThenAnErrorIsReturned(new ErrorResponse<ConfigurationHeaderExtractorProperties>(
|
||||||
|
new List<Error>
|
||||||
|
{
|
||||||
|
new NoInstructionsError(">")
|
||||||
|
})))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void returns_no_instructions_not_for_claims_error()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"CustomerId", "Cheese[CustomerId] > value"},
|
||||||
|
}))
|
||||||
|
.When(x => x.WhenICallTheExtractor())
|
||||||
|
.Then(
|
||||||
|
x =>
|
||||||
|
x.ThenAnErrorIsReturned(new ErrorResponse<ConfigurationHeaderExtractorProperties>(
|
||||||
|
new List<Error>
|
||||||
|
{
|
||||||
|
new InstructionNotForClaimsError()
|
||||||
|
})))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void can_parse_entry_to_work_out_properties_with_key()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"CustomerId", "Claims[CustomerId] > value"},
|
||||||
|
}))
|
||||||
|
.When(x => x.WhenICallTheExtractor())
|
||||||
|
.Then(
|
||||||
|
x =>
|
||||||
|
x.ThenTheClaimParserPropertiesAreReturned(
|
||||||
|
new OkResponse<ConfigurationHeaderExtractorProperties>(
|
||||||
|
new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0))))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void can_parse_entry_to_work_out_properties_with_key_delimiter_and_index()
|
||||||
|
{
|
||||||
|
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"UserId", "Claims[Subject] > value[0] > |"},
|
||||||
|
}))
|
||||||
|
.When(x => x.WhenICallTheExtractor())
|
||||||
|
.Then(
|
||||||
|
x =>
|
||||||
|
x.ThenTheClaimParserPropertiesAreReturned(
|
||||||
|
new OkResponse<ConfigurationHeaderExtractorProperties>(
|
||||||
|
new ConfigurationHeaderExtractorProperties("UserId", "Subject", "|", 0))))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenAnErrorIsReturned(Response<ConfigurationHeaderExtractorProperties> expected)
|
||||||
|
{
|
||||||
|
_result.IsError.ShouldBe(expected.IsError);
|
||||||
|
_result.Errors[0].ShouldBeOfType(expected.Errors[0].GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThenTheClaimParserPropertiesAreReturned(Response<ConfigurationHeaderExtractorProperties> expected)
|
||||||
|
{
|
||||||
|
_result.Data.ClaimKey.ShouldBe(expected.Data.ClaimKey);
|
||||||
|
_result.Data.Delimiter.ShouldBe(expected.Data.Delimiter);
|
||||||
|
_result.Data.Index.ShouldBe(expected.Data.Index);
|
||||||
|
_result.IsError.ShouldBe(expected.IsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WhenICallTheExtractor()
|
||||||
|
{
|
||||||
|
var first = _dictionary.First();
|
||||||
|
_result = _configurationHeaderExtrator.Extract(first.Key, first.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenTheDictionaryIs(Dictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
_dictionary = dictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user