From 1712fb393ad3fdca5a3c8d94eb43d73379f85dc4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:02:19 +0200 Subject: [PATCH] fix(transfer): reuse or create the private key in the target team only The import looked up the key in all teams and stopped with "already exists on another team", which also told the user that another team has the key. Keys are unique per team, so reuse a key of the target team or create one. Co-Authored-By: Claude Opus 5.5 --- .../ServerTransfer/ServerTransferImporter.php | 12 +++++------ .../ServerTransferExporterImporterTest.php | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/Services/ServerTransfer/ServerTransferImporter.php b/app/Services/ServerTransfer/ServerTransferImporter.php index f83b281993..4642cf8e53 100644 --- a/app/Services/ServerTransfer/ServerTransferImporter.php +++ b/app/Services/ServerTransfer/ServerTransferImporter.php @@ -384,14 +384,12 @@ class ServerTransferImporter ?? data_get($payload, 'fingerprint'); if ($fingerprint) { - $existing = PrivateKey::query()->where('fingerprint', $fingerprint)->first(); + // Keys are unique per team, so only a key of the target team can be reused. + $existing = PrivateKey::query() + ->where('team_id', $teamId) + ->where('fingerprint', $fingerprint) + ->first(); if ($existing) { - if ((int) $existing->team_id !== $teamId) { - throw ValidationException::withMessages([ - 'private_key' => ['This SSH private key already exists on another team on this instance.'], - ]); - } - return $existing; } } diff --git a/tests/Unit/ServerTransfer/ServerTransferExporterImporterTest.php b/tests/Unit/ServerTransfer/ServerTransferExporterImporterTest.php index e77ea16ad7..85dc538ad4 100644 --- a/tests/Unit/ServerTransfer/ServerTransferExporterImporterTest.php +++ b/tests/Unit/ServerTransfer/ServerTransferExporterImporterTest.php @@ -379,6 +379,26 @@ test('import reuses existing private key fingerprint on same team', function () expect($server->private_key_id)->toBe($originalKeyId); }); +test('import into another team creates its own key when the same key exists in a different team', function () { + $bundle = $this->exporter->export($this->server); + $sourceKeyId = $this->privateKey->id; + $targetTeam = Team::factory()->create(); + + // Free the IP without deleting the source team's key. + $this->application->forceDelete(); + $this->database->forceDelete(); + $this->server->forceDelete(); + + $result = $this->importer->import($bundle, teamId: $targetTeam->id); + + $server = Server::where('uuid', $result['server_uuid'])->first(); + expect($server->team_id)->toBe($targetTeam->id) + ->and($server->private_key_id)->not->toBe($sourceKeyId) + ->and($server->privateKey->team_id)->toBe($targetTeam->id) + ->and($server->privateKey->fingerprint)->toBe($this->privateKey->fingerprint) + ->and(PrivateKey::whereKey($sourceKeyId)->where('team_id', $this->team->id)->exists())->toBeTrue(); +}); + test('encrypted export decrypts for import', function () { $bundle = $this->exporter->export($this->server); $encrypted = ServerTransferBundle::encryptWithPassphrase($bundle, 'transfer-pass');