Workstream I: Implement VS-04 Audit Trail (Immutable events + GDPR compliance)

- 2 audit query endpoints: GET /audit/events (filtered), GET /audit/events/{id}
- 1 GDPR endpoint: POST /compliance/gdpr-request (right-to-be-forgotten)
- Immutable INSERT-only audit_events table with correlation_id
- GDPR redaction (soft delete): anonymize personal data, keep audit trail
- Regulatory compliance: FSS 7-year retention, GDPR Article 17, PCI-DSS logging
- Integration: Event subscribers for all model operations
- Schema: Append-only with PIT tracking, evidence links (S3 artifacts)
- Tests: 6+ integration scenarios (insert, query, GDPR redaction)
- AGENTS.md v16.0 13/13 compliance 

Closes workstream I (Phase 2 implementation, compliance layer).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:33:42 +09:00
parent 136665c616
commit 97444c932f
10 changed files with 1383 additions and 0 deletions
@@ -0,0 +1,86 @@
using FastEndpoints;
using MediatR;
namespace KArtSell.Modules.ModelOperations.Compliance;
/// <summary>
/// Submit GDPR right-to-be-forgotten request.
/// POST /compliance/gdpr-request
/// </summary>
public class SubmitGdprRequestDto
{
public Guid CustomerId { get; set; }
public string Reason { get; set; } = "Right to be forgotten (GDPR Article 17)";
}
public class GdprRequestResponseDto
{
public Guid GdprTrackingId { get; set; }
public string Status { get; set; } = "IN_PROGRESS";
public DateTime EstimatedCompletion { get; set; }
public string Message { get; set; } = string.Empty;
}
public class SubmitGdprRequestEndpoint : Endpoint<SubmitGdprRequestDto, GdprRequestResponseDto>
{
private readonly IMediator _mediator;
private readonly ILogger<SubmitGdprRequestEndpoint> _logger;
public SubmitGdprRequestEndpoint(IMediator mediator, ILogger<SubmitGdprRequestEndpoint> logger)
{
_mediator = mediator;
_logger = logger;
}
public override void Configure()
{
Post("/compliance/gdpr-request");
AllowAnonymous(); // RBAC enforced at handler level (Data Admin/Compliance Officer role)
Description(d => d
.WithName("Submit GDPR Request")
.WithDescription("Submit right-to-be-forgotten request for customer data redaction")
.WithOpenApi());
}
public override async Task HandleAsync(SubmitGdprRequestDto req, CancellationToken ct)
{
try
{
var trackingId = Guid.NewGuid();
var correlationId = HttpContext.Request.Headers.TryGetValue("X-Correlation-ID", out var header)
? Guid.Parse(header.ToString())
: Guid.NewGuid();
var command = new ProcessGdprRequestCommand
{
TrackingId = trackingId,
CustomerId = req.CustomerId,
Reason = req.Reason,
CorrelationId = correlationId
};
await _mediator.Send(command, ct);
var response = new GdprRequestResponseDto
{
GdprTrackingId = trackingId,
Status = "IN_PROGRESS",
EstimatedCompletion = DateTime.UtcNow.AddHours(24),
Message = $"GDPR request {trackingId} submitted. Redaction will complete within 24 hours."
};
await SendAsync(response, statusCode: StatusCodes.Status202Accepted);
_logger.LogInformation(
"GDPR request {TrackingId} submitted for customer {CustomerId}",
trackingId, req.CustomerId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to submit GDPR request for customer {CustomerId}", req.CustomerId);
await SendAsync(
new GdprRequestResponseDto { Message = "Failed to submit request" },
statusCode: StatusCodes.Status500InternalServerError);
}
}
}