diff --git a/build.bat b/build.bat index 037ad2b4..a7866f1d 100644 --- a/build.bat +++ b/build.bat @@ -4,5 +4,7 @@ echo Building Ocelot dotnet restore src/Ocelot dotnet build src/Ocelot dotnet publish src/Ocelot -o artifacts/ +dotnet pack src/Ocelot/project.json --output nupkgs/ + diff --git a/src/Ocelot/Authorisation/AuthorisationMiddleware.cs b/src/Ocelot/Authorisation/AuthorisationMiddleware.cs new file mode 100644 index 00000000..6f3d54e1 --- /dev/null +++ b/src/Ocelot/Authorisation/AuthorisationMiddleware.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Errors; +using Ocelot.Middleware; +using Ocelot.ScopedData; + +namespace Ocelot.Authorisation +{ + public class AuthorisationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IScopedRequestDataRepository _scopedRequestDataRepository; + private readonly IAuthoriser _authoriser; + + public AuthorisationMiddleware(RequestDelegate next, + IScopedRequestDataRepository scopedRequestDataRepository, + IAuthoriser authoriser) + : base(scopedRequestDataRepository) + { + _next = next; + _scopedRequestDataRepository = scopedRequestDataRepository; + _authoriser = authoriser; + } + + public async Task Invoke(HttpContext context) + { + var downstreamRoute = _scopedRequestDataRepository.Get("DownstreamRoute"); + + if (downstreamRoute.IsError) + { + SetPipelineError(downstreamRoute.Errors); + return; + } + + var authorised = _authoriser.Authorise(context.User, new RouteClaimsRequirement()); + + if (authorised) + { + await _next.Invoke(context); + } + else + { + //set an error + SetPipelineError(new List + { + new UnauthorisedError($"{context.User.Identity.Name} unable to access {downstreamRoute.Data.ReRoute.UpstreamTemplate}") + }); + } + } + } +} diff --git a/src/Ocelot/Authorisation/AuthorisationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authorisation/AuthorisationMiddlewareMiddlewareExtensions.cs new file mode 100644 index 00000000..291a47ad --- /dev/null +++ b/src/Ocelot/Authorisation/AuthorisationMiddlewareMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Authorisation +{ + public static class AuthorisationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthorisationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs new file mode 100644 index 00000000..631767df --- /dev/null +++ b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs @@ -0,0 +1,12 @@ +using System.Security.Claims; + +namespace Ocelot.Authorisation +{ + public class ClaimsAuthoriser : IAuthoriser + { + public bool Authorise(ClaimsPrincipal claimsPrincipal, RouteClaimsRequirement routeClaimsRequirement) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authorisation/IAuthoriser.cs b/src/Ocelot/Authorisation/IAuthoriser.cs new file mode 100644 index 00000000..091eea3f --- /dev/null +++ b/src/Ocelot/Authorisation/IAuthoriser.cs @@ -0,0 +1,10 @@ +using System.Security.Claims; + +namespace Ocelot.Authorisation +{ + public interface IAuthoriser + { + bool Authorise(ClaimsPrincipal claimsPrincipal, + RouteClaimsRequirement routeClaimsRequirement); + } +} diff --git a/src/Ocelot/Authorisation/RouteClaimsRequirement.cs b/src/Ocelot/Authorisation/RouteClaimsRequirement.cs new file mode 100644 index 00000000..bca35bc9 --- /dev/null +++ b/src/Ocelot/Authorisation/RouteClaimsRequirement.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Authorisation +{ + public class RouteClaimsRequirement + { + } +} diff --git a/src/Ocelot/Authorisation/UnauthorisedError.cs b/src/Ocelot/Authorisation/UnauthorisedError.cs new file mode 100644 index 00000000..766c1a7b --- /dev/null +++ b/src/Ocelot/Authorisation/UnauthorisedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorisation +{ + public class UnauthorisedError : Error + { + public UnauthorisedError(string message) + : base(message, OcelotErrorCode.UnauthorizedError) + { + } + } +} diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 38dad7ba..20803dcb 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,8 +1,10 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Authentication.Handler.Creator; using Ocelot.Authentication.Handler.Factory; +using Ocelot.Authorisation; using Ocelot.Configuration.Creator; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Provider; @@ -45,6 +47,7 @@ namespace Ocelot.DependencyInjection services.AddLogging(); // ocelot services. + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index 28679449..ed15ce2e 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -14,6 +14,7 @@ CannotFindClaimError, ParsingConfigurationHeaderError, NoInstructionsError, - InstructionNotForClaimsError + InstructionNotForClaimsError, + UnauthorizedError } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 148dc8fe..515ac74f 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Ocelot.Authentication.Middleware; +using Ocelot.Authorisation; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.HeaderBuilder.Middleware; @@ -19,6 +20,8 @@ namespace Ocelot.Middleware builder.UseAuthenticationMiddleware(); + //builder.UseAuthorisationMiddleware(); + builder.UseHttpRequestHeadersBuilderMiddleware(); builder.UseDownstreamUrlCreatorMiddleware(); diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs new file mode 100644 index 00000000..b9d1746b --- /dev/null +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Authorisation; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Ocelot.ScopedData; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Authorization +{ + public class AuthorizationMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _authService; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _downstreamRoute; + + public AuthorizationMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _authService = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(_authService.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseAuthorisationMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void happy_path() + { + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().Build()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheAuthServiceIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheAuthServiceIsCalledCorrectly() + { + _authService + .Verify(x => x.Authorise(It.IsAny(), + It.IsAny()), Times.Once); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +}