Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
@@ -1,8 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using ItWorx.KnowledgeHub.Api.Authorization;
|
||||
using ItWorx.KnowledgeHub.Api.Security;
|
||||
using ItWorx.KnowledgeHub.Application.Audit;
|
||||
using ItWorx.KnowledgeHub.Application.Operations;
|
||||
using ItWorx.KnowledgeHub.Application.Operations.UseCases;
|
||||
using ItWorx.KnowledgeHub.Application.Platform;
|
||||
using ItWorx.KnowledgeHub.Contracts.Operations;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
||||
@@ -22,31 +25,93 @@ public static class BackupsEndpoints
|
||||
.RequireAuthorization(AuthorizationPolicies.RequireAdministrator);
|
||||
|
||||
group.MapGet("", ListAsync);
|
||||
group.MapGet("/status", StatusAsync);
|
||||
group.MapPut("/{backupId}/protection", ProtectAsync)
|
||||
.AddEndpointFilter<AntiforgeryEndpointFilter>();
|
||||
|
||||
group.MapPost("", CreateAsync)
|
||||
.RequireRateLimiting(RateLimitPolicies.JobCreation)
|
||||
.AddEndpointFilter<AntiforgeryEndpointFilter>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<Ok<BackupListResponse>> ListAsync(ListBackupsUseCase useCase, CancellationToken cancellationToken)
|
||||
private static async Task<Ok<BackupListResponse>> ListAsync(ListBackupsUseCase useCase, IBackupEvidenceStore evidence, CancellationToken cancellationToken)
|
||||
{
|
||||
var backups = await useCase.ExecuteAsync(cancellationToken);
|
||||
return TypedResults.Ok(new BackupListResponse(backups.Select(ToResponse).ToList()));
|
||||
var responses = new List<BackupSummaryResponse>(backups.Count);
|
||||
foreach (var backup in backups)
|
||||
responses.Add(ToResponse(backup, await evidence.GetAsync(backup.BackupId, cancellationToken)));
|
||||
return TypedResults.Ok(new BackupListResponse(responses));
|
||||
}
|
||||
|
||||
private static async Task<Ok<BackupSummaryResponse>> CreateAsync(
|
||||
CreateBackupUseCase useCase, ClaimsPrincipal principal, HttpContext httpContext, CancellationToken cancellationToken)
|
||||
CreateBackupUseCase useCase, IBackupEvidenceStore evidence, ClaimsPrincipal principal, HttpContext httpContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var correlationId = httpContext.Items[CorrelationIdMiddleware.ItemKey] as string;
|
||||
var actorId = Guid.Parse(principal.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
|
||||
var summary = await useCase.ExecuteAsync(new CreateBackupCommand(actorId, correlationId), cancellationToken);
|
||||
return TypedResults.Ok(ToResponse(summary));
|
||||
return TypedResults.Ok(ToResponse(summary, await evidence.GetAsync(summary.BackupId, cancellationToken)));
|
||||
}
|
||||
|
||||
private static BackupSummaryResponse ToResponse(BackupSummary summary) =>
|
||||
private static BackupSummaryResponse ToResponse(BackupSummary summary, BackupEvidence? evidence) =>
|
||||
new(
|
||||
summary.BackupId, summary.CreatedAt, summary.TotalSizeBytes, summary.TableCount,
|
||||
summary.TotalRowCount, summary.AttachmentCount, summary.SchemaVersion, summary.ProductVersion);
|
||||
summary.TotalRowCount, summary.AttachmentCount, summary.SchemaVersion, summary.ProductVersion,
|
||||
evidence?.Protected ?? false, (evidence?.ProtectionVersion ?? 0).ToString(CultureInfo.InvariantCulture),
|
||||
evidence?.IntegrityVerifiedAt, evidence?.RestoreVerifiedAt, evidence?.LastRestoreSucceeded);
|
||||
|
||||
private static async Task<Ok<BackupStatusResponse>> StatusAsync(
|
||||
ListBackupsUseCase backups, IBackupEvidenceStore evidence, IPlatformSettingsReader settings,
|
||||
IConfiguration configuration, TimeProvider clock, CancellationToken cancellationToken)
|
||||
{
|
||||
var policy = (await settings.GetEffectiveAsync(cancellationToken)).Values.BackupPolicyExpectation;
|
||||
var items = await backups.ExecuteAsync(cancellationToken);
|
||||
var receipts = new List<BackupEvidence>();
|
||||
BackupEvidence? latestReceipt = null;
|
||||
foreach (var backup in items)
|
||||
if (await evidence.GetAsync(backup.BackupId, cancellationToken) is { } receipt)
|
||||
{
|
||||
receipts.Add(receipt);
|
||||
if (items.Count > 0 && backup.BackupId == items[0].BackupId) latestReceipt = receipt;
|
||||
}
|
||||
DateTimeOffset? last = items.Count > 0 ? items[0].CreatedAt : null;
|
||||
var integrity = receipts.Select(r => r.IntegrityVerifiedAt).Max();
|
||||
var restore = receipts.Select(r => r.RestoreVerifiedAt).Max();
|
||||
var now = clock.GetUtcNow();
|
||||
var state = last is null || latestReceipt?.IntegrityVerifiedAt is null ? "Unknown"
|
||||
: now - last > TimeSpan.FromHours(policy.ExpectedIntervalHours) ? "Stale" : "Current";
|
||||
var restoreState = restore is null ? "Unknown"
|
||||
: now - restore > TimeSpan.FromDays(policy.RestoreVerificationMaxAgeDays) ? "Stale" : "Verified";
|
||||
return TypedResults.Ok(new BackupStatusResponse(state, "External", policy.ScheduleDescription, null,
|
||||
last?.AddHours(policy.ExpectedIntervalHours), last, integrity, restore, restoreState,
|
||||
items.Sum(b => b.TotalSizeBytes), receipts.Count(r => r.Protected), policy.ExpectedIntervalHours,
|
||||
configuration.GetValue<int?>("KnowledgeHub:Backup:RetainedCount") ?? 14,
|
||||
policy.DailyRetentionCount, policy.WeeklyRetentionCount, policy.MonthlyRetentionCount,
|
||||
policy.RestoreVerificationMaxAgeDays));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ProtectAsync(
|
||||
string backupId, BackupProtectionRequest request, IBackupEvidenceStore evidence,
|
||||
IAuditEventRecorder audit, HttpContext context, TimeProvider clock, CancellationToken cancellationToken)
|
||||
{
|
||||
var header = context.Request.Headers.IfMatch.ToString();
|
||||
if (header.Length < 3 || header[0] != '"' || header[^1] != '"'
|
||||
|| !int.TryParse(header[1..^1], NumberStyles.None, CultureInfo.InvariantCulture, out var version))
|
||||
return Results.Problem(statusCode: 428, title: "If-Match is verplicht.");
|
||||
var actor = Guid.Parse(context.User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
BackupProtectionResult result;
|
||||
try { result = await evidence.SetProtectionAsync(backupId, request.Protected, version, actor, cancellationToken); }
|
||||
catch (IOException) { return Results.Problem(statusCode: 409, title: "Back-upopslag is in gebruik; probeer opnieuw."); }
|
||||
if (result == BackupProtectionResult.NotFound) return Results.NotFound();
|
||||
if (result == BackupProtectionResult.Conflict) return Results.Problem(statusCode: 412, title: "De bescherming is intussen gewijzigd.");
|
||||
await audit.RecordAsync(new AuditEventRecord(AuditActorKind.User, actor, "backup",
|
||||
request.Protected ? "BackupProtected" : "BackupUnprotected", "Backup", backupId,
|
||||
request.Protected ? "Back-up beschermd tegen automatische retentie." : "Back-upbescherming verwijderd.",
|
||||
context.Items[CorrelationIdMiddleware.ItemKey] as string, clock.GetUtcNow()), cancellationToken);
|
||||
var receipt = await evidence.GetAsync(backupId, cancellationToken);
|
||||
context.Response.Headers.ETag = $"\"{receipt!.ProtectionVersion}\"";
|
||||
return Results.Ok(receipt);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user