archive: Auftragstabelle statt Marker, exklusiver Abschluss, Quarantäne, Restore über Payload-Identität und signierte Exporte, Schlüsselrotation (Umsetzungsaudit 29.09.: A-01 bis A-15, OPT-6, 5.1 d; 02.10.: O04, O05, O07, O08)
- A-01/A-02/A-04: archive_pending_jobs je Vorgang (ArchivePendingQueue), Depublikationsstand am Auftrag festgehalten, Nachholer ohne Schlüssel FAILURE; archive_pending_at nur noch Sammelmarker - O07: Direktweg und Nachholer beanspruchen den Auftrag unter Zeilensperre - O08: fehlende Snapshot-Datei → Verweis auf den image_removed-Eintrag oder offener Auftrag, nie ein still unvollständiger Nachweis - A-03/A-06: Sweep verschiebt in die Quarantäne, Pause beim Restore, archive:reregister; Restore-Abgleich löscht Anwendungskopien erneut - O04/O05: Restore ordnet über den Payload-Pfad zu; unsignierte Exporte nur mit --accept-unsigned, reregister bricht bei ungültigem/fehlendem Export ab - A-05, A-07 bis A-15: Altbestand als möglicherweise veröffentlicht, Hold- Historie, Zweck per POST, Nachweise gelöschter Bilder nur mit compliance:manage, Anhänge auf ihrer Disk, Schlüssel-ID und ARCHIVE_PREVIOUS_KEYS, HMAC per HKDF, archive:verify-export - 5.1 d: Depublikation im Meldeverfahren trägt legal_request_id PressReleaseService.php enthält zusätzlich K-01 (Prüfung im Publish-Lock), N19 (archive() gibt Kopien frei) und N09 (markImagesPublished nur für auslieferbare Bilder), weil die Datei ungeteilt committet ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c82f279fef
commit
60650baeb8
29 changed files with 2918 additions and 412 deletions
|
|
@ -3,16 +3,19 @@
|
|||
namespace App\Services\PressRelease;
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\ArchiveEvent;
|
||||
use App\Enums\ArchiveReasonCategory;
|
||||
use App\Enums\LegalRequestStatus;
|
||||
use App\Enums\LegalRequestType;
|
||||
use App\Enums\LegalRequestVerificationMethod;
|
||||
use App\Models\ArchiveEntry;
|
||||
use App\Models\ArchivePendingJob;
|
||||
use App\Models\LegalRequest;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseRedactionSnapshot;
|
||||
use App\Models\User;
|
||||
use App\Services\ActivityLog;
|
||||
use App\Services\Archive\ArchivePendingQueue;
|
||||
use App\Services\Archive\PressReleaseArchiver;
|
||||
use App\Services\PressRelease\Classification\ClassificationManager;
|
||||
use App\Services\PressRelease\Classification\ClassificationResult;
|
||||
|
|
@ -152,7 +155,7 @@ class CorrectionService
|
|||
|
||||
$this->assertNotRateLimited($pressRelease);
|
||||
|
||||
[$legalRequest, $snapshot] = DB::transaction(function () use ($pressRelease, $newValues, $requesterEmail, $method, $classification): array {
|
||||
[$legalRequest, $snapshot, $job] = DB::transaction(function () use ($pressRelease, $newValues, $requesterEmail, $method, $classification, $actor): array {
|
||||
$legalRequest = LegalRequest::query()->create([
|
||||
'press_release_id' => $pressRelease->id,
|
||||
'portal' => $pressRelease->portal?->value,
|
||||
|
|
@ -175,15 +178,16 @@ class CorrectionService
|
|||
], $legalRequest);
|
||||
|
||||
$this->writeChangedFields($pressRelease, $classification['fields'], $newValues);
|
||||
$pressRelease->forceFill(['archive_pending_at' => now()])->save();
|
||||
$pressRelease->save();
|
||||
$pressRelease->lockSync('Kleine Korrektur '.$legalRequest->reference());
|
||||
$job = $this->queueArchive($pressRelease, $snapshot, $legalRequest, $actor, 'Kleine Korrektur (Selbstbedienung)');
|
||||
|
||||
return [$legalRequest, $snapshot];
|
||||
return [$legalRequest, $snapshot, $job];
|
||||
});
|
||||
|
||||
WebCaches::forgetSitemaps();
|
||||
|
||||
$entry = $this->archiveChange($pressRelease, $snapshot, $legalRequest, $actor, 'Kleine Korrektur (Selbstbedienung)');
|
||||
$entry = $this->archiveChange($pressRelease, $job, $snapshot, $legalRequest, $actor);
|
||||
|
||||
$changedFields = collect($classification['fields'])
|
||||
->filter(fn (array $meta): bool => $meta['changed'])
|
||||
|
|
@ -238,7 +242,7 @@ class CorrectionService
|
|||
return false;
|
||||
}
|
||||
|
||||
$snapshot = DB::transaction(function () use ($pressRelease, $legalRequest, $classification, $newValues): PressReleaseRedactionSnapshot {
|
||||
[$snapshot, $job] = DB::transaction(function () use ($pressRelease, $legalRequest, $classification, $newValues, $actor): array {
|
||||
$legalRequest->forceFill([
|
||||
'status' => LegalRequestStatus::Resolved->value,
|
||||
'payload' => array_merge($legalRequest->payload ?? [], ['fields' => $classification['fields']]),
|
||||
|
|
@ -251,15 +255,16 @@ class CorrectionService
|
|||
], $legalRequest);
|
||||
|
||||
$this->writeChangedFields($pressRelease, $classification['fields'], $newValues);
|
||||
$pressRelease->forceFill(['archive_pending_at' => now()])->save();
|
||||
$pressRelease->save();
|
||||
$pressRelease->lockSync('Kostenpflichtige Änderung '.$legalRequest->reference());
|
||||
$job = $this->queueArchive($pressRelease, $snapshot, $legalRequest, $actor, 'Kostenpflichtige Änderung');
|
||||
|
||||
return $snapshot;
|
||||
return [$snapshot, $job];
|
||||
});
|
||||
|
||||
WebCaches::forgetSitemaps();
|
||||
|
||||
$entry = $this->archiveChange($pressRelease, $snapshot, $legalRequest, $actor, 'Kostenpflichtige Änderung');
|
||||
$entry = $this->archiveChange($pressRelease, $job, $snapshot, $legalRequest, $actor);
|
||||
|
||||
$changedFields = collect($classification['fields'])
|
||||
->filter(fn (array $meta): bool => $meta['changed'])
|
||||
|
|
@ -284,47 +289,54 @@ class CorrectionService
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beweisarchiv (F.4): Auftrag in der fachlichen Transaktion mit
|
||||
* Vorher-Stand aus dem Snapshot und Nachher-Stand dieser Änderung
|
||||
* (Umsetzungsaudit 29.09.2026, A-02/A-04).
|
||||
*/
|
||||
private function queueArchive(PressRelease $pressRelease, PressReleaseRedactionSnapshot $snapshot, LegalRequest $legalRequest, ?User $actor, string $note): ArchivePendingJob
|
||||
{
|
||||
return app(ArchivePendingQueue::class)->push(
|
||||
(int) $pressRelease->getKey(),
|
||||
ArchiveEvent::GdprChange,
|
||||
$snapshot,
|
||||
ArchiveReasonCategory::Correction,
|
||||
$legalRequest->getKey(),
|
||||
$actor?->getKey(),
|
||||
$note,
|
||||
[
|
||||
'before' => (array) ($snapshot->encrypted_before['fields'] ?? []),
|
||||
'after' => $pressRelease->only(self::CORRECTABLE_FIELDS),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eintrag nach dem Commit schreiben. Scheitert die Ablage, bleibt der
|
||||
* Auftrag offen und `archive:retry-pending` holt ihn nach (N11).
|
||||
*/
|
||||
private function archiveChange(PressRelease $pressRelease, ArchivePendingJob $job, PressReleaseRedactionSnapshot $snapshot, LegalRequest $legalRequest, ?User $actor): ?ArchiveEntry
|
||||
{
|
||||
$payload = (array) $job->payload;
|
||||
|
||||
return app(ArchivePendingQueue::class)->completeWith($job, fn () => app(PressReleaseArchiver::class)->gdprChange(
|
||||
$pressRelease,
|
||||
(array) ($payload['before'] ?? []),
|
||||
(array) ($payload['after'] ?? []),
|
||||
$legalRequest,
|
||||
$actor,
|
||||
ArchiveReasonCategory::Correction,
|
||||
$job->note,
|
||||
$snapshot->getKey(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt die geänderten Felder (Text durch den Sanitizer) auf die PM.
|
||||
*
|
||||
* @param array<string, array{distance_percent: float, changed: bool}> $classifiedFields
|
||||
* @param array<string, string|null> $newValues
|
||||
*/
|
||||
/**
|
||||
* Beweisarchiv (F.4): Vorher-Stand aus dem Snapshot, Nachher-Stand aus
|
||||
* der Meldung. Scheitert die Ablage, bleibt `archive_pending_at`
|
||||
* stehen und `archive:retry-pending` holt den Eintrag nach (N11).
|
||||
*/
|
||||
private function archiveChange(PressRelease $pressRelease, PressReleaseRedactionSnapshot $snapshot, LegalRequest $legalRequest, ?User $actor, string $note): ?ArchiveEntry
|
||||
{
|
||||
try {
|
||||
$entry = app(PressReleaseArchiver::class)->gdprChange(
|
||||
$pressRelease,
|
||||
(array) ($snapshot->encrypted_before['fields'] ?? []),
|
||||
$pressRelease->fresh()?->only(self::CORRECTABLE_FIELDS) ?? [],
|
||||
$legalRequest,
|
||||
$actor,
|
||||
ArchiveReasonCategory::Correction,
|
||||
$note,
|
||||
$snapshot->getKey(),
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
Log::error('Beweisarchiv: Korrektur konnte nicht archiviert werden, wird nachgeholt.', [
|
||||
'press_release_id' => $pressRelease->getKey(),
|
||||
'legal_request_id' => $legalRequest->getKey(),
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entry !== null) {
|
||||
$pressRelease->forceFill(['archive_pending_at' => null])->save();
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function writeChangedFields(PressRelease $pressRelease, array $classifiedFields, array $newValues): void
|
||||
{
|
||||
foreach ($classifiedFields as $field => $meta) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue