Add authentication log cleaning and associated tests

This commit is contained in:
Bubka 2024-05-24 13:50:19 +02:00
parent a9b1a20f30
commit e73fbf658f
5 changed files with 180 additions and 5 deletions

View File

@ -9,6 +9,7 @@
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
@ -215,6 +216,13 @@ public function authentications(Request $request, User $user)
{
$this->authorize('view', $user);
// Here we purge the authentication log.
// Running the purge command when someone fetchs the auth log
// is not very elegant but it's straitforward compared
// to a scheduled task, and the delete query is light.
// => To enhance.
Artisan::call('2fauth:purge-log');
$validated = $this->validate($request, [
'period' => 'sometimes|numeric',
'limit' => 'sometimes|numeric',

View File

@ -26,6 +26,7 @@
use App\Models\AuthLog;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class PurgeAuthLog extends Command
{
@ -41,17 +42,23 @@ class PurgeAuthLog extends Command
*
* @var string
*/
public $description = 'Purge all authentication logs older than the configurable amount of days.';
public $description = 'Delete all authentication log entries older than the configurable amount of days (see env vars).';
/**
* Execute the console command.
*/
public function handle() : void
{
$this->comment('Clearing authentication log...');
$retentionTime = config('2fauth.config.authLogRetentionTime');
$retentionTime = is_numeric($retentionTime) ? (int) $retentionTime : 365;
$date = now()->subDays($retentionTime)->format('Y-m-d H:i:s');
$deleted = AuthLog::where('login_at', '<', now()->subDays(config('2fauth.authLogRetentionTime'))->format('Y-m-d H:i:s'))->delete();
AuthLog::where('login_at', '<', $date)
->orWhere('logout_at', '<', $date)
->delete();
$this->info($deleted . ' authentication logs cleared.');
Log::info('Authentication log purged');
$this->components->info('Authentication log purged successfully.');
}
}

View File

@ -28,7 +28,7 @@
'outgoingProxy' => env('PROXY_FOR_OUTGOING_REQUESTS', ''),
'proxyLogoutUrl' => env('PROXY_LOGOUT_URL', null),
'appSubdirectory' => env('APP_SUBDIRECTORY', ''),
'authLogRetentionTime' => env('AUTHENTICATION_LOG_RETENTION', 365),
'authLogRetentionTime' => envUnlessEmpty('AUTHENTICATION_LOG_RETENTION', 365),
],
/*

View File

@ -28,6 +28,24 @@ public function definition()
];
}
/**
* Indicate that the model has login before last year.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
*/
public function daysAgo(int $days)
{
return $this->state(function (array $attributes) use ($days) {
$loginDate = now()->subDays($days);
$logoutDate = $loginDate->addHours(1);
return [
'login_at' => $loginDate,
'logout_at' => $logoutDate,
];
});
}
/**
* Indicate that the model is a failed login.
*

View File

@ -0,0 +1,142 @@
<?php
namespace Tests\Feature\Console;
use App\Models\AuthLog;
use App\Models\User;
use Illuminate\Support\Carbon;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\FeatureTestCase;
class PurgeLogTest extends FeatureTestCase
{
/**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
*/
protected $user;
/**
* @test
*/
public function setUp() : void
{
parent::setUp();
$this->user = User::factory()->create();
}
/**
* @test
*/
public function test_purgeLog_completes()
{
$this->artisan('2fauth:purge-log')
->assertSuccessful();
}
/**
* @test
*/
public function test_purgeLog_defaults_to_one_year()
{
$oneYearOldLog = AuthLog::factory()->daysAgo(366)->for($this->user, 'authenticatable')->create();
$sixMonthsOldLog = AuthLog::factory()->daysAgo(364)->for($this->user, 'authenticatable')->create();
$this->artisan('2fauth:purge-log');
$this->assertDatabaseHas('auth_logs', [
'id' => $sixMonthsOldLog->id
]);
$this->assertDatabaseMissing('auth_logs', [
'id' => $oneYearOldLog->id
]);
}
/**
* @test
*/
public function test_purgeLog_deletes_records_older_than_retention_time()
{
$retention = 180;
config(['2fauth.config.authLogRetentionTime' => $retention]);
$log = AuthLog::factory()->daysAgo($retention + 1)->for($this->user, 'authenticatable')->create();
$this->artisan('2fauth:purge-log');
$this->assertDatabaseMissing('auth_logs', [
'id' => $log->id
]);
}
/**
* @test
*/
public function test_purgeLog_deletes_logout_only_records_older_than_retention_time()
{
$retention = 180;
config(['2fauth.config.authLogRetentionTime' => $retention]);
$log = AuthLog::factory()->logoutOnly()->for($this->user, 'authenticatable')->create();
$this->travelTo(Carbon::now()->addDays($retention + 1));
$this->artisan('2fauth:purge-log');
$this->assertDatabaseMissing('auth_logs', [
'id' => $log->id
]);
}
/**
* @test
*/
public function test_purgeLog_does_not_delete_records_younger_than_retention_time()
{
$retention = 180;
config(['2fauth.config.authLogRetentionTime' => $retention]);
$log = AuthLog::factory()->daysAgo($retention - 1)->for($this->user, 'authenticatable')->create();
$this->artisan('2fauth:purge-log');
$this->assertDatabaseHas('auth_logs', [
'id' => $log->id
]);
}
/**
* @test
*/
#[DataProvider('provideInvalidConfig')]
public function test_purgeLog_with_invalid_config_defaults_to_one_year($config)
{
config(['2fauth.config.authLogRetentionTime' => $config]);
$oneYearOldLog = AuthLog::factory()->daysAgo(366)->for($this->user, 'authenticatable')->create();
$sixMonthsOldLog = AuthLog::factory()->daysAgo(364)->for($this->user, 'authenticatable')->create();
$this->artisan('2fauth:purge-log');
$this->assertDatabaseHas('auth_logs', [
'id' => $sixMonthsOldLog->id
]);
$this->assertDatabaseMissing('auth_logs', [
'id' => $oneYearOldLog->id
]);
}
/**
* Provide invalid config for validation test
*/
public static function provideInvalidConfig() : array
{
return [
'NULL' => [
null
],
'EMPTY' => [
''
],
'STRING' => [
'ljhkjh'
],
];
}
}