mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(servers): validate the selected private key when adding a server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6c3249d324
commit
f8b320ab40
@@ -15,7 +15,7 @@ class SshMultiplexingHelper
|
||||
{
|
||||
public static function serverSshConfiguration(Server $server): array
|
||||
{
|
||||
$privateKey = PrivateKey::findOrFail($server->private_key_id);
|
||||
$privateKey = PrivateKey::where('team_id', $server->team_id)->findOrFail($server->private_key_id);
|
||||
|
||||
return [
|
||||
'sshKeyLocation' => $privateKey->getKeyLocation(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Team;
|
||||
use App\Rules\ValidServerIp;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -52,7 +53,7 @@ class ByIp extends Component
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'private_key_id' => 'nullable|integer',
|
||||
'private_key_id' => ['nullable', 'integer', Rule::exists('private_keys', 'id')->where('team_id', currentTeam()->id)],
|
||||
'new_private_key_name' => 'nullable|string',
|
||||
'new_private_key_description' => 'nullable|string',
|
||||
'new_private_key_value' => 'nullable|string',
|
||||
@@ -70,6 +71,7 @@ class ByIp extends Component
|
||||
return array_merge(ValidationPatterns::combinedMessages(), [
|
||||
'private_key_id.integer' => 'The Private Key field must be an integer.',
|
||||
'private_key_id.nullable' => 'The Private Key field is optional.',
|
||||
'private_key_id.exists' => 'The selected Private Key is invalid.',
|
||||
'new_private_key_name.string' => 'The Private Key Name must be a string.',
|
||||
'new_private_key_description.string' => 'The Private Key Description must be a string.',
|
||||
'new_private_key_value.string' => 'The Private Key Value must be a string.',
|
||||
|
||||
@@ -216,12 +216,17 @@ function deleteBackupsLocally(string|array|null $filenames, Server $server, bool
|
||||
|
||||
function streamBackupFromServer(Server $server, string $filename, string $contentType): StreamedResponse
|
||||
{
|
||||
$privateKey = $server->privateKey;
|
||||
if (! $privateKey || $privateKey->team_id !== $server->team_id) {
|
||||
throw new RuntimeException('Private key not found for this server.');
|
||||
}
|
||||
|
||||
$disk = Storage::build([
|
||||
'driver' => 'sftp',
|
||||
'host' => $server->ip,
|
||||
'port' => (int) $server->port,
|
||||
'username' => $server->user,
|
||||
'privateKey' => $server->privateKey->getKeyLocation(),
|
||||
'privateKey' => $privateKey->getKeyLocation(),
|
||||
'root' => '/',
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Livewire\Server\New\ByIp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
@@ -91,3 +94,45 @@ it('preselects a manually added private key without clearing server form data',
|
||||
->assertSet('server_role', 'build')
|
||||
->assertSee('Manual SSH Key');
|
||||
});
|
||||
|
||||
it('only accepts a private key from the current team when adding a server', function () {
|
||||
$otherTeamKey = PrivateKey::factory()->create(['team_id' => Team::factory()->create()->id]);
|
||||
|
||||
Livewire::test(ByIp::class, [
|
||||
'private_keys' => collect([$this->existingPrivateKey]),
|
||||
'limit_reached' => false,
|
||||
])
|
||||
->set('ip', '192.0.2.60')
|
||||
->set('private_key_id', $otherTeamKey->id)
|
||||
->call('submit')
|
||||
->assertHasErrors(['private_key_id']);
|
||||
|
||||
expect(Server::where('ip', '192.0.2.60')->exists())->toBeFalse();
|
||||
|
||||
Livewire::test(ByIp::class, [
|
||||
'private_keys' => collect([$this->existingPrivateKey]),
|
||||
'limit_reached' => false,
|
||||
])
|
||||
->set('ip', '192.0.2.61')
|
||||
->set('private_key_id', $this->existingPrivateKey->id)
|
||||
->call('submit')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Server::where('ip', '192.0.2.61')->value('private_key_id'))->toBe($this->existingPrivateKey->id);
|
||||
});
|
||||
|
||||
it('ssh configuration only uses a private key from the server team', function () {
|
||||
$otherTeamKey = PrivateKey::factory()->create(['team_id' => Team::factory()->create()->id]);
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->existingPrivateKey->id,
|
||||
]);
|
||||
|
||||
expect(SshMultiplexingHelper::serverSshConfiguration($server)['sshKeyLocation'])
|
||||
->toBe($this->existingPrivateKey->getKeyLocation());
|
||||
|
||||
Server::query()->whereKey($server->id)->update(['private_key_id' => $otherTeamKey->id]);
|
||||
|
||||
expect(fn () => SshMultiplexingHelper::serverSshConfiguration($server->fresh()))
|
||||
->toThrow(ModelNotFoundException::class);
|
||||
});
|
||||
|
||||
@@ -13,9 +13,11 @@ function backupDownloadServer(): Server
|
||||
'ip' => '192.0.2.10',
|
||||
'port' => 2222,
|
||||
'user' => 'root',
|
||||
'team_id' => 5,
|
||||
]);
|
||||
|
||||
$privateKey = Mockery::mock(PrivateKey::class);
|
||||
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
||||
$privateKey->forceFill(['team_id' => 5]);
|
||||
$privateKey->shouldReceive('getKeyLocation')->andReturn('/tmp/private-key');
|
||||
$server->setRelation('privateKey', $privateKey);
|
||||
|
||||
@@ -44,6 +46,15 @@ it('throws when the backup file does not exist on the server', function () {
|
||||
streamBackupFromServer(backupDownloadServer(), '/backups/archive.tar.gz', 'application/gzip');
|
||||
})->throws(FileNotFoundException::class);
|
||||
|
||||
it('refuses a private key from another team', function () {
|
||||
$server = backupDownloadServer();
|
||||
$server->privateKey->forceFill(['team_id' => 6]);
|
||||
|
||||
Storage::shouldReceive('build')->never();
|
||||
|
||||
streamBackupFromServer($server, '/backups/archive.tar.gz', 'application/gzip');
|
||||
})->throws(RuntimeException::class);
|
||||
|
||||
it('streams a backup file with the requested content type', function () {
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
fwrite($stream, 'backup contents');
|
||||
|
||||
Reference in New Issue
Block a user