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
f0eeef0144
commit
7cd84b2be3
29 changed files with 2918 additions and 412 deletions
|
|
@ -10,9 +10,10 @@ use Illuminate\Http\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Öffnen und Export von Archiveinträgen (Beweisarchiv F.2): ohne Zweck
|
||||
* zeigt die Route ein kleines Formular, mit Zweck kommt der Download und
|
||||
* der Zugriff steht im Protokoll.
|
||||
* Öffnen und Export von Archiveinträgen (Beweisarchiv F.2): GET zeigt ein
|
||||
* kleines Formular, erst der POST mit Zweck (CSRF) liefert den Download,
|
||||
* und der Zugriff steht im Protokoll. Der Zweck steht nie in der URL und
|
||||
* damit nicht in Server-Logs (Umsetzungsaudit 29.09.2026, A-08).
|
||||
*/
|
||||
class ArchiveEntryController extends Controller
|
||||
{
|
||||
|
|
@ -32,10 +33,15 @@ class ArchiveEntryController extends Controller
|
|||
{
|
||||
abort_if($entry->isTombstoned(), 410, 'Der Eintrag ist nach Fristablauf gelöscht (Tombstone).');
|
||||
|
||||
$purpose = trim((string) $request->input('purpose', ''));
|
||||
$purpose = $request->isMethod('post') ? trim((string) $request->request->get('purpose', '')) : '';
|
||||
|
||||
if ($purpose === '') {
|
||||
return view('admin.archive.purpose', ['entry' => $entry, 'action' => $action]);
|
||||
return view('admin.archive.purpose', [
|
||||
'formAction' => route($action === 'export' ? 'admin.archive.export' : 'admin.archive.download', ['entry' => $entry->id]),
|
||||
'description' => __('Archiveintrag #:id (:event). Jeder Zugriff auf das Beweisarchiv wird mit Zweck protokolliert.', ['id' => $entry->id, 'event' => $entry->event->label()]),
|
||||
'submitLabel' => $action === 'export' ? __('Export herunterladen') : __('Payload öffnen'),
|
||||
'cancelUrl' => route('admin.archive.index'),
|
||||
]);
|
||||
}
|
||||
|
||||
$bytes = $action === 'export'
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Authorization\Permission;
|
||||
use App\Enums\ActivityType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\ActivityLog;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
|
|
@ -18,30 +21,51 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
|||
* Abruf im Aktivitätsverlauf. Medienroute, Vorschau-Signaturen und API
|
||||
* kennen diese Pfade nicht. Block F verlagert das Protokoll in
|
||||
* `archive_access_log`.
|
||||
*
|
||||
* Umsetzungsaudit 29.09.2026: Der Zweck kommt nur per POST (A-08), GET
|
||||
* zeigt das Zweckformular. Restnachweise entfernter Bilder (weich
|
||||
* gelöschte Zeile) nur mit `compliance:manage` (A-11) — wie das Archiv.
|
||||
*/
|
||||
class PressReleaseImageEvidenceController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ActivityLog $activityLog) {}
|
||||
|
||||
public function original(Request $request, int $id, int $image): StreamedResponse
|
||||
public function original(Request $request, int $id, int $image): StreamedResponse|View
|
||||
{
|
||||
return $this->stream($request, $id, $image, 'original');
|
||||
}
|
||||
|
||||
public function published(Request $request, int $id, int $image): StreamedResponse
|
||||
public function published(Request $request, int $id, int $image): StreamedResponse|View
|
||||
{
|
||||
return $this->stream($request, $id, $image, 'published');
|
||||
}
|
||||
|
||||
private function stream(Request $request, int $pressReleaseId, int $imageId, string $kind): StreamedResponse
|
||||
private function stream(Request $request, int $pressReleaseId, int $imageId, string $kind): StreamedResponse|View
|
||||
{
|
||||
$purpose = trim((string) $request->query('purpose', ''));
|
||||
|
||||
abort_if($purpose === '', 422, 'Zweck des Zugriffs fehlt (purpose).');
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScope(PortalScope::class)->findOrFail($pressReleaseId);
|
||||
$image = PressReleaseImage::query()->withTrashed()->where('press_release_id', $pressRelease->getKey())->findOrFail($imageId);
|
||||
|
||||
if ($image->trashed()) {
|
||||
Gate::authorize(Permission::ComplianceManage);
|
||||
}
|
||||
|
||||
$purpose = $request->isMethod('post') ? trim((string) $request->request->get('purpose', '')) : '';
|
||||
|
||||
if ($purpose === '') {
|
||||
abort_if($request->isMethod('post'), 422, 'Zweck des Zugriffs fehlt (purpose).');
|
||||
|
||||
return view('admin.archive.purpose', [
|
||||
'formAction' => route($kind === 'original' ? 'admin.press-releases.images.original' : 'admin.press-releases.images.published', ['id' => $pressRelease->getKey(), 'image' => $image->getKey()]),
|
||||
'description' => __(':kind zu Bild #:image (PM #:pr). Jeder Abruf wird mit Zweck protokolliert.', [
|
||||
'kind' => $kind === 'original' ? __('Beweisoriginal') : __('Veröffentlichte Fassung'),
|
||||
'image' => $image->getKey(),
|
||||
'pr' => $pressRelease->getKey(),
|
||||
]),
|
||||
'submitLabel' => __('Datei öffnen'),
|
||||
'cancelUrl' => route('admin.press-releases.show', ['id' => $pressRelease->getKey()]),
|
||||
]);
|
||||
}
|
||||
|
||||
$path = $kind === 'original' ? $image->original_path : $image->published_snapshot_path;
|
||||
$disk = Storage::disk($image->original_disk ?? (string) config('images.originals_disk'));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue