From 81173663132787b63404c0fcb1b973d3a0334415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Martos?= Date: Mon, 12 Aug 2019 19:03:20 +0200 Subject: [PATCH] [New feature] Support claims to path transformation (#968) * Add the option to change DownstreamPath based on Claims * Add tests for Claims to downstream path --- .../Builder/DownstreamReRouteBuilder.cs | 10 +- .../Configuration/Creator/ReRoutesCreator.cs | 3 + src/Ocelot/Configuration/DownstreamReRoute.cs | 3 + src/Ocelot/Configuration/File/FileReRoute.cs | 2 + .../DependencyInjection/OcelotBuilder.cs | 2 + .../ChangeDownstreamPathTemplate.cs | 52 +++++ .../IChangeDownstreamPathTemplate.cs | 18 ++ .../ClaimsToDownstreamPathMiddleware.cs | 42 ++++ ...imsToDownstreamPathMiddlewareExtensions.cs | 12 ++ .../Pipeline/OcelotPipelineExtensions.cs | 3 + .../ClaimsToDownstreamPathTests.cs | 201 ++++++++++++++++++ .../ChangeDownstreamPathTemplateTests.cs | 196 +++++++++++++++++ .../ClaimsToDownstreamPathMiddlewareTests.cs | 101 +++++++++ 13 files changed, 644 insertions(+), 1 deletion(-) create mode 100644 src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs create mode 100644 src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs create mode 100644 src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs create mode 100644 src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs create mode 100644 test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs create mode 100644 test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs create mode 100644 test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs index 2d7e9207..4b3e6ea3 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs @@ -19,6 +19,7 @@ namespace Ocelot.Configuration.Builder private Dictionary _routeClaimRequirement; private bool _isAuthorised; private List _claimToQueries; + private List _claimToDownstreamPath; private string _requestIdHeaderKey; private bool _isCached; private CacheOptions _fileCacheOptions; @@ -127,6 +128,12 @@ namespace Ocelot.Configuration.Builder return this; } + public DownstreamReRouteBuilder WithClaimsToDownstreamPath(List input) + { + _claimToDownstreamPath = input; + return this; + } + public DownstreamReRouteBuilder WithIsCached(bool input) { _isCached = input; @@ -186,7 +193,7 @@ namespace Ocelot.Configuration.Builder _serviceName = serviceName; return this; } - + public DownstreamReRouteBuilder WithServiceNamespace(string serviceNamespace) { _serviceNamespace = serviceNamespace; @@ -265,6 +272,7 @@ namespace Ocelot.Configuration.Builder _claimToQueries, _claimsToHeaders, _claimToClaims, + _claimToDownstreamPath, _isAuthenticated, _isAuthorised, _authenticationOptions, diff --git a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs b/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs index 52b81985..4443e201 100644 --- a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs @@ -86,6 +86,8 @@ namespace Ocelot.Configuration.Creator var claimsToQueries = _claimsToThingCreator.Create(fileReRoute.AddQueriesToRequest); + var claimsToDownstreamPath = _claimsToThingCreator.Create(fileReRoute.ChangeDownstreamPathTemplate); + var qosOptions = _qosOptionsCreator.Create(fileReRoute.QoSOptions, fileReRoute.UpstreamPathTemplate, fileReRoute.UpstreamHttpMethod); var rateLimitOption = _rateLimitOptionsCreator.Create(fileReRoute.RateLimitOptions, globalConfiguration); @@ -114,6 +116,7 @@ namespace Ocelot.Configuration.Creator .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) .WithIsAuthorised(fileReRouteOptions.IsAuthorised) .WithClaimsToQueries(claimsToQueries) + .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithRequestIdKey(requestIdKey) .WithIsCached(fileReRouteOptions.IsCached) .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds, region)) diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamReRoute.cs index cf421285..e8dfade5 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamReRoute.cs @@ -28,6 +28,7 @@ namespace Ocelot.Configuration List claimsToQueries, List claimsToHeaders, List claimsToClaims, + List claimsToPath, bool isAuthenticated, bool isAuthorised, AuthenticationOptions authenticationOptions, @@ -63,6 +64,7 @@ namespace Ocelot.Configuration ClaimsToQueries = claimsToQueries ?? new List(); ClaimsToHeaders = claimsToHeaders ?? new List(); ClaimsToClaims = claimsToClaims ?? new List(); + ClaimsToPath = claimsToPath ?? new List(); IsAuthenticated = isAuthenticated; IsAuthorised = isAuthorised; AuthenticationOptions = authenticationOptions; @@ -93,6 +95,7 @@ namespace Ocelot.Configuration public List ClaimsToQueries { get; } public List ClaimsToHeaders { get; } public List ClaimsToClaims { get; } + public List ClaimsToPath { get; } public bool IsAuthenticated { get; } public bool IsAuthorised { get; } public AuthenticationOptions AuthenticationOptions { get; } diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index bad0e2af..b15653ea 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -11,6 +11,7 @@ namespace Ocelot.Configuration.File AddClaimsToRequest = new Dictionary(); RouteClaimsRequirement = new Dictionary(); AddQueriesToRequest = new Dictionary(); + ChangeDownstreamPathTemplate = new Dictionary(); DownstreamHeaderTransform = new Dictionary(); FileCacheOptions = new FileCacheOptions(); QoSOptions = new FileQoSOptions(); @@ -34,6 +35,7 @@ namespace Ocelot.Configuration.File public Dictionary AddClaimsToRequest { get; set; } public Dictionary RouteClaimsRequirement { get; set; } public Dictionary AddQueriesToRequest { get; set; } + public Dictionary ChangeDownstreamPathTemplate { get; set; } public string RequestIdKey { get; set; } public FileCacheOptions FileCacheOptions { get; set; } public bool ReRouteIsCaseSensitive { get; set; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index c8a4cc50..3f9d3fb5 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -25,6 +25,7 @@ namespace Ocelot.DependencyInjection using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Middleware.Multiplexer; + using Ocelot.PathManipulation; using Ocelot.QueryStrings; using Ocelot.RateLimit; using Ocelot.Request.Creator; @@ -92,6 +93,7 @@ namespace Ocelot.DependencyInjection Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs b/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs new file mode 100644 index 00000000..a458992f --- /dev/null +++ b/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.PathManipulation +{ + public class ChangeDownstreamPathTemplate : IChangeDownstreamPathTemplate + { + private readonly IClaimsParser _claimsParser; + + public ChangeDownstreamPathTemplate(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response ChangeDownstreamPath(List claimsToThings, IEnumerable claims, + DownstreamPathTemplate downstreamPathTemplate, List placeholders) + { + foreach (var config in claimsToThings) + { + var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index); + + if (value.IsError) + { + return new ErrorResponse(value.Errors); + } + + var placeholderName = $"{{{config.ExistingKey}}}"; + + if (!downstreamPathTemplate.Value.Contains(placeholderName)) + { + return new ErrorResponse(new CouldNotFindPlaceholderError(placeholderName)); + } + + if (placeholders.Any(ph => ph.Name == placeholderName)) + { + placeholders.RemoveAll(ph => ph.Name == placeholderName); + } + + placeholders.Add(new PlaceholderNameAndValue(placeholderName, value.Data)); + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs b/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs new file mode 100644 index 00000000..36ed7f6d --- /dev/null +++ b/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs @@ -0,0 +1,18 @@ +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using Ocelot.Values; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; + +namespace Ocelot.PathManipulation +{ + public interface IChangeDownstreamPathTemplate + { + Response ChangeDownstreamPath(List claimsToThings, IEnumerable claims, + DownstreamPathTemplate downstreamPathTemplate, List placeholders); + } +} diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs new file mode 100644 index 00000000..47107c67 --- /dev/null +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs @@ -0,0 +1,42 @@ +using Ocelot.Logging; +using Ocelot.Middleware; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.PathManipulation.Middleware +{ + public class ClaimsToDownstreamPathMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly IChangeDownstreamPathTemplate _changeDownstreamPathTemplate; + + public ClaimsToDownstreamPathMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IChangeDownstreamPathTemplate changeDownstreamPathTemplate) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _changeDownstreamPathTemplate = changeDownstreamPathTemplate; + } + + public async Task Invoke(DownstreamContext context) + { + if (context.DownstreamReRoute.ClaimsToPath.Any()) + { + Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); + var response = _changeDownstreamPathTemplate.ChangeDownstreamPath(context.DownstreamReRoute.ClaimsToPath, context.HttpContext.User.Claims, + context.DownstreamReRoute.DownstreamPathTemplate, context.TemplatePlaceholderNameAndValues); + + if (response.IsError) + { + Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); + + SetPipelineError(context, response.Errors); + return; + } + } + + await _next.Invoke(context); + } + } +} diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs new file mode 100644 index 00000000..04fbc78d --- /dev/null +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Ocelot.Middleware.Pipeline; + +namespace Ocelot.PathManipulation.Middleware +{ + public static class ClaimsToDownstreamPathMiddlewareExtensions + { + public static IOcelotPipelineBuilder UseClaimsToDownstreamPathMiddleware(this IOcelotPipelineBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs index b75a5b1d..d52cfb0d 100644 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs @@ -7,6 +7,7 @@ using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.Errors.Middleware; using Ocelot.Headers.Middleware; using Ocelot.LoadBalancer.Middleware; +using Ocelot.PathManipulation.Middleware; using Ocelot.QueryStrings.Middleware; using Ocelot.RateLimit.Middleware; using Ocelot.Request.Middleware; @@ -118,6 +119,8 @@ namespace Ocelot.Middleware.Pipeline // Now we can run any claims to query string transformation middleware builder.UseClaimsToQueryStringMiddleware(); + builder.UseClaimsToDownstreamPathMiddleware(); + // Get the load balancer for this request builder.UseLoadBalancingMiddleware(); diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs new file mode 100644 index 00000000..5e3f667f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -0,0 +1,201 @@ +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + using IdentityServer4.AccessTokenValidation; + using IdentityServer4.Models; + using IdentityServer4.Test; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.Configuration.File; + using Shouldly; + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using TestStack.BDDfy; + + public class ClaimsToDownstreamPathTests : IDisposable + { + private IWebHost _servicebuilder; + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + private Action _options; + private string _identityServerRootUrl = "http://localhost:57888"; + private string _downstreamFinalPath; + + public ClaimsToDownstreamPathTests() + { + _steps = new Steps(); + _options = o => + { + o.Authority = _identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + } + + [Fact] + public void should_return_200_and_change_downstream_path() + { + var user = new TestUser() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/users/{userId}", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 57876, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/users", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List + { + "openid", "offline_access", "api", + }, + }, + ChangeDownstreamPathTemplate = + { + {"userId", "Claims[sub] > value[1] > |"}, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:57888", "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:57876", 200)) + .And(x => _steps.GivenIHaveAToken("http://localhost:57888")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/users")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("UserId: 1231231")) + .And(x => _downstreamFinalPath.ShouldBe("/users/1231231")) + .BDDfy(); + } + + 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 => + { + _downstreamFinalPath = context.Request.Path.Value; + + string userId = _downstreamFinalPath.Replace("/users/", string.Empty); + + var responseBody = $"UserId: {userId}"; + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() + { + new Secret + { + Value = "secret".Sha256() + } + }, + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { apiName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false + } + }) + .AddTestUsers(new List + { + user + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs new file mode 100644 index 00000000..6e6f6c12 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs @@ -0,0 +1,196 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Errors; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.PathManipulation; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; +using Ocelot.Values; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamPathManipulation +{ + public class ChangeDownstreamPathTemplateTests + { + private readonly ChangeDownstreamPathTemplate _changeDownstreamPath; + private DownstreamPathTemplate _downstreamPathTemplate; + private readonly Mock _parser; + private List _configuration; + private List _claims; + private Response _result; + private Response _claimValue; + private List _placeholderValues; + + public ChangeDownstreamPathTemplateTests() + { + _parser = new Mock(); + _changeDownstreamPath = new ChangeDownstreamPathTemplate(_parser.Object); + } + + [Fact] + public void should_change_downstream_path_request() + { + var claims = new List + { + new Claim("test", "data"), + }; + var placeHolderValues = new List(); + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("path-key", "", "", 0), + })) + .And(x => x.GivenClaims(claims)) + .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) + .And(x => x.GivenPlaceholderNameAndValues(placeHolderValues)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIChangeDownstreamPath()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenClaimDataIsContainedInPlaceHolder("{path-key}", "value")) + .BDDfy(); + } + + [Fact] + public void should_replace_existing_placeholder_value() + { + var claims = new List + { + new Claim("test", "data"), + }; + var placeHolderValues = new List + { + new PlaceholderNameAndValue ("{path-key}", "old_value"), + }; + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("path-key", "", "", 0), + })) + .And(x => x.GivenClaims(claims)) + .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) + .And(x => x.GivenPlaceholderNameAndValues(placeHolderValues)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIChangeDownstreamPath()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenClaimDataIsContainedInPlaceHolder("{path-key}", "value")) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_no_placeholder_in_downstream_path() + { + var claims = new List + { + new Claim("test", "data"), + }; + var placeHolderValues = new List(); + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("path-key", "", "", 0), + })) + .And(x => x.GivenClaims(claims)) + .And(x => x.GivenDownstreamPathTemplate("/api/test")) + .And(x => x.GivenPlaceholderNameAndValues(placeHolderValues)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIChangeDownstreamPath()) + .Then(x => x.ThenTheResultIsCouldNotFindPlaceholderError()) + .BDDfy(); + } + + [Fact] + private void should_return_error_when_claim_parser_returns_error() + { + var claims = new List + { + new Claim("test", "data"), + }; + var placeHolderValues = new List(); + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("path-key", "", "", 0), + })) + .And(x => x.GivenClaims(claims)) + .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) + .And(x => x.GivenPlaceholderNameAndValues(placeHolderValues)) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError(), + }))) + .When(x => x.WhenIChangeDownstreamPath()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void GivenAClaimToThing(List configuration) + { + _configuration = configuration; + } + + private void GivenClaims(List claims) + { + _claims = claims; + } + + private void GivenDownstreamPathTemplate(string template) + { + _downstreamPathTemplate = new DownstreamPathTemplate(template); + } + + private void GivenPlaceholderNameAndValues(List placeholders) + { + _placeholderValues = placeholders; + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => + x.GetValue(It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIChangeDownstreamPath() + { + _result = _changeDownstreamPath.ChangeDownstreamPath(_configuration, _claims, + _downstreamPathTemplate, _placeholderValues); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsCouldNotFindPlaceholderError() + { + _result.IsError.ShouldBe(true); + _result.Errors.Count.ShouldBe(1); + _result.Errors.First().ShouldBeOfType(); + } + + private void ThenTheResultIsError() + { + _result.IsError.ShouldBe(true); + } + + private void ThenClaimDataIsContainedInPlaceHolder(string name, string value) + { + var placeHolder = _placeholderValues.FirstOrDefault(ph => ph.Name == name && ph.Value == value); + placeHolder.ShouldNotBeNull(); + _placeholderValues.Count.ShouldBe(1); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs new file mode 100644 index 00000000..80003300 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.PathManipulation; +using Ocelot.PathManipulation.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using Ocelot.Values; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamPathManipulation +{ + public class ClaimsToDownstreamPathMiddlewareTests + { + private readonly Mock _changePath; + private Mock _loggerFactory; + private Mock _logger; + private ClaimsToDownstreamPathMiddleware _middleware; + private DownstreamContext _downstreamContext; + private OcelotRequestDelegate _next; + + public ClaimsToDownstreamPathMiddlewareTests() + { + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _changePath = new Mock(); + _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + _middleware = new ClaimsToDownstreamPathMiddleware(_next, _loggerFactory.Object, _changePath.Object); + } + + [Fact] + public void should_call_add_queries_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToDownstreamPath(new List + { + new ClaimToThing("UserId", "Subject", "", 0), + }) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheChangeDownstreamPathReturnsOk()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenChangeDownstreamPathIsCalledCorrectly()) + .BDDfy(); + + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + } + + private void GivenTheChangeDownstreamPathReturnsOk() + { + _changePath + .Setup(x => x.ChangeDownstreamPath( + It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny>())) + .Returns(new OkResponse()); + } + + private void ThenChangeDownstreamPathIsCalledCorrectly() + { + _changePath + .Verify(x => x.ChangeDownstreamPath( + It.IsAny>(), + It.IsAny>(), + _downstreamContext.DownstreamReRoute.DownstreamPathTemplate, + _downstreamContext.TemplatePlaceholderNameAndValues), Times.Once); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; + _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + } + + } +}