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:
Kevin Adametz 2026-10-02 13:09:01 +00:00
parent c82f279fef
commit 60650baeb8
29 changed files with 2918 additions and 412 deletions

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Beweisarchiv (Umsetzungsaudit 29.09.2026, A-02/A-04/A-09/A-10): ein
* Auftrag je ausstehendem Archiveintrag statt eines einzigen Zeitstempels
* an der Meldung. Der Auftrag entsteht in der fachlichen Transaktion und
* trägt, was der Nachholer braucht — bei `pm_depublished` den Stand zum
* Zeitpunkt der Depublikation (verschlüsselt, APP_KEY wie die
* Redaction-Snapshots). Er wird gelöscht, sobald der Eintrag geschrieben
* ist; `press_releases.archive_pending_at` bleibt als Sammelmarker
* (Systemstatus, Kompatibilität) und fällt erst mit dem letzten Auftrag.
*
* Kein Fremdschlüssel auf press_releases: Aufträge sollen eine
* Hartlöschung der Meldung überleben und im Systemstatus sichtbar bleiben.
* Neue, kleine Tabelle — läuft sofort.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('archive_pending_jobs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('press_release_id');
$table->string('event', 30);
$table->string('subject_type', 80)->nullable();
$table->unsignedBigInteger('subject_id')->nullable();
$table->string('reason_category', 30)->nullable();
$table->unsignedBigInteger('legal_request_id')->nullable();
$table->unsignedBigInteger('actor_id')->nullable();
$table->string('note', 500)->nullable();
$table->longText('payload')->nullable();
$table->unsignedInteger('attempts')->default(0);
$table->string('last_error', 1000)->nullable();
$table->timestamp('last_attempted_at')->nullable();
$table->timestamp('created_at')->nullable();
$table->index(['press_release_id'], 'archive_pending_jobs_press_release_idx');
$table->index(['event', 'created_at'], 'archive_pending_jobs_event_created_idx');
});
}
public function down(): void
{
Schema::dropIfExists('archive_pending_jobs');
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Beweisarchiv (Umsetzungsaudit 29.09.2026):
*
* - `key_id` (A-15): kurze Kennung des Schlüssels, mit dem der Payload
* verschlüsselt wurde. Nach einer Rotation (ARCHIVE_PREVIOUS_KEYS)
* entschlüsselt das Archiv mit dem passenden Schlüssel; Bestand ohne
* Kennung probiert alle bekannten Schlüssel.
* - Index auf `payload_path` (OPT-6): Orphan-Sweep, Wiederregistrierung und
* Restore-Abgleich suchen nach Pfad.
*
* archive_entries ist klein und ohne FULLTEXT — ADD COLUMN und ADD INDEX
* laufen online.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('archive_entries', function (Blueprint $table) {
$table->char('key_id', 16)->nullable()->after('payload_bytes');
$table->index(['payload_path'], 'archive_entries_payload_path_idx');
});
}
public function down(): void
{
Schema::table('archive_entries', function (Blueprint $table) {
$table->dropIndex('archive_entries_payload_path_idx');
$table->dropColumn('key_id');
});
}
};

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Beweisarchiv (Umsetzungsaudit 29.09.2026, A-07): Name und E-Mail des
* Admins werden mitgeschrieben. `admin_id` bleibt `nullOnDelete` (eine
* Kontolöschung soll nicht am Zugriffsprotokoll scheitern) — nach einer
* Löschung weist die Zeile den Zugreifenden trotzdem aus. Kleine Tabelle,
* ADD COLUMN läuft sofort.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('archive_access_log', function (Blueprint $table) {
$table->string('admin_name')->nullable()->after('admin_id');
$table->string('admin_email')->nullable()->after('admin_name');
});
}
public function down(): void
{
Schema::table('archive_access_log', function (Blueprint $table) {
$table->dropColumn(['admin_name', 'admin_email']);
});
}
};