privacy: Löschfristen für Aktivitätsverlauf (395 Tage) und API-Zugriffsprotokoll (90 Tage)
Beide Tabellen hatten keine Frist; activity_events trägt Namen und Adressen im Klartext, api_usage_logs IP und User-Agent. MassPrunable, täglich 04:05 über model:prune, Fristen in config/monitoring.php (Roadmap 4.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d1b9ffdcf1
commit
002dc08b5c
5 changed files with 127 additions and 4 deletions
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\MassPrunable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
|
@ -13,6 +15,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||
*/
|
||||
class ActivityEvent extends Model
|
||||
{
|
||||
use MassPrunable;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
|
|
@ -50,4 +54,14 @@ class ActivityEvent extends Model
|
|||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Einträge nach der Löschfrist (config monitoring.retention, 395 Tage).
|
||||
*
|
||||
* @return Builder<static>
|
||||
*/
|
||||
public function prunable(): Builder
|
||||
{
|
||||
return static::query()->where('occurred_at', '<', now()->subDays((int) config('monitoring.retention.activity_events_days')));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\MassPrunable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class ApiUsageLog extends Model
|
||||
{
|
||||
use MassPrunable;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
|
|
@ -45,4 +49,14 @@ class ApiUsageLog extends Model
|
|||
{
|
||||
return $this->belongsTo(PersonalAccessToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zugriffe nach der Löschfrist (config monitoring.retention, 90 Tage).
|
||||
*
|
||||
* @return Builder<static>
|
||||
*/
|
||||
public function prunable(): Builder
|
||||
{
|
||||
return static::query()->where('requested_at', '<', now()->subDays((int) config('monitoring.retention.api_usage_logs_days')));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,23 @@ return [
|
|||
'warn_per_day' => (int) env('MONITORING_FAILED_LOGIN_WARN_PER_DAY', 1000),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Löschfristen der Protokolle (Roadmap 4.2)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Aktivitäts-Verlauf (activity_events: Anmeldungen, Statuswechsel,
|
||||
| Admin-Aktionen) wie die Nutzungsstatistik in DSE 5a nach 395 Tagen;
|
||||
| API-Zugriffsprotokoll (api_usage_logs, mit IP und User-Agent) nach
|
||||
| 90 Tagen. Entfernt täglich um 04:05 über `model:prune`.
|
||||
|
|
||||
*/
|
||||
|
||||
'retention' => [
|
||||
'activity_events_days' => (int) env('MONITORING_ACTIVITY_RETENTION_DAYS', 395),
|
||||
'api_usage_logs_days' => (int) env('MONITORING_API_USAGE_RETENTION_DAYS', 90),
|
||||
],
|
||||
|
||||
'scheduler' => [
|
||||
'heartbeat_warn_minutes' => 3,
|
||||
'heartbeat_critical_minutes' => 10,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ use App\Console\Commands\PurgeRedactionSnapshots;
|
|||
use App\Console\Commands\RefreshMarketData;
|
||||
use App\Console\Commands\ResetMonthlyPressReleaseQuota;
|
||||
use App\Console\Commands\SweepOrphanPressReleaseOriginals;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\ApiUsageLog;
|
||||
use App\Models\ContactMessage;
|
||||
use App\Models\FailedLoginTally;
|
||||
use App\Support\OperationalAlert;
|
||||
|
|
@ -259,13 +261,15 @@ Schedule::command(MonitoringHeartbeat::class)
|
|||
->onFailure($alertOnFailure('monitoring:heartbeat'));
|
||||
|
||||
// Tageszähler fehlgeschlagener Anmeldungen nach der Löschfrist entfernen
|
||||
// (täglich 04:05, config monitoring.failed_logins.retention_days) und
|
||||
// Spam-Quarantäne des Kontaktformulars nach 30 Tagen (Spamschutz B3).
|
||||
Schedule::command('model:prune', ['--model' => [FailedLoginTally::class, ContactMessage::class]])
|
||||
// (täglich 04:05, config monitoring.failed_logins.retention_days),
|
||||
// Spam-Quarantäne des Kontaktformulars nach 30 Tagen (Spamschutz B3),
|
||||
// Aktivitäts-Verlauf nach 395 und API-Zugriffsprotokoll nach 90 Tagen
|
||||
// (config monitoring.retention, Roadmap 4.2).
|
||||
Schedule::command('model:prune', ['--model' => [FailedLoginTally::class, ContactMessage::class, ActivityEvent::class, ApiUsageLog::class]])
|
||||
->dailyAt('04:05')
|
||||
->withoutOverlapping(30)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('model:prune failed_login_tallies, contact_messages'));
|
||||
->onFailure($alertOnFailure('model:prune failed_login_tallies, contact_messages, activity_events, api_usage_logs'));
|
||||
|
||||
// Fehlgeschlagene Jobs nach 30 Tagen entfernen (täglich 03:55). Bis dahin
|
||||
// sind sie im Systemstatus sichtbar und per `queue:retry` wiederholbar.
|
||||
|
|
|
|||
74
tests/Feature/Monitoring/LogRetentionTest.php
Normal file
74
tests/Feature/Monitoring/LogRetentionTest.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\ApiUsageLog;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Löschfristen von Aktivitäts-Verlauf und API-Zugriffsprotokoll
|
||||
* (Roadmap 4.2, config monitoring.retention).
|
||||
*/
|
||||
function activityEventAt(int $daysAgo): ActivityEvent
|
||||
{
|
||||
return ActivityEvent::create([
|
||||
'type' => ActivityType::UserLoggedIn,
|
||||
'occurred_at' => now()->subDays($daysAgo),
|
||||
'description' => 'Anmeldung',
|
||||
]);
|
||||
}
|
||||
|
||||
function apiUsageLogAt(int $daysAgo): ApiUsageLog
|
||||
{
|
||||
return ApiUsageLog::create([
|
||||
'method' => 'GET',
|
||||
'path' => 'api/v1/press-releases',
|
||||
'status_code' => 200,
|
||||
'ip_address' => '203.0.113.7',
|
||||
'requested_at' => now()->subDays($daysAgo),
|
||||
]);
|
||||
}
|
||||
|
||||
test('activity events are pruned after 395 days', function () {
|
||||
/** @var TestCase $this */
|
||||
$old = activityEventAt(396);
|
||||
$recent = activityEventAt(394);
|
||||
|
||||
$this->artisan('model:prune', ['--model' => [ActivityEvent::class]])->assertSuccessful();
|
||||
|
||||
$this->assertModelMissing($old);
|
||||
$this->assertModelExists($recent);
|
||||
});
|
||||
|
||||
test('api usage logs are pruned after 90 days', function () {
|
||||
/** @var TestCase $this */
|
||||
$old = apiUsageLogAt(91);
|
||||
$recent = apiUsageLogAt(89);
|
||||
|
||||
$this->artisan('model:prune', ['--model' => [ApiUsageLog::class]])->assertSuccessful();
|
||||
|
||||
$this->assertModelMissing($old);
|
||||
$this->assertModelExists($recent);
|
||||
});
|
||||
|
||||
test('the retention periods follow the configuration', function () {
|
||||
/** @var TestCase $this */
|
||||
config()->set('monitoring.retention.activity_events_days', 30);
|
||||
config()->set('monitoring.retention.api_usage_logs_days', 7);
|
||||
$event = activityEventAt(31);
|
||||
$log = apiUsageLogAt(8);
|
||||
|
||||
$this->artisan('model:prune', ['--model' => [ActivityEvent::class, ApiUsageLog::class]])->assertSuccessful();
|
||||
|
||||
$this->assertModelMissing($event);
|
||||
$this->assertModelMissing($log);
|
||||
});
|
||||
|
||||
test('the daily prune run covers both logs', function () {
|
||||
$command = collect(app(Schedule::class)->events())
|
||||
->map(fn ($event) => (string) $event->command)
|
||||
->first(fn (string $command) => str_contains($command, 'model:prune'));
|
||||
|
||||
expect($command)->toContain('ActivityEvent')->toContain('ApiUsageLog');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue