Files
tiku-backend.net/Tiku.Infrastructure/Persistence/TenantIsolationSaveChangesInterceptor.cs

90 lines
3.2 KiB
C#

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<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
Enforce(eventData.Context);
return result;
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> 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}");