Files
pos-system/services/order-service-net/src/OrderService.API/Middleware/TenantMiddleware.cs
Ho Ngoc Hai 653322b26c fix: resolve 12 critical/high issues from code review across backend, frontend, and infra
Backend (7 fixes):
- wallet-service: remove conflicting EF Ignore() calls for mapped backing fields
- fnb-engine: remove KitchenTicket short constructor that set productId=orderItemId
- fnb-engine: replace fire-and-forget Task.Run with direct await for inventory deduction
- TenantMiddleware: implement PostgreSQL RLS SET LOCAL in 4 services (wallet, fnb, inventory, catalog)
- order-service: fix SQL injection pattern in TenantMiddleware with Guid.ToString("D")
- order-service: add ValidateShopAccess() authorization check in SignalR PosHub
- 4 services: register IDbConnection (NpgsqlConnection) in DI for RLS middleware

Frontend (3 fixes):
- PosDataService: return Success=false (not true) when PayOrder response parsing fails
- QrPayment: add _disposed guard to prevent timer race condition after component disposal
- BFF OrderController: add [Authorize] attribute to require JWT for all endpoints

Infrastructure (3 fixes):
- docker-compose: upgrade PostgreSQL 15-alpine to 16-alpine per project spec
- init-databases.sh: add 4 missing marketing service databases (mkt_*)
- Traefik routes: add wallet, catalog, booking routers and /api/v1/stock path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:22:08 +07:00

113 lines
4.4 KiB
C#

// 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;
/// <summary>
/// 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.
/// </summary>
public class TenantMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TenantMiddleware> _logger;
public TenantMiddleware(RequestDelegate next, ILogger<TenantMiddleware> 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);
}
/// <summary>
/// EN: Set PostgreSQL session variables for RLS enforcement.
/// VI: Đặt biến session PostgreSQL cho RLS enforcement.
/// </summary>
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");
}
}
}
/// <summary>
/// EN: Extension method for registering TenantMiddleware.
/// VI: Extension method để đăng ký TenantMiddleware.
/// </summary>
public static class TenantMiddlewareExtensions
{
/// <summary>
/// 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().
/// </summary>
public static IApplicationBuilder UseTenantMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TenantMiddleware>();
}
}