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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-28 11:02:19 +02:00
co-authored by Claude Opus 5.5
parent df9f1555b3
commit 1712fb393a
2 changed files with 25 additions and 7 deletions
@@ -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;
}
}
@@ -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');