mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(patches): record package updates, reboots, and auto-update changes in the audit log
- ui.server.packages_update_requested: user, server, packages (or all), package manager, and the activity of the update. - ui.server.packages_update_finished: written when the remote process finishes (also when it fails), with the result and the exit code of the activity. The update gets a fresh type_uuid (an indexed activity property), so the listener finds exactly this activity. The actor comes from the request, because the listener runs in the queue. - ui.server.reboot_requested with result started or failed. - ui.server.auto_security_updates_updated (enabled, time, package manager, result) and ui.server.patch_settings_updated for a changed check frequency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
83ea5cc1f7
commit
f32ecca44c
@@ -3,6 +3,7 @@
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Str;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Contracts\Activity;
|
||||
|
||||
@@ -54,9 +55,18 @@ class UpdatePackage
|
||||
];
|
||||
}
|
||||
|
||||
return remote_process($commands, $server, callEventOnFinish: 'ServerPackagesUpdateFinished', callEventData: [
|
||||
$updateId = (string) Str::uuid();
|
||||
$packages = $all ? [] : [$package];
|
||||
$user = auth()->user();
|
||||
|
||||
return remote_process($commands, $server, type_uuid: $updateId, callEventOnFinish: 'ServerPackagesUpdateFinished', callEventData: [
|
||||
'update_id' => $updateId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'packages' => $all ? [] : [$package],
|
||||
'packages' => $packages,
|
||||
'package_manager' => $packageManager,
|
||||
'actor_id' => $user?->id,
|
||||
'actor_name' => $user?->name,
|
||||
'actor_email' => $user?->email,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\ServerPackagesUpdateFinished;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Models\Server;
|
||||
|
||||
class CheckPatchesAfterPackageUpdate
|
||||
{
|
||||
public function handle(ServerPackagesUpdateFinished $event): void
|
||||
{
|
||||
$server = Server::query()->where('uuid', $event->data['server_uuid'] ?? null)->first();
|
||||
if ($server === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServerPatchCheckJob::dispatchManual($server);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ServerPackagesUpdateFinished;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Models\Server;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
/**
|
||||
* Records the result of a package update in the audit log and checks the server again.
|
||||
*/
|
||||
class FinishServerPackagesUpdate
|
||||
{
|
||||
public function handle(ServerPackagesUpdateFinished $event): void
|
||||
{
|
||||
$server = Server::query()->where('uuid', $event->data['server_uuid'] ?? null)->first();
|
||||
if ($server === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = Activity::query()->where('properties->type_uuid', $event->data['update_id'] ?? null)->first();
|
||||
$status = data_get($activity, 'properties.status');
|
||||
$packages = $event->data['packages'] ?? [];
|
||||
|
||||
auditLog('ui.server.packages_update_finished', [
|
||||
'team_id' => $server->team_id,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'packages' => $packages === [] ? 'all' : $packages,
|
||||
'package_manager' => $event->data['package_manager'] ?? null,
|
||||
'activity_id' => $activity?->id,
|
||||
'result' => $status === ProcessStatus::FINISHED->value ? 'success' : 'failed',
|
||||
'exit_code' => data_get($activity, 'properties.exitCode'),
|
||||
'actor_id' => $event->data['actor_id'] ?? null,
|
||||
'actor_name' => $event->data['actor_name'] ?? null,
|
||||
'actor_email' => $event->data['actor_email'] ?? null,
|
||||
], $status === ProcessStatus::FINISHED->value ? 'info' : 'error');
|
||||
|
||||
ServerPatchCheckJob::dispatchManual($server);
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,14 @@ class Patches extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
RebootServer::run($this->server);
|
||||
try {
|
||||
RebootServer::run($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
$this->auditPatchAction('reboot_requested', ['result' => 'failed', 'error' => $e->getMessage()], 'error');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
$this->auditPatchAction('reboot_requested', ['result' => 'started']);
|
||||
$this->dispatch('success', 'Reboot started. The server is back in a few minutes, and Coolify then checks it again.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -152,13 +159,26 @@ class Patches extends Component
|
||||
if (! ConfigureAutomaticSecurityUpdates::supportFor($packageManager)) {
|
||||
throw new \Exception('Automatic security updates are supported only on Debian, Ubuntu, and RHEL-family servers. Run "Check for updates" first, so Coolify knows the package manager.');
|
||||
}
|
||||
ConfigureAutomaticSecurityUpdates::run($this->server, $packageManager, $this->autoSecurityUpdates, $time);
|
||||
$auditContext = ['enabled' => $this->autoSecurityUpdates, 'time' => $time, 'package_manager' => $packageManager];
|
||||
try {
|
||||
ConfigureAutomaticSecurityUpdates::run($this->server, $packageManager, $this->autoSecurityUpdates, $time);
|
||||
} catch (\Throwable $e) {
|
||||
$this->auditPatchAction('auto_security_updates_updated', [...$auditContext, 'result' => 'failed', 'error' => $e->getMessage()], 'error');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$settings->patch_check_frequency = $frequency === '' ? null : $frequency;
|
||||
$settings->is_auto_security_updates_enabled = $this->autoSecurityUpdates;
|
||||
$settings->auto_security_updates_time = $time;
|
||||
$changedFields = auditChangedFields($settings);
|
||||
$settings->save();
|
||||
if ($autoUpdatesChanged) {
|
||||
$this->auditPatchAction('auto_security_updates_updated', [...$auditContext, 'result' => 'success']);
|
||||
} elseif ($changedFields !== []) {
|
||||
$this->auditPatchAction('patch_settings_updated', ['changed_fields' => $changedFields]);
|
||||
}
|
||||
$this->syncSettings();
|
||||
$this->dispatch('success', 'Patch settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -187,6 +207,19 @@ class Patches extends Component
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function auditPatchAction(string $action, array $context, string $level = 'info'): void
|
||||
{
|
||||
auditLog("ui.server.{$action}", [
|
||||
'team_id' => $this->server->team_id,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'server_name' => $this->server->name,
|
||||
...$context,
|
||||
], $level);
|
||||
}
|
||||
|
||||
/**
|
||||
* A transferred server is an archive: another instance manages the host.
|
||||
*/
|
||||
@@ -215,6 +248,11 @@ class Patches extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
$this->auditPatchAction('packages_update_requested', [
|
||||
'packages' => $all ? 'all' : [$package],
|
||||
'package_manager' => $packageManager,
|
||||
'activity_id' => $activity->id,
|
||||
]);
|
||||
$this->dispatch('startupdate');
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
use App\Actions\Server\CheckUpdates;
|
||||
use App\Actions\Server\ConfigureAutomaticSecurityUpdates;
|
||||
use App\Actions\Server\RebootServer;
|
||||
use App\Actions\Server\UpdatePackage;
|
||||
use App\Events\ServerPackagesUpdateFinished;
|
||||
use App\Events\ServerPatchCheckFinished;
|
||||
use App\Jobs\ServerManagerJob;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Livewire\Server\Security\Patches;
|
||||
use App\Models\AuditEvent;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
@@ -22,6 +24,7 @@ use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@@ -401,3 +404,71 @@ describe('automatic security updates', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('audit trail', function () {
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
$this->withoutDefer();
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$this->server->settings->forceFill(['patch_check_result' => patchesTestResult(), 'patch_checked_at' => now()])->saveQuietly();
|
||||
});
|
||||
|
||||
it('records who requested a package update', function () {
|
||||
$activity = Activity::query()->create(['log_name' => 'default', 'description' => '', 'properties' => []]);
|
||||
UpdatePackage::shouldRun()->once()->andReturn($activity);
|
||||
|
||||
Livewire::test(Patches::class, ['server_uuid' => $this->server->uuid])->call('updatePackage', 'openssl');
|
||||
|
||||
$event = AuditEvent::query()->where('event', 'ui.server.packages_update_requested')->sole();
|
||||
expect($event->actor_email)->toBe($this->admin->email)
|
||||
->and($event->team_id)->toBe($this->team->id)
|
||||
->and($event->resource_uuid)->toBe($this->server->uuid)
|
||||
->and($event->metadata['packages'])->toBe(['openssl'])
|
||||
->and($event->metadata['activity_id'])->toBe($activity->id);
|
||||
});
|
||||
|
||||
it('records the result of a finished package update and checks the server again', function (string $status, string $result) {
|
||||
$activity = Activity::query()->create(['log_name' => 'default', 'description' => '', 'properties' => [
|
||||
'type_uuid' => 'update-1', 'status' => $status, 'exitCode' => $status === 'finished' ? 0 : 100,
|
||||
]]);
|
||||
auth()->logout();
|
||||
|
||||
ServerPackagesUpdateFinished::dispatch([
|
||||
'update_id' => 'update-1', 'server_uuid' => $this->server->uuid, 'packages' => [], 'package_manager' => 'apt',
|
||||
'actor_id' => $this->admin->id, 'actor_name' => $this->admin->name, 'actor_email' => $this->admin->email,
|
||||
]);
|
||||
|
||||
$event = AuditEvent::query()->where('event', 'ui.server.packages_update_finished')->sole();
|
||||
expect($event->actor_email)->toBe($this->admin->email)
|
||||
->and($event->team_id)->toBe($this->team->id)
|
||||
->and($event->metadata)->toMatchArray(['packages' => 'all', 'result' => $result, 'activity_id' => $activity->id]);
|
||||
Queue::assertPushed(ServerPatchCheckJob::class);
|
||||
})->with([
|
||||
['finished', 'success'],
|
||||
['error', 'failed'],
|
||||
]);
|
||||
|
||||
it('records a reboot and a failed reboot', function () {
|
||||
RebootServer::shouldRun()->once();
|
||||
Livewire::test(Patches::class, ['server_uuid' => $this->server->uuid])->call('rebootServer');
|
||||
|
||||
RebootServer::shouldRun()->once()->andThrow(new RuntimeException('Server is not reachable or not ready.'));
|
||||
Livewire::test(Patches::class, ['server_uuid' => $this->server->uuid])->call('rebootServer');
|
||||
|
||||
expect(AuditEvent::query()->where('event', 'ui.server.reboot_requested')->orderBy('id')->get()->map(fn ($event) => $event->metadata['result'])->all())
|
||||
->toBe(['started', 'failed']);
|
||||
});
|
||||
|
||||
it('records a change of the automatic security updates', function () {
|
||||
ConfigureAutomaticSecurityUpdates::shouldRun()->once();
|
||||
|
||||
Livewire::test(Patches::class, ['server_uuid' => $this->server->uuid])
|
||||
->set('autoSecurityUpdates', true)
|
||||
->call('submit');
|
||||
|
||||
$event = AuditEvent::query()->where('event', 'ui.server.auto_security_updates_updated')->sole();
|
||||
expect($event->actor_email)->toBe($this->admin->email)
|
||||
->and($event->metadata)->toMatchArray(['enabled' => true, 'time' => '03:00', 'package_manager' => 'apt', 'result' => 'success']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user