// EN: Middleware to set PostgreSQL session variable for RLS policies.
// VI: Middleware để đặt biến session PostgreSQL cho RLS policies.
using System.Data;
using Npgsql;
using OrderService.API.Infrastructure.Tenant;
namespace OrderService.API.Middleware;
///
/// EN: Sets PostgreSQL session variables (app.current_tenant_id) for row-level security.
/// This provides defense-in-depth: even if EF Core global query filters are bypassed
/// (e.g., via raw SQL/Dapper), PostgreSQL RLS policies will still enforce tenant isolation.
/// VI: Đặt biến session PostgreSQL (app.current_tenant_id) cho bảo mật row-level.
/// Điều này cung cấp phòng thủ theo chiều sâu: ngay cả khi global query filters của EF Core
/// bị bỏ qua (ví dụ: qua raw SQL/Dapper), RLS policies của PostgreSQL vẫn đảm bảo cách ly tenant.
///
public class TenantMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public TenantMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, ITenantProvider tenantProvider, IDbConnection dbConnection)
{
var shopId = tenantProvider.GetCurrentShopId();
var merchantId = tenantProvider.GetCurrentMerchantId();
var isServiceCall = tenantProvider.IsServiceCall();
var isAdmin = tenantProvider.IsAdmin();
// EN: Skip tenant SET for service-to-service calls and admin users
// VI: Bỏ qua tenant SET cho cuộc gọi service-to-service và admin users
if (!isServiceCall && !isAdmin)
{
if (shopId.HasValue)
{
await SetTenantContextAsync(dbConnection, shopId.Value, merchantId);
}
else
{
_logger.LogDebug(
"EN: No tenant context available for request {Path} / " +
"VI: Không có tenant context cho request {Path}",
context.Request.Path);
}
}
await _next(context);
}
///
/// EN: Set PostgreSQL session variables for RLS enforcement.
/// VI: Đặt biến session PostgreSQL cho RLS enforcement.
///
private async Task SetTenantContextAsync(IDbConnection dbConnection, Guid shopId, Guid? merchantId)
{
try
{
if (dbConnection is NpgsqlConnection npgsqlConnection)
{
if (npgsqlConnection.State != ConnectionState.Open)
{
await npgsqlConnection.OpenAsync();
}
// EN: Set shop_id as the primary tenant identifier
// VI: Đặt shop_id làm tenant identifier chính
await using var cmd = npgsqlConnection.CreateCommand();
cmd.CommandText = $"SET LOCAL app.current_shop_id = '{shopId.ToString("D")}'";
await cmd.ExecuteNonQueryAsync();
if (merchantId.HasValue)
{
cmd.CommandText = $"SET LOCAL app.current_merchant_id = '{merchantId.Value.ToString("D")}'";
await cmd.ExecuteNonQueryAsync();
}
_logger.LogDebug(
"EN: Tenant context set - ShopId: {ShopId}, MerchantId: {MerchantId} / " +
"VI: Tenant context đã đặt - ShopId: {ShopId}, MerchantId: {MerchantId}",
shopId, merchantId);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"EN: Failed to set PostgreSQL tenant context / " +
"VI: Không thể đặt PostgreSQL tenant context");
}
}
}
///
/// EN: Extension method for registering TenantMiddleware.
/// VI: Extension method để đăng ký TenantMiddleware.
///
public static class TenantMiddlewareExtensions
{
///
/// EN: Use tenant middleware for PostgreSQL RLS. Must be placed after UseAuthentication().
/// VI: Sử dụng tenant middleware cho PostgreSQL RLS. Phải đặt sau UseAuthentication().
///
public static IApplicationBuilder UseTenantMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}