refactor: simplify remote process chain and harden ActivityMonitor

- Inline PrepareCoolifyTask and CoolifyTaskArgs into remote_process(),
  removing two single-consumer abstraction layers
- Add #[Locked] attribute to ActivityMonitor $activityId property
- Add team ownership verification in ActivityMonitor.hydrateActivity()
  with server_uuid fallback and fail-closed default
- Store team_id in activity properties for proper scoping
- Update CLAUDE.md to remove stale reference
- Add comprehensive tests for activity monitor authorization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-03-26 13:26:16 +01:00
co-authored by Claude Opus 4.6
parent c8efbf107a
commit 3e0d48faea
7 changed files with 155 additions and 131 deletions
+41 -23
View File
@@ -1,9 +1,10 @@
<?php
use App\Actions\CoolifyTask\PrepareCoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Helpers\SshRetryHandler;
use App\Jobs\CoolifyTask;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
@@ -38,29 +39,46 @@ function remote_process(
if (Auth::check()) {
$teams = Auth::user()->teams->pluck('id');
if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
throw new \Exception('User is not part of the team that owns this server');
throw new Exception('User is not part of the team that owns this server');
}
}
SshMultiplexingHelper::ensureMultiplexedConnection($server);
return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs(
server_uuid: $server->uuid,
command: $command_string,
type: $type,
type_uuid: $type_uuid,
model: $model,
ignore_errors: $ignore_errors,
call_event_on_finish: $callEventOnFinish,
call_event_data: $callEventData,
),
])();
$properties = [
'server_uuid' => $server->uuid,
'command' => $command_string,
'type' => $type,
'type_uuid' => $type_uuid,
'status' => ProcessStatus::QUEUED->value,
'team_id' => $server->team_id,
];
$activityLog = activity()
->withProperties($properties)
->event($type);
if ($model) {
$activityLog->performedOn($model);
}
$activity = $activityLog->log('[]');
dispatch(new CoolifyTask(
activity: $activity,
ignore_errors: $ignore_errors,
call_event_on_finish: $callEventOnFinish,
call_event_data: $callEventData,
));
$activity->refresh();
return $activity;
}
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{
return \App\Helpers\SshRetryHandler::retry(
return SshRetryHandler::retry(
function () use ($source, $dest, $server) {
$scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
@@ -92,7 +110,7 @@ function instant_remote_process_with_timeout(Collection|array $command, Server $
}
$command_string = implode("\n", $command);
return \App\Helpers\SshRetryHandler::retry(
return SshRetryHandler::retry(
function () use ($server, $command_string) {
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
$process = Process::timeout(30)->run($sshCommand);
@@ -128,7 +146,7 @@ function instant_remote_process(Collection|array $command, Server $server, bool
$command_string = implode("\n", $command);
$effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout');
return \App\Helpers\SshRetryHandler::retry(
return SshRetryHandler::retry(
function () use ($server, $command_string, $effectiveTimeout, $disableMultiplexing) {
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing);
$process = Process::timeout($effectiveTimeout)->run($sshCommand);
@@ -170,9 +188,9 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
if ($ignored) {
// TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorMessage, $exitCode);
throw new RuntimeException($errorMessage, $exitCode);
}
throw new \RuntimeException($errorMessage, $exitCode);
throw new RuntimeException($errorMessage, $exitCode);
}
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null, bool $includeAll = false): Collection
@@ -194,7 +212,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
associative: true,
flags: JSON_THROW_ON_ERROR
);
} catch (\JsonException $e) {
} catch (JsonException $e) {
// If JSON decoding fails, try to clean up the logs and retry
try {
// Ensure valid UTF-8 encoding
@@ -204,7 +222,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
associative: true,
flags: JSON_THROW_ON_ERROR
);
} catch (\JsonException $e) {
} catch (JsonException $e) {
// If it still fails, return empty collection to prevent crashes
return collect([]);
}
@@ -353,7 +371,7 @@ function checkRequiredCommands(Server $server)
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
} catch (\Throwable) {
} catch (Throwable) {
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);