Skip to content

Commit cc3e010

Browse files
Updated Redis session configuration to use subdomain as instance name, ensuring tenant-specific session isolation in a multi-tenant environment
1 parent f0cc294 commit cc3e010

File tree

3 files changed

+106
-7
lines changed

3 files changed

+106
-7
lines changed

dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ public class GXSessionServiceFactory
1212
{
1313
private static readonly IGXLogger log = GXLoggerFactory.GetLogger<GXSessionServiceFactory>();
1414

15+
static ISessionService sessionService;
1516
static string REDIS = "REDIS";
1617
static string DATABASE = "DATABASE";
1718
public static ISessionService GetProvider()
1819
{
19-
ISessionService sessionService = null;
20+
if (sessionService != null)
21+
return sessionService;
2022
GXService providerService = GXServices.Instance?.Get(GXServices.SESSION_SERVICE);
2123
if (providerService != null)
2224
{
@@ -63,6 +65,7 @@ public class GxRedisSession : ISessionService
6365
internal static string SESSION_INSTANCE = "SESSION_PROVIDER_INSTANCE_NAME";
6466
internal static string SESSION_PASSWORD = "SESSION_PROVIDER_PASSWORD";
6567
static string SESSION_TIMEOUT = "SESSION_PROVIDER_SESSION_TIMEOUT";
68+
const string SUBDOMAIN = "%SUBDOMAIN%";
6669
public GxRedisSession(GXService serviceProvider)
6770
{
6871
string password = serviceProvider.Properties.Get(SESSION_PASSWORD);
@@ -100,6 +103,10 @@ public GxRedisSession(string host, string password, string instanceName, int ses
100103
GXLogging.Debug(log, "Redis Host:", host, ", InstanceName:", instanceName);
101104
GXLogging.Debug(log, "Redis sessionTimeoutMinutes:", sessionTimeout.ToString());
102105
}
106+
internal bool IsMultitenant
107+
{
108+
get { return InstanceName == SUBDOMAIN; }
109+
}
103110
public string ConnectionString { get; }
104111
public string InstanceName { get; }
105112
public int SessionTimeout { get; }
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using Microsoft.Extensions.Caching.StackExchangeRedis;
4+
using System.Collections.Concurrent;
5+
using System.Threading.Tasks;
6+
using System.Threading;
7+
using System;
8+
using GeneXus.Services;
9+
using System.Linq;
10+
11+
namespace GeneXus.Application
12+
{
13+
public class TenantRedisCache : IDistributedCache
14+
{
15+
private readonly IHttpContextAccessor _httpContextAccessor;
16+
private readonly IServiceProvider _serviceProvider;
17+
private readonly ConcurrentDictionary<string, RedisCache> _redisCaches = new();
18+
19+
public TenantRedisCache(IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider)
20+
{
21+
_httpContextAccessor = httpContextAccessor;
22+
_serviceProvider = serviceProvider;
23+
}
24+
25+
private IDistributedCache GetTenantCache()
26+
{
27+
string tenantId = _httpContextAccessor.HttpContext?.Items[TenantMiddleware.TENANT_ID]?.ToString() ?? "default";
28+
29+
return _redisCaches.GetOrAdd(tenantId, id =>
30+
{
31+
ISessionService sessionService = GXSessionServiceFactory.GetProvider();
32+
var options = new RedisCacheOptions
33+
{
34+
Configuration = sessionService.ConnectionString,
35+
InstanceName = $"{id}:"
36+
};
37+
return new RedisCache(options);
38+
});
39+
}
40+
41+
public byte[] Get(string key) => GetTenantCache().Get(key);
42+
public Task<byte[]> GetAsync(string key, CancellationToken token = default) => GetTenantCache().GetAsync(key, token);
43+
public void Refresh(string key) => GetTenantCache().Refresh(key);
44+
public Task RefreshAsync(string key, CancellationToken token = default) => GetTenantCache().RefreshAsync(key, token);
45+
public void Remove(string key) => GetTenantCache().Remove(key);
46+
public Task RemoveAsync(string key, CancellationToken token = default) => GetTenantCache().RemoveAsync(key, token);
47+
public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => GetTenantCache().Set(key, value, options);
48+
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => GetTenantCache().SetAsync(key, value, options, token);
49+
}
50+
51+
52+
public class TenantMiddleware
53+
{
54+
internal const string TENANT_ID = "TenantId";
55+
private readonly RequestDelegate _next;
56+
57+
public TenantMiddleware(RequestDelegate next)
58+
{
59+
_next = next;
60+
}
61+
62+
public async Task Invoke(HttpContext context)
63+
{
64+
string host = context.Request.Host.Host;
65+
string subdomain = host.Split('.').FirstOrDefault();
66+
context.Items[TENANT_ID] = subdomain;
67+
68+
await _next(context);
69+
}
70+
}
71+
}

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using Microsoft.AspNetCore.Routing;
2929
using Microsoft.AspNetCore.Server.Kestrel.Core;
3030
using Microsoft.AspNetCore.StaticFiles;
31+
using Microsoft.Extensions.Caching.Distributed;
3132
using Microsoft.Extensions.Configuration;
3233
using Microsoft.Extensions.DependencyInjection;
3334
using Microsoft.Extensions.FileProviders;
@@ -436,15 +437,27 @@ private void DefineCorsPolicy(IServiceCollection services)
436437

437438
private void ConfigureSessionService(IServiceCollection services, ISessionService sessionService)
438439
{
440+
439441
if (sessionService is GxRedisSession)
440442
{
441-
services.AddStackExchangeRedisCache(options =>
443+
GxRedisSession gxRedisSession = (GxRedisSession)sessionService;
444+
if (gxRedisSession.IsMultitenant)
442445
{
443-
GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}");
444-
options.Configuration = sessionService.ConnectionString;
445-
options.InstanceName = sessionService.InstanceName;
446-
});
447-
services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName(sessionService.InstanceName);
446+
GXLogging.Info(log, $"Using multi-tenant Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}");
447+
448+
services.AddSingleton<IDistributedCache, TenantRedisCache>();
449+
services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName("default");
450+
}
451+
else
452+
{
453+
services.AddStackExchangeRedisCache(options =>
454+
{
455+
GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}");
456+
options.Configuration = sessionService.ConnectionString;
457+
options.InstanceName = sessionService.InstanceName;
458+
});
459+
services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName(sessionService.InstanceName);
460+
}
448461
}
449462
else if (sessionService is GxDatabaseSession)
450463
{
@@ -491,6 +504,14 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
491504
app.UseCookiePolicy();
492505
app.UseSession();
493506
app.UseStaticFiles();
507+
508+
ISessionService sessionService = GXSessionServiceFactory.GetProvider();
509+
GxRedisSession gxRedisSession = sessionService as GxRedisSession;
510+
if (gxRedisSession != null && gxRedisSession.IsMultitenant)
511+
{
512+
app.UseMiddleware<TenantMiddleware>();
513+
}
514+
494515
ConfigureCors(app);
495516
ConfigureSwaggerUI(app, baseVirtualPath);
496517

0 commit comments

Comments
 (0)