forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Tiku.Application.Security;
|
||||
|
||||
public sealed class CurrentTenant : ICurrentTenant
|
||||
{
|
||||
public Guid? TenantId { get; private set; }
|
||||
public string? Role { get; private set; }
|
||||
public bool IsResolved => TenantId.HasValue;
|
||||
|
||||
public void Load(ClaimsPrincipal principal)
|
||||
{
|
||||
TenantId = principal.FindGuid(TikuClaimTypes.TenantId);
|
||||
Role = principal.FindValue(TikuClaimTypes.TenantRole);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public sealed class CurrentUser : ICurrentUser
|
||||
public Guid? SessionId { get; private set; }
|
||||
public string? Phone { get; private set; }
|
||||
public string? Email { get; private set; }
|
||||
public string? TenantRole { get; private set; }
|
||||
public bool IsAuthenticated { get; private set; }
|
||||
|
||||
public void Load(ClaimsPrincipal principal)
|
||||
@@ -17,5 +18,6 @@ public sealed class CurrentUser : ICurrentUser
|
||||
SessionId = principal.FindGuid(TikuClaimTypes.SessionId);
|
||||
Phone = principal.FindValue(TikuClaimTypes.Phone);
|
||||
Email = principal.FindValue(TikuClaimTypes.Email);
|
||||
TenantRole = principal.FindValue(TikuClaimTypes.TenantRole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Tiku.Application.Security;
|
||||
|
||||
public interface ICurrentTenant
|
||||
{
|
||||
Guid? TenantId { get; }
|
||||
string? Role { get; }
|
||||
bool IsResolved { get; }
|
||||
void Load(ClaimsPrincipal principal);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public interface ICurrentUser
|
||||
Guid? SessionId { get; }
|
||||
string? Phone { get; }
|
||||
string? Email { get; }
|
||||
string? TenantRole { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
void Load(ClaimsPrincipal principal);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
16
Tiku.Application/Security/ITenantExecutionScope.cs
Normal file
16
Tiku.Application/Security/ITenantExecutionScope.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Tiku.Application.Security;
|
||||
|
||||
public interface ITenantExecutionScope
|
||||
{
|
||||
Task ExecuteAsync(
|
||||
Guid? targetTenantId,
|
||||
string reason,
|
||||
Func<IServiceProvider, CancellationToken, Task> operation,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TResult> ExecuteAsync<TResult>(
|
||||
Guid? targetTenantId,
|
||||
string reason,
|
||||
Func<IServiceProvider, CancellationToken, Task<TResult>> operation,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user