diff --git a/docs/Administrator/Gateway/readme.md b/docs/Administrator/Gateway/readme.md new file mode 100644 index 00000000..6d1ea653 --- /dev/null +++ b/docs/Administrator/Gateway/readme.md @@ -0,0 +1,22 @@ +# Gateway + +The `Middleware` system includes diverse microservices. When a `robot/user` has to consume multiple microservices, setting up distinct endpoints for each microservice and managing them separately can be challenging. A solution for handling such tasks is placing a gateway in front of the microservices. In this way, the `robot/user` can communicate with the various microservices by using a single entry-point. + +## What technologies have been used and for what purposes + +The gateway for the `5G-ERA Middleware` has been implemented using the `Ocelot` and `YARP` reverse proxy server. `Ocelot` is an open-source application, which is lightweight, fast, scalable, cross-platform, and most importantly it was specifically designed for `.NET Core` Microservices Architecture. `Ocelot` is responsible for handling the incoming requests, by routing the traffic to the desired endpoints, retrieving the information from the backend, and relaying it back to the `robots/users`. On the other hand, `YARP` is as well an open source application built on top of the `.NET` environment, being designed to be easily customizable and tweaked. `YARP` will accommodate the functionalities for dynamic gateway configuration and websocket for robotic communication purposes. + +# Gateway functionalities + +While both technologies serve the common purpose for routing the incoming requests, their distinct functionalities are described below. + +## Ocelot authentication/authorization and RBAC + +The `Ocelot` also fulfils the role of an `Identity Service`, through a `REST API` implementation. The user credentials are stored in a safe manner using a `HASH + SALT` model, following current best practices and using cryptographically strong libraries from `.NET` environment. The passwords are SALT-ed and hashed using the `PBKDF2` algorithm with `HMACSHA256` hashing. Authentication is achieved by reconstructing the salted hash from the credentials provided at log-in and comparing the result with the salted hash stored in the system at registration. +Furthermore, the security of the `Middleware` systems is enhanced using the `JWT Bearer Token` standard and `Role Based Access Control` (`RBAC`) model. More precisely when the `robots/users` are successfully authenticated they will receive a `JWT Token`. In terms of authorization, the `robots/users` will have to pass the generated token along with the request in order to be able to perform operations on the data, through the endpoints that are implemented in the `Middleware` system. Finally, the security of the system is boosted with the implementation of the `RBAC` model that will provide restricted access to the `Middleware` system based on the role of the user. + +## YARP dynamic gateway configuration and WebSockets + +The `Middleware` system offers the possibility to dynamically configure the `Gateway` in order to route the traffic through a websocket to a specific `Network Application` using the `YARP` reverse proxy functionalities. This allows enabling `RBAC` to the `Network Applications` and expose everything behind the `Gateway` using `SSL` termination. Moreover, this will also remove the need to enable the authentication on the `Network Application` level. +The dynamic `Gateway` configuration is accomplished through standard `REST` requests and `WebSockets`, in fact, the whole mechanism is triggered through `RabbitMQ` queueing system for both creating the new route and deleting the Route once the `Network Application` has finished conducting the task. +The messages to open or close the route are sent from the Orchestrator after the desired `Network Application` is deployed or terminated. \ No newline at end of file diff --git a/src/Common/Helpers/QueueHelpers.cs b/src/Common/Helpers/QueueHelpers.cs index c89b2438..46d8da21 100644 --- a/src/Common/Helpers/QueueHelpers.cs +++ b/src/Common/Helpers/QueueHelpers.cs @@ -54,6 +54,37 @@ public static string ConstructSwitchoverDeleteActionQueueName(string organizatio return GetQueueName(organization, instanceName, "switchover-action-delete"); } + /// + /// Constructs the name of the queue that will be used to create a new YARP dynamic route + /// + /// + /// + /// + /// When parameters are not specified or contain empty or whitespace string + public static string ConstructGatewayAddNetAppEntryMessageQueueName(string organization, string instanceName) + { + if (string.IsNullOrWhiteSpace(organization)) throw new ArgumentNullException(nameof(organization)); + if (string.IsNullOrWhiteSpace(organization)) throw new ArgumentNullException(nameof(instanceName)); + + return GetQueueName(organization, instanceName, "gateway-add-entry"); + } + + + /// + /// Constructs the name of the queue that will be used to delete the YARP dynamic route + /// + /// + /// + /// + /// When parameters are not specified or contain empty or whitespace string + public static string ConstructGatewayDeleteNetAppEntryMessageQueueName(string organization, string instanceName) + { + if (string.IsNullOrWhiteSpace(organization)) throw new ArgumentNullException(nameof(organization)); + if (string.IsNullOrWhiteSpace(organization)) throw new ArgumentNullException(nameof(instanceName)); + + return GetQueueName(organization, instanceName, "gateway-delete-entry"); + } + /// /// Constructs the switchover deployment queue name for this specific Middleware instance /// diff --git a/src/Common/MessageContracts/GatewayAddNetAppEntryMessage.cs b/src/Common/MessageContracts/GatewayAddNetAppEntryMessage.cs new file mode 100644 index 00000000..fd97c462 --- /dev/null +++ b/src/Common/MessageContracts/GatewayAddNetAppEntryMessage.cs @@ -0,0 +1,13 @@ +namespace Middleware.Common.MessageContracts; +public record GatewayAddNetAppEntryMessage : Message +{ + public Guid ActionPlanId { get; set; } + + public Guid ServiceInstanceId { get; set; } + + public string NetAppName { get; set; } + + public string DeploymentLocation { get; init; } + + +} diff --git a/src/Common/MessageContracts/GatewayDeleteNetAppEntryMessage.cs b/src/Common/MessageContracts/GatewayDeleteNetAppEntryMessage.cs new file mode 100644 index 00000000..0846a987 --- /dev/null +++ b/src/Common/MessageContracts/GatewayDeleteNetAppEntryMessage.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Middleware.Common.MessageContracts; +public record GatewayDeleteNetAppEntryMessage : Message +{ + public Guid ActionPlanId { get; set; } + + public Guid ServiceInstanceId { get; set; } + + public string NetAppName { get; set; } + + public string DeploymentLocation { get; init; } +} diff --git a/src/OcelotGateway/ExtensionMethods/ServiceCollectionExtensions.cs b/src/OcelotGateway/ExtensionMethods/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..4232d4c0 --- /dev/null +++ b/src/OcelotGateway/ExtensionMethods/ServiceCollectionExtensions.cs @@ -0,0 +1,84 @@ +using MassTransit; +using Middleware.Common.Config; +using Middleware.Common.Helpers; +using Middleware.Common.MessageContracts; +using Middleware.OcelotGateway.Handlers; +using Middleware.OcelotGateway.Services; +using RabbitMQ.Client; + +namespace Middleware.OcelotGateway.ExtensionMethods; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection RegisterRabbitMqConsumers(this IServiceCollection services, + RabbitMqConfig mqConfig, MiddlewareConfig mwConfig) + { + var routingKey = QueueHelpers.ConstructRoutingKey(mwConfig.InstanceName, mwConfig.InstanceType); + + services.AddMassTransit(x => + { + services.AddScoped(); + x.AddConsumer(); + + services.AddScoped(); + x.AddConsumer(); + + x.UsingRabbitMq((busRegistrationContext, mqBusFactoryConfigurator) => + { + mqBusFactoryConfigurator.Host(mqConfig.Address, "/", hostConfig => + { + hostConfig.Username(mqConfig.User); + hostConfig.Password(mqConfig.Pass); + }); + + mqBusFactoryConfigurator.ReceiveEndpoint( + QueueHelpers.ConstructGatewayAddNetAppEntryMessageQueueName(mwConfig.Organization, mwConfig.InstanceName), + ec => + { + ec.ConfigureConsumeTopology = false; + ec.Bind(nameof(GatewayAddNetAppEntryMessage), b => + { + b.ExchangeType = ExchangeType.Direct; + b.RoutingKey = routingKey; + }); + ec.ConfigureConsumer(busRegistrationContext); + }); + + mqBusFactoryConfigurator.ReceiveEndpoint( + QueueHelpers.ConstructGatewayDeleteNetAppEntryMessageQueueName(mwConfig.Organization, mwConfig.InstanceName), + ec => + { + ec.ConfigureConsumeTopology = false; + ec.Bind(nameof(GatewayDeleteNetAppEntryMessage), b => + { + b.ExchangeType = ExchangeType.Direct; + b.RoutingKey = routingKey; + }); + ec.ConfigureConsumer(busRegistrationContext); + }); + + mqBusFactoryConfigurator.ConfigureEndpoints(busRegistrationContext); + }); + + + }); + + + services.AddOptions() + .Configure(options => + { + // if specified, waits until the bus is started before + // returning from IHostedService.StartAsync + // default is false + options.WaitUntilStarted = true; + + // if specified, limits the wait time when starting the bus + options.StartTimeout = TimeSpan.FromSeconds(10); + + // if specified, limits the wait time when stopping the bus + options.StopTimeout = TimeSpan.FromSeconds(30); + }); + + return services; + } +} diff --git a/src/OcelotGateway/Handlers/CreateDynamicRouteConsumer.cs b/src/OcelotGateway/Handlers/CreateDynamicRouteConsumer.cs new file mode 100644 index 00000000..c5d42830 --- /dev/null +++ b/src/OcelotGateway/Handlers/CreateDynamicRouteConsumer.cs @@ -0,0 +1,35 @@ +using Middleware.Common.MessageContracts; +using MassTransit; +using Middleware.OcelotGateway.Services; +using Middleware.Common.Config; +using System.Numerics; + +namespace Middleware.OcelotGateway.Handlers; + +public class CreateDynamicRouteConsumer : IConsumer +{ + private readonly ILogger _logger; + + private GatewayConfigurationService _gatewayConfigurationService; + + private readonly IConfiguration _cfg; + + public CreateDynamicRouteConsumer(ILogger logger, GatewayConfigurationService gatewayConfigurationService, IConfiguration cfg) + { + _logger = logger; + _gatewayConfigurationService = gatewayConfigurationService; + _cfg = cfg; + + } + + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Started processing GatewayAddNetAppEntryMessage"); + var mwconfig = _cfg.GetSection(MiddlewareConfig.ConfigName).Get(); + var msg = context.Message; + _logger.LogDebug("Location {0}-{1} received message request addressed to {2}", mwconfig.InstanceName, mwconfig.InstanceType, msg.DeploymentLocation); + _gatewayConfigurationService.CreateDynamicRoute(msg); + return Task.CompletedTask; + } +} diff --git a/src/OcelotGateway/Handlers/DeleteDynamicRouteConsumer.cs b/src/OcelotGateway/Handlers/DeleteDynamicRouteConsumer.cs new file mode 100644 index 00000000..d02e42b8 --- /dev/null +++ b/src/OcelotGateway/Handlers/DeleteDynamicRouteConsumer.cs @@ -0,0 +1,34 @@ +using MassTransit; +using Middleware.Common.Config; +using Middleware.Common.MessageContracts; +using Middleware.OcelotGateway.Services; + +namespace Middleware.OcelotGateway.Handlers; + +public class DeleteDynamicRouteConsumer : IConsumer +{ + private readonly ILogger _logger; + + private GatewayConfigurationService _gatewayConfigurationService; + + private readonly IConfiguration _cfg; + + public DeleteDynamicRouteConsumer(ILogger logger, GatewayConfigurationService gatewayConfigurationService, IConfiguration cfg) + { + _logger = logger; + _gatewayConfigurationService = gatewayConfigurationService; + _cfg = cfg; + } + + + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Started processing GatewayDeleteNetAppEntryMessage"); + var mwconfig = _cfg.GetSection(MiddlewareConfig.ConfigName).Get(); + var msg = context.Message; + _logger.LogDebug("Location {0}-{1} received message request addressed to {2}", mwconfig.InstanceName, mwconfig.InstanceType, msg.DeploymentLocation); + _gatewayConfigurationService.DeleteDynamicRoute(msg); + return Task.CompletedTask; + } +} diff --git a/src/OcelotGateway/OcelotGateway.csproj b/src/OcelotGateway/OcelotGateway.csproj index 2ed2ee3e..060f856e 100644 --- a/src/OcelotGateway/OcelotGateway.csproj +++ b/src/OcelotGateway/OcelotGateway.csproj @@ -14,6 +14,8 @@ + + @@ -23,6 +25,7 @@ + diff --git a/src/OcelotGateway/Program.cs b/src/OcelotGateway/Program.cs index 44844163..2951c682 100644 --- a/src/OcelotGateway/Program.cs +++ b/src/OcelotGateway/Program.cs @@ -1,22 +1,17 @@ -using System.Configuration; using System.Text; +using IdentityModel; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Middleware.Common.Config; using Middleware.Common.ExtensionMethods; using Middleware.DataAccess.ExtensionMethods; using Middleware.DataAccess.Repositories; using Middleware.DataAccess.Repositories.Abstract; +using Middleware.OcelotGateway.Services; using Ocelot.Cache.CacheManager; using Ocelot.DependencyInjection; using Ocelot.Middleware; -using Ocelot.Values; -using Microsoft.IdentityModel; -using static IdentityModel.ClaimComparer; -using Microsoft.IdentityModel.Claims; -using Ocelot.Authentication.Middleware; -using Middleware.OcelotGateway.Services; +using Yarp.ReverseProxy.Configuration; var builder = WebApplication.CreateBuilder(args); @@ -30,25 +25,25 @@ }); builder.Services.AddOcelot() - .AddCacheManager(settings => settings.WithDictionaryHandle()); + .AddCacheManager(settings => settings.WithDictionaryHandle()); builder.Services.DecorateClaimAuthoriser(); var config = builder.Configuration.GetSection(JwtConfig.ConfigName).Get(); builder.Services.Configure(builder.Configuration.GetSection(JwtConfig.ConfigName)); builder.Services.AddAuthentication(options => -{ - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -} + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + } ).AddJwtBearer("Bearer", options => { - options.TokenValidationParameters = new TokenValidationParameters() + options.TokenValidationParameters = new() { IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.Key)), - NameClaimType = IdentityModel.JwtClaimTypes.Name, - RoleClaimType = IdentityModel.JwtClaimTypes.Role, + NameClaimType = JwtClaimTypes.Name, + RoleClaimType = JwtClaimTypes.Role, ValidAudience = "redisinterfaceAudience", ValidIssuer = "redisinterfaceIssuer", ValidateIssuerSigningKey = true, @@ -60,6 +55,7 @@ builder.RegisterRedis(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var ocelotConfig = new OcelotPipelineConfiguration { @@ -69,8 +65,14 @@ } }; +builder.Services.AddReverseProxy() + .LoadFromMemory(new List(), new List()); + + var app = builder.Build(); +app.MapReverseProxy(); + app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); @@ -78,6 +80,7 @@ app.UseEndpoints(endpoints => { endpoints.MapControllers(); + //endpoints.MapReverseProxy(); }); await app.UseOcelot(ocelotConfig); diff --git a/src/OcelotGateway/Services/GatewayConfigurationService.cs b/src/OcelotGateway/Services/GatewayConfigurationService.cs new file mode 100644 index 00000000..7f0d3cda --- /dev/null +++ b/src/OcelotGateway/Services/GatewayConfigurationService.cs @@ -0,0 +1,86 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Middleware.Common.MessageContracts; +using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.Transforms; + +namespace Middleware.OcelotGateway.Services; + +public class GatewayConfigurationService +{ + private readonly InMemoryConfigProvider _inMemoryConfigProvider; + + public GatewayConfigurationService(IProxyConfigProvider inMemoryConfigProvider) + { + if (inMemoryConfigProvider is InMemoryConfigProvider imcp) + _inMemoryConfigProvider = imcp; + } + + public void CreateDynamicRoute(GatewayAddNetAppEntryMessage msg) + { + var config = _inMemoryConfigProvider.GetConfig(); + + var clusterList = config.Clusters.ToList(); + var routeList = config.Routes.ToList(); + + var clusterCfg = new ClusterConfig + { + ClusterId = msg.NetAppName + "-Cluster", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig { Address = $"http://{msg.NetAppName}" } } + } + }; + var routeCfg = new RouteConfig + { + RouteId = msg.NetAppName + "-Route", + Match = new() + { + Path = msg.NetAppName + }, + ClusterId = clusterCfg.ClusterId // + }; + // transforms allow us to change the path that is requested like below to replace direct forwarding + routeCfg = routeCfg.WithTransformPathRemovePrefix($"/{msg.NetAppName}"); + + clusterList.Add(clusterCfg); + routeList.Add(routeCfg); + + _inMemoryConfigProvider.Update(routeList, clusterList); + + } + + public void DeleteDynamicRoute(GatewayDeleteNetAppEntryMessage msg) + { + var config = _inMemoryConfigProvider.GetConfig(); + + var clusterList = config.Clusters.ToList(); + var routeList = config.Routes.ToList(); + + var matchRouteToDelete = msg.NetAppName + "-Route"; + RouteConfig routeToDelete = null; + foreach (var route in routeList) + { + + if (route.RouteId == matchRouteToDelete) + { + routeToDelete = route; + } + } + routeList.Remove(routeToDelete); + + var matchClusterToDelete = msg.NetAppName + "-Cluster"; + ClusterConfig clusterToDelete = null; + foreach (var cluster in clusterList) + { + + if (cluster.ClusterId == matchClusterToDelete) + { + clusterToDelete = cluster; + } + } + clusterList.Remove(clusterToDelete); + + _inMemoryConfigProvider.Update(routeList, clusterList); + } +}