feat(security): add distributed authorization foundation
This commit is contained in:
@@ -16,7 +16,8 @@ namespace Tiku.Infrastructure.Jobs;
|
||||
internal sealed class BackgroundJobService(
|
||||
TikuDbContext dbContext,
|
||||
IServiceProvider serviceProvider,
|
||||
ITenantExecutionScope tenantExecutionScope) : IBackgroundJobService
|
||||
ITenantExecutionScope tenantExecutionScope,
|
||||
ICapabilityAccessEvaluator capabilityAccessEvaluator) : IBackgroundJobService
|
||||
{
|
||||
private static readonly TimeSpan LeaseDuration = TimeSpan.FromMinutes(5);
|
||||
|
||||
@@ -24,10 +25,19 @@ internal sealed class BackgroundJobService(
|
||||
CreateBackgroundJobCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedJobType = NormalizeJobType(command.JobType);
|
||||
if (!await capabilityAccessEvaluator.IsAllowedAsync(
|
||||
command.TenantId,
|
||||
ResolveCapabilityModule(normalizedJobType),
|
||||
CapabilityOperation.Write,
|
||||
cancellationToken))
|
||||
{
|
||||
throw new InvalidOperationException("Tenant capability does not allow this background job.");
|
||||
}
|
||||
var job = new BackgroundJob
|
||||
{
|
||||
TenantId = command.TenantId,
|
||||
JobType = NormalizeJobType(command.JobType),
|
||||
JobType = normalizedJobType,
|
||||
Payload = command.Payload,
|
||||
RunAfter = command.RunAfter,
|
||||
MaxRetries = Math.Clamp(command.MaxRetries, 0, 20)
|
||||
@@ -55,6 +65,19 @@ internal sealed class BackgroundJobService(
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!await capabilityAccessEvaluator.IsAllowedAsync(
|
||||
job.TenantId,
|
||||
ResolveCapabilityModule(job.JobType),
|
||||
CapabilityOperation.Write,
|
||||
cancellationToken))
|
||||
{
|
||||
job.Status = BackgroundJobStatus.Failed;
|
||||
job.CompletedAt = DateTimeOffset.UtcNow;
|
||||
job.LastError = "Tenant capability was revoked before job execution.";
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
job.Status = BackgroundJobStatus.Processing;
|
||||
job.LockedBy = workerId;
|
||||
job.LockExpiresAt = now.Add(LeaseDuration);
|
||||
@@ -64,8 +87,9 @@ internal sealed class BackgroundJobService(
|
||||
try
|
||||
{
|
||||
var result = await tenantExecutionScope.ExecuteAsync(
|
||||
job.TenantId,
|
||||
$"Background job {job.JobType}",
|
||||
new SystemScopeRequest(
|
||||
job.TenantId, SystemScopeCallerType.Worker, workerId,
|
||||
$"Background job {job.JobType}", job.Id.ToString("N")),
|
||||
(provider, token) => ProcessCoreAsync(provider, job, token),
|
||||
cancellationToken);
|
||||
job.Status = BackgroundJobStatus.Succeeded;
|
||||
@@ -328,6 +352,15 @@ internal sealed class BackgroundJobService(
|
||||
return jobType.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ResolveCapabilityModule(string jobType) => jobType switch
|
||||
{
|
||||
"content_export" or "content_import" or "asset_security_scan" => "content",
|
||||
"statistics_aggregation" => "dashboard",
|
||||
"commerce_reconciliation" => "commerce",
|
||||
"tenant_domain_recheck" => "settings",
|
||||
_ => "job"
|
||||
};
|
||||
|
||||
private static string NormalizeProvider(string? provider)
|
||||
{
|
||||
var normalized = (provider ?? string.Empty).Trim().ToLowerInvariant();
|
||||
|
||||
Reference in New Issue
Block a user