From 7859e1b3cf879899c875f01d8495856003a3d6ed Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Thu, 5 Mar 2026 16:30:50 +0100 Subject: [PATCH] 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 --- .../TenantDbConnectionInterceptor.cs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/backend/WorkClub.Infrastructure/Data/Interceptors/TenantDbConnectionInterceptor.cs b/backend/WorkClub.Infrastructure/Data/Interceptors/TenantDbConnectionInterceptor.cs index b7e8623..4acce9c 100644 --- a/backend/WorkClub.Infrastructure/Data/Interceptors/TenantDbConnectionInterceptor.cs +++ b/backend/WorkClub.Infrastructure/Data/Interceptors/TenantDbConnectionInterceptor.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Data.Common; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -63,14 +64,40 @@ public class TenantDbConnectionInterceptor : DbCommandInterceptor if (!string.IsNullOrWhiteSpace(tenantId) && command.Connection is NpgsqlConnection conn) { - // Prepend SET LOCAL to set tenant context - // Note: EF Core creates implicit transactions for queries, so SET LOCAL will work - command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'; " + command.CommandText; - _logger.LogInformation("SetTenantContext: Prepended SET LOCAL for tenant {TenantId}", tenantId); + try + { + // If no transaction exists AND connection is open, start one + // 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 { - _logger.LogWarning("ReaderExecuting: No tenant context available (tenantId={TenantId})", tenantId ?? "null"); + _logger.LogWarning("SetTenantContext: No tenant context available (tenantId={TenantId})", tenantId ?? "null"); } } }