forked from gongxuegit/tiku-backend.net
feat(security): add distributed authorization foundation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user