forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
87
Tiku.Application/Security/ITenantContext.cs
Normal file
87
Tiku.Application/Security/ITenantContext.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
namespace Tiku.Application.Security;
|
||||
|
||||
public enum TenantResolutionSource
|
||||
{
|
||||
None,
|
||||
Host,
|
||||
TenantCode,
|
||||
Jwt,
|
||||
RefreshToken,
|
||||
System
|
||||
}
|
||||
|
||||
public interface ITenantContext
|
||||
{
|
||||
Guid? TenantId { get; }
|
||||
string? TenantCode { get; }
|
||||
TenantResolutionSource ResolutionSource { get; }
|
||||
bool IsResolved { get; }
|
||||
bool IsSystem { get; }
|
||||
}
|
||||
|
||||
public interface ITenantContextInitializer
|
||||
{
|
||||
void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source);
|
||||
void InitializeSystem(Guid? targetTenantId, string reason);
|
||||
}
|
||||
|
||||
public sealed class TenantContext : ITenantContext, ITenantContextInitializer
|
||||
{
|
||||
public Guid? TenantId { get; private set; }
|
||||
public string? TenantCode { get; private set; }
|
||||
public TenantResolutionSource ResolutionSource { get; private set; }
|
||||
public bool IsResolved => TenantId.HasValue;
|
||||
public bool IsSystem { get; private set; }
|
||||
internal string? SystemReason { get; private set; }
|
||||
|
||||
public void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentException("Tenant ID cannot be empty.", nameof(tenantId));
|
||||
}
|
||||
|
||||
if (IsSystem)
|
||||
{
|
||||
throw new InvalidOperationException("A system tenant context cannot be replaced.");
|
||||
}
|
||||
|
||||
if (TenantId.HasValue && TenantId.Value != tenantId)
|
||||
{
|
||||
throw new TenantContextConflictException(TenantId.Value, tenantId);
|
||||
}
|
||||
|
||||
var wasResolved = TenantId.HasValue;
|
||||
TenantId = tenantId;
|
||||
TenantCode = string.IsNullOrWhiteSpace(tenantCode) ? TenantCode : tenantCode.Trim();
|
||||
if (!wasResolved)
|
||||
{
|
||||
ResolutionSource = source;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeSystem(Guid? targetTenantId, string reason)
|
||||
{
|
||||
if (IsResolved || IsSystem)
|
||||
{
|
||||
throw new InvalidOperationException("Tenant context has already been initialized.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
{
|
||||
throw new ArgumentException("A system scope requires an audit reason.", nameof(reason));
|
||||
}
|
||||
|
||||
TenantId = targetTenantId;
|
||||
ResolutionSource = TenantResolutionSource.System;
|
||||
IsSystem = true;
|
||||
SystemReason = reason.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantContextConflictException(Guid expectedTenantId, Guid actualTenantId)
|
||||
: Exception($"Resolved tenant '{expectedTenantId}' conflicts with authenticated tenant '{actualTenantId}'.")
|
||||
{
|
||||
public Guid ExpectedTenantId { get; } = expectedTenantId;
|
||||
public Guid ActualTenantId { get; } = actualTenantId;
|
||||
}
|
||||
Reference in New Issue
Block a user