feat(security): add distributed authorization foundation

This commit is contained in:
2026-07-29 10:40:10 +08:00
parent c7f9a4e3c9
commit df88fa19cb
76 changed files with 22020 additions and 88 deletions

View File

@@ -1,6 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Security;
using System.Diagnostics;
using System.Text.Json;
using Tiku.Domain.Operations;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Tenancy;
@@ -9,42 +14,102 @@ public sealed class TenantExecutionScope(
ILogger<TenantExecutionScope> logger) : ITenantExecutionScope
{
public Task ExecuteAsync(
Guid? targetTenantId,
string reason,
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task> operation,
CancellationToken cancellationToken = default)
{
return ExecuteAsync<object?>(
targetTenantId,
reason,
async (provider, token) =>
{
await operation(provider, token);
return null;
},
cancellationToken);
}
CancellationToken cancellationToken = default) =>
ExecuteAsync<object?>(request, async (provider, token) =>
{
await operation(provider, token);
return null;
}, cancellationToken);
public async Task<TResult> ExecuteAsync<TResult>(
Guid? targetTenantId,
string reason,
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task<TResult>> operation,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(operation);
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("A system scope requires an audit reason.", nameof(reason));
}
Validate(request, operation);
await using var scope = scopeFactory.CreateAsyncScope();
var initializer = scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>();
initializer.InitializeSystem(targetTenantId, reason);
initializer.InitializeSystem(request.TargetTenantId, request.Reason);
logger.LogWarning(
"Entering audited system tenant scope. TargetTenantId={TargetTenantId} Reason={Reason}",
targetTenantId,
reason);
"Entering audited system scope. CallerType={CallerType} Caller={Caller} TargetTenantId={TargetTenantId} CorrelationId={CorrelationId} Reason={Reason}",
request.CallerType,
request.Caller,
request.TargetTenantId,
request.CorrelationId,
request.Reason);
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
var started = DateTimeOffset.UtcNow;
var stopwatch = Stopwatch.StartNew();
await using var transaction = dbContext.Database.IsRelational()
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
: null;
await WriteAuditAsync(dbContext, request, "system_scope.entered", started, null, null, cancellationToken);
try
{
var result = await operation(scope.ServiceProvider, cancellationToken);
await WriteAuditAsync(
dbContext, request, "system_scope.completed", started, stopwatch.ElapsedMilliseconds, null, cancellationToken);
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
return result;
}
catch (Exception exception)
{
if (transaction is not null)
{
await transaction.RollbackAsync(CancellationToken.None);
dbContext.ChangeTracker.Clear();
await WriteAuditAsync(
dbContext, request, "system_scope.entered", started, null, null, CancellationToken.None);
}
await WriteAuditAsync(
dbContext, request, "system_scope.failed", started, stopwatch.ElapsedMilliseconds,
exception.GetType().Name, CancellationToken.None);
throw;
}
}
return await operation(scope.ServiceProvider, cancellationToken);
private static void Validate<TResult>(
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task<TResult>> operation)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(operation);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Caller);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Reason);
ArgumentException.ThrowIfNullOrWhiteSpace(request.CorrelationId);
}
private static async Task WriteAuditAsync(
TikuDbContext dbContext,
SystemScopeRequest request,
string action,
DateTimeOffset startedAt,
long? elapsedMilliseconds,
string? failureType,
CancellationToken cancellationToken)
{
dbContext.AuditLogs.Add(new AuditLog
{
TenantId = request.TargetTenantId,
Action = action,
TargetType = "system_scope",
TargetId = request.CorrelationId,
Details = JsonSerializer.SerializeToElement(new
{
callerType = request.CallerType.ToString(),
request.Caller,
request.Reason,
request.CorrelationId,
startedAt,
elapsedMilliseconds,
failureType
})
});
await dbContext.SaveChangesAsync(cancellationToken);
}
}