// 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(); } }