forked from xiongyuxing/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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}");
|
||||
Reference in New Issue
Block a user