diff --git a/app/Api/v1/Controllers/UserManagerController.php b/app/Api/v1/Controllers/UserManagerController.php index 7a8ba4e0..19bd5b76 100644 --- a/app/Api/v1/Controllers/UserManagerController.php +++ b/app/Api/v1/Controllers/UserManagerController.php @@ -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', diff --git a/app/Console/Commands/PurgeAuthLog.php b/app/Console/Commands/PurgeAuthLog.php index 5bb6d351..4baf84d4 100644 --- a/app/Console/Commands/PurgeAuthLog.php +++ b/app/Console/Commands/PurgeAuthLog.php @@ -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.'); } } diff --git a/config/2fauth.php b/config/2fauth.php index 255a2e7b..ff74c7a7 100644 --- a/config/2fauth.php +++ b/config/2fauth.php @@ -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), ], /* diff --git a/database/factories/AuthLogFactory.php b/database/factories/AuthLogFactory.php index 68903916..65f5319f 100644 --- a/database/factories/AuthLogFactory.php +++ b/database/factories/AuthLogFactory.php @@ -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. * diff --git a/tests/Feature/Console/PurgeLogTest.php b/tests/Feature/Console/PurgeLogTest.php new file mode 100644 index 00000000..afd06c8f --- /dev/null +++ b/tests/Feature/Console/PurgeLogTest.php @@ -0,0 +1,142 @@ +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' + ], + ]; + } + +}