Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

134 dynamic gateway reconfiguration #174

Merged
merged 14 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading