Skip to content

Commit

Permalink
Merge pull request #174 from 5G-ERA/134-dynamic-gateway-reconfiguration
Browse files Browse the repository at this point in the history
134 dynamic gateway reconfiguration
  • Loading branch information
radu-popescu committed Jun 30, 2023
2 parents 59be9a0 + d470455 commit ec8d849
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 17 deletions.
22 changes: 22 additions & 0 deletions docs/Administrator/Gateway/readme.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions src/Common/Helpers/QueueHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ public static string ConstructSwitchoverDeleteActionQueueName(string organizatio
return GetQueueName(organization, instanceName, "switchover-action-delete");
}

/// <summary>
/// Constructs the name of the queue that will be used to create a new YARP dynamic route
/// </summary>
/// <param name="organization"></param>
/// <param name="instanceName"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">When parameters are not specified or contain empty or whitespace string</exception>
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");
}


/// <summary>
/// Constructs the name of the queue that will be used to delete the YARP dynamic route
/// </summary>
/// <param name="organization"></param>
/// <param name="instanceName"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">When parameters are not specified or contain empty or whitespace string</exception>
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");
}

/// <summary>
/// Constructs the switchover deployment queue name for this specific Middleware instance
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Common/MessageContracts/GatewayAddNetAppEntryMessage.cs
Original file line number Diff line number Diff line change
@@ -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; }


}
17 changes: 17 additions & 0 deletions src/Common/MessageContracts/GatewayDeleteNetAppEntryMessage.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
84 changes: 84 additions & 0 deletions src/OcelotGateway/ExtensionMethods/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<CreateDynamicRouteConsumer>();
x.AddConsumer<CreateDynamicRouteConsumer>();
services.AddScoped<DeleteDynamicRouteConsumer>();
x.AddConsumer<DeleteDynamicRouteConsumer>();
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<CreateDynamicRouteConsumer>(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<DeleteDynamicRouteConsumer>(busRegistrationContext);
});
mqBusFactoryConfigurator.ConfigureEndpoints(busRegistrationContext);
});
});


services.AddOptions<MassTransitHostOptions>()
.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;
}
}
35 changes: 35 additions & 0 deletions src/OcelotGateway/Handlers/CreateDynamicRouteConsumer.cs
Original file line number Diff line number Diff line change
@@ -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<GatewayAddNetAppEntryMessage>
{
private readonly ILogger _logger;

private GatewayConfigurationService _gatewayConfigurationService;

private readonly IConfiguration _cfg;

public CreateDynamicRouteConsumer(ILogger<CreateDynamicRouteConsumer> logger, GatewayConfigurationService gatewayConfigurationService, IConfiguration cfg)
{
_logger = logger;
_gatewayConfigurationService = gatewayConfigurationService;
_cfg = cfg;

}


public Task Consume(ConsumeContext<GatewayAddNetAppEntryMessage> context)
{
_logger.LogInformation("Started processing GatewayAddNetAppEntryMessage");
var mwconfig = _cfg.GetSection(MiddlewareConfig.ConfigName).Get<MiddlewareConfig>();
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;
}
}
34 changes: 34 additions & 0 deletions src/OcelotGateway/Handlers/DeleteDynamicRouteConsumer.cs
Original file line number Diff line number Diff line change
@@ -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<GatewayDeleteNetAppEntryMessage>
{
private readonly ILogger _logger;

private GatewayConfigurationService _gatewayConfigurationService;

private readonly IConfiguration _cfg;

public DeleteDynamicRouteConsumer(ILogger<DeleteDynamicRouteConsumer> logger, GatewayConfigurationService gatewayConfigurationService, IConfiguration cfg)
{
_logger = logger;
_gatewayConfigurationService = gatewayConfigurationService;
_cfg = cfg;
}



public Task Consume(ConsumeContext<GatewayDeleteNetAppEntryMessage> context)
{
_logger.LogInformation("Started processing GatewayDeleteNetAppEntryMessage");
var mwconfig = _cfg.GetSection(MiddlewareConfig.ConfigName).Get<MiddlewareConfig>();
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;
}
}
3 changes: 3 additions & 0 deletions src/OcelotGateway/OcelotGateway.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="MassTransit" Version="8.0.16" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" />
<PackageReference Include="Microsoft.IdentityModel" Version="7.0.0" />
Expand All @@ -23,6 +25,7 @@
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.28.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
37 changes: 20 additions & 17 deletions src/OcelotGateway/Program.cs
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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<JwtConfig>();
builder.Services.Configure<JwtConfig>(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,
Expand All @@ -60,6 +55,7 @@
builder.RegisterRedis();

builder.Services.AddScoped<IUserRepository, RedisUserRepository>();
builder.Services.AddScoped<GatewayConfigurationService>();

var ocelotConfig = new OcelotPipelineConfiguration
{
Expand All @@ -69,15 +65,22 @@
}
};

builder.Services.AddReverseProxy()
.LoadFromMemory(new List<RouteConfig>(), new List<ClusterConfig>());


var app = builder.Build();

app.MapReverseProxy();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
//endpoints.MapReverseProxy();
});

await app.UseOcelot(ocelotConfig);
Expand Down
Loading

0 comments on commit ec8d849

Please sign in to comment.