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);
+ }
+}