fix(infra): add explicit transaction management to TenantDbConnectionInterceptor for RLS

PostgreSQL SET LOCAL only persists within a transaction scope. Added explicit transaction creation if none exists, ensuring tenant context is properly set before queries execute. Fixes tenant isolation for multi-tenant RLS filtering.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
WorkClub Automation
2026-03-05 16:30:50 +01:00
parent 8d3ac6e64a
commit 7859e1b3cf

View File

@@ -1,3 +1,4 @@
using System.Data;
using System.Data.Common; using System.Data.Common;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -63,14 +64,40 @@ public class TenantDbConnectionInterceptor : DbCommandInterceptor
if (!string.IsNullOrWhiteSpace(tenantId) && command.Connection is NpgsqlConnection conn) if (!string.IsNullOrWhiteSpace(tenantId) && command.Connection is NpgsqlConnection conn)
{ {
// Prepend SET LOCAL to set tenant context try
// Note: EF Core creates implicit transactions for queries, so SET LOCAL will work {
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'; " + command.CommandText; // If no transaction exists AND connection is open, start one
_logger.LogInformation("SetTenantContext: Prepended SET LOCAL for tenant {TenantId}", tenantId); // SET LOCAL only persists within a transaction, so we need explicit transaction scope
if (command.Transaction == null && conn.State == ConnectionState.Open)
{
command.Transaction = (NpgsqlTransaction)conn.BeginTransaction();
_logger.LogInformation("SetTenantContext: Started transaction for tenant context");
}
// Execute SET LOCAL as a separate command on the same connection
// This ensures the session variable is properly set before the main query executes
using var setLocalCmd = conn.CreateCommand();
setLocalCmd.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
// Use the same transaction if one exists (which it should now)
if (command.Transaction is NpgsqlTransaction transaction)
{
setLocalCmd.Transaction = transaction;
}
// Execute synchronously (safe in interceptor context)
setLocalCmd.ExecuteNonQuery();
_logger.LogInformation("SetTenantContext: Executed SET LOCAL for tenant {TenantId}", tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "SetTenantContext: Failed to execute SET LOCAL for tenant {TenantId}", tenantId);
throw;
}
} }
else else
{ {
_logger.LogWarning("ReaderExecuting: No tenant context available (tenantId={TenantId})", tenantId ?? "null"); _logger.LogWarning("SetTenantContext: No tenant context available (tenantId={TenantId})", tenantId ?? "null");
} }
} }
} }