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');