75 lines
3.0 KiB
C#
75 lines
3.0 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}"); |