using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Tiku.Application.Security; namespace Tiku.Infrastructure.Persistence; public sealed class TenantIsolationSaveChangesInterceptor(ITenantContext tenantContext) : SaveChangesInterceptor { public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) { Enforce(eventData.Context); return result; } public override ValueTask> SavingChangesAsync( DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Enforce(eventData.Context); return ValueTask.FromResult(result); } private void Enforce(DbContext? dbContext) { if (dbContext is null) { return; } foreach (var entry in dbContext.ChangeTracker.Entries() .Where(entry => entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) { var tenantProperty = entry.Metadata.FindProperty("TenantId"); if (tenantProperty?.ClrType != typeof(Guid)) { continue; } var property = entry.Property("TenantId"); var currentTenantId = (Guid)(property.CurrentValue ?? Guid.Empty); var originalTenantId = (Guid)(property.OriginalValue ?? Guid.Empty); if (entry.State == EntityState.Added) { if (!tenantContext.TenantId.HasValue && !tenantContext.IsSystem) { throw new TenantIsolationException( entry.Metadata.ClrType, "Cannot add tenant-owned data without a resolved tenant."); } if (currentTenantId == Guid.Empty && tenantContext.TenantId.HasValue) { property.CurrentValue = tenantContext.TenantId.Value; currentTenantId = tenantContext.TenantId.Value; } if (!tenantContext.IsSystem && currentTenantId != tenantContext.TenantId) { throw new TenantIsolationException( entry.Metadata.ClrType, "Cannot add data for another tenant."); } continue; } if (property.IsModified || currentTenantId != originalTenantId) { throw new TenantIsolationException( entry.Metadata.ClrType, "Tenant ownership cannot be changed."); } if (!tenantContext.IsSystem && (!tenantContext.TenantId.HasValue || originalTenantId != tenantContext.TenantId.Value)) { throw new TenantIsolationException( entry.Metadata.ClrType, "Cannot modify or delete data owned by another tenant."); } } } } public sealed class TenantIsolationException(Type entityType, string message) : InvalidOperationException($"Tenant isolation rejected {entityType.Name}: {message}");