Skip to content

Commit 6554164

Browse files
committed
Add test coverage for the SSH key endpoints
1 parent 97280a6 commit 6554164

File tree

7 files changed

+201
-7
lines changed

7 files changed

+201
-7
lines changed

app/Http/Controllers/Api/Client/SSHKeyController.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Illuminate\Http\JsonResponse;
66
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
7-
use Pterodactyl\Transformers\Api\Client\SSHKeyTransformer;
7+
use Pterodactyl\Transformers\Api\Client\UserSSHKeyTransformer;
88
use Pterodactyl\Http\Requests\Api\Client\Account\StoreSSHKeyRequest;
99

1010
class SSHKeyController extends ClientApiController
@@ -16,7 +16,7 @@ class SSHKeyController extends ClientApiController
1616
public function index(ClientApiRequest $request): array
1717
{
1818
return $this->fractal->collection($request->user()->sshKeys)
19-
->transformWith($this->getTransformer(SSHKeyTransformer::class))
19+
->transformWith($this->getTransformer(UserSSHKeyTransformer::class))
2020
->toArray();
2121
}
2222

@@ -32,7 +32,7 @@ public function store(StoreSSHKeyRequest $request): array
3232
]);
3333

3434
return $this->fractal->item($model)
35-
->transformWith($this->getTransformer(SSHKeyTransformer::class))
35+
->transformWith($this->getTransformer(UserSSHKeyTransformer::class))
3636
->toArray();
3737
}
3838

app/Http/Requests/Api/Client/Account/StoreSSHKeyRequest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ public function withValidator(Validator $validator): void
4343
}
4444

4545
if ($this->key instanceof DSA) {
46-
$this->validator->errors()->add('public_key', 'DSA public keys are not supported.');
46+
$this->validator->errors()->add('public_key', 'DSA keys are not supported.');
4747
}
4848

4949
if ($this->key instanceof RSA && $this->key->getLength() < 2048) {
50-
$this->validator->errors()->add('public_key', 'RSA keys must be at 2048 bytes.');
50+
$this->validator->errors()->add('public_key', 'RSA keys must be at least 2048 bytes in length.');
5151
}
5252

5353
$fingerprint = $this->key->getFingerprint('sha256');

app/Models/UserSSHKey.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* @property \Illuminate\Support\Carbon|null $updated_at
1818
* @property \Illuminate\Support\Carbon|null $deleted_at
1919
* @property \Pterodactyl\Models\User $user
20-
*
2120
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newModelQuery()
2221
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newQuery()
2322
* @method static \Illuminate\Database\Query\Builder|UserSSHKey onlyTrashed()
@@ -33,6 +32,7 @@
3332
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withTrashed()
3433
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withoutTrashed()
3534
* @mixin \Eloquent
35+
* @method static \Database\Factories\UserSSHKeyFactory factory(...$parameters)
3636
*/
3737
class UserSSHKey extends Model
3838
{

app/Transformers/Api/Client/SSHKeyTransformer.php renamed to app/Transformers/Api/Client/UserSSHKeyTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Pterodactyl\Models\UserSSHKey;
66

7-
class SSHKeyTransformer extends BaseClientTransformer
7+
class UserSSHKeyTransformer extends BaseClientTransformer
88
{
99
public function getResourceName(): string
1010
{
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use Illuminate\Database\Eloquent\Factories\Factory;
6+
7+
class UserSSHKeyFactory extends Factory
8+
{
9+
/**
10+
* Returns a fake public key for use on the system.
11+
*
12+
* @return array
13+
*/
14+
public function definition()
15+
{
16+
return [
17+
'name' => $this->faker->name(),
18+
'public_key' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaXIq09NH4a93EVdrvHYiZ67Wj+GBEBQ9ou4W0qSYm2',
19+
'fingerprint' => 'T2b3VnvHWUmfhDOFLqzZg2VfLgqJhWxzrMUy8WZ+V8M',
20+
];
21+
}
22+
23+
/**
24+
* Returns a DSA public key.
25+
*/
26+
public function dsa(): self
27+
{
28+
return $this->state([
29+
'public_key' => 'ssh-dss AAAAB3NzaC1kc3MAAACBAPfiWwEFvBOafdUmHDPjXsUttt+65FHSZSCVVeEFOTaL7Y3d0CJyrtck8KS1vmXHSb8QFBY2B1yVSb/reaQvNreWZN3KDYfLbF57/zimBn+IrHrJR+ZglhOxDRHoGPWK7q9jYIrOLwoOjkNKXxz1eOHKUgufFfSNtIRLycEXczLrAAAAFQC6LnBErezotG52jN4JostfC/TfEwAAAIACuTxRzYFDXHAxFICeqqY9w+y+v2yQfdeQ1BgCq2GMagUYfOdqnjizTO9M614r/nXZK1SV10TqhUcQtkJzDQIUtBqzBF5cIC/1cIFKzXi5rNHs8Y4bz/PBD+EbQJdiy+1so1oi790r710bqnkzTravAOJ5rGyfuQRLt+f+kuS9NAAAAIEA7tjGtJuXGUtPIXfnrMYS1iOWryO4irqnvaWfel002/DaGaNjRghNe/cUBYlAsjPhGJ1F7BQlLAY1koliTY6l0svs7ZPBM5QOumrr8OaNXGGVIq/RkkxuZHmRoUL2qH3DGYaktPUn4vFPliiAmGWOHAEu1K6B4g4vG/SKgMRpIvc=',
30+
'fingerprint' => '9Fb/RODt9N6aldcB+lc6ih0ovr2G/JUjts2Wh21uxYI',
31+
]);
32+
}
33+
34+
/**
35+
* Returns an RSA public key, if "weak" is specified a 1024-bit RSA key is returned
36+
* which should fail validation when being stored.
37+
*/
38+
public function rsa(bool $weak = false): self
39+
{
40+
if (!$weak) {
41+
return $this->state([
42+
'public_key' => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo/YLm2SPSlOIG7AagBSmEe5c0b2PLPzUGFp3gARhD6n6ydBS40TlWzeg2qV95lh6fWBd8LsNgPOFmmuKuNZdBjAGeTY4gxKfHY1vK5/zOI4jPPqAMcCMNfd82aM97kx6dO8Hw1R79OyVpOZylpXLHayVPGHUK37Tpih4W7TeVSMrOqQF9F72lzhwgEtkdjm4gLBL6RpdNXrdnjIaNVnuade0Sb3w384vecZPe+S/997WirOMNy2JU4NdMHEnSjd1/i463RpN96AsXFAu1zl9nrXVhA7DVfSHoigXAqbs/xav8PRpLgAKjYpPohxQ9Nu6tP5jRUhfWdYwNFFp/aWloD/0JdP9LqcBBc9sO9TLkz3fBiUf11VM/QT1UhO84G+ahMxVn95jA472VPUe8uKff69lzbvSavEE6qcQX2TzVKOSi1E26Fzc6IZ/tHEuGEbGFxTsiQ1GysVZ0wr1p6ftd1SVqH5F/oaEK7UO8+xn/syEqaPf6A0eJWRNc0+lHA1sIRjmo9MOBvbkKExkx5JLHgGG81DYDFdZUuHY1BgSxJJcmNWV5BKRm350EbgRngoYI5tB3tCiZVW1PI8qyff9mBae11LY5GPlUeDnPrMvSdCKMIWrg7nC8SbndBCO3Fx4z7G2dTQy4ZmY7Ae9jR4pyg7tTOI3qgl8Z462GZi/jzw==',
43+
'fingerprint' => 'vjccQdGfqAvuEK3Bki1Ji6aOo3rIuGU0BGJ0ml4CjLQ',
44+
]);
45+
}
46+
47+
return $this->state([
48+
'public_key' => 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4VVsHFO5MxvCtAPyoKGANWyuwZ4fvllvFog5RJbfpDpw8etDFVGEXl+uRR8p79g9oV7MscoFo6HiWrJc4/4vlP665msjosILdIcbnuzMhvXnKisaGh9zflkpyR3KhUxoHxqYp2q8XtffjKKAHz1a8o7OUG6fwaKIqu+d0PoICZQ==',
49+
'fingerprint' => 'LQSzAAfAsbKpU94gojxXfjGjYcEv8UZIwyzwhcEr/aw',
50+
]);
51+
}
52+
}

tests/Integration/Api/Client/ClientApiIntegrationTestCase.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Pterodactyl\Models\Schedule;
1616
use Illuminate\Support\Collection;
1717
use Pterodactyl\Models\Allocation;
18+
use Pterodactyl\Models\UserSSHKey;
1819
use Pterodactyl\Models\DatabaseHost;
1920
use Pterodactyl\Tests\Integration\TestResponse;
2021
use Pterodactyl\Tests\Integration\IntegrationTestCase;
@@ -77,6 +78,9 @@ protected function link($model, $append = null): string
7778
case Backup::class:
7879
$link = "/api/client/servers/{$model->server->uuid}/backups/{$model->uuid}";
7980
break;
81+
case UserSSHKey::class:
82+
$link = "/api/client/account/ssh-keys/$model->fingerprint";
83+
break;
8084
default:
8185
throw new InvalidArgumentException(sprintf('Cannot create link for Model of type %s', class_basename($model)));
8286
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client;
4+
5+
use phpseclib3\Crypt\EC;
6+
use Pterodactyl\Models\User;
7+
use Pterodactyl\Models\UserSSHKey;
8+
9+
class SSHKeyControllerTest extends ClientApiIntegrationTestCase
10+
{
11+
/**
12+
* Test that only the SSH keys for the authenticated user are returned.
13+
*/
14+
public function testSSHKeysAreReturned()
15+
{
16+
$user = User::factory()->create();
17+
$user2 = User::factory()->create();
18+
19+
$key = UserSSHKey::factory()->for($user)->create();
20+
UserSSHKey::factory()->for($user2)->rsa()->create();
21+
22+
$this->actingAs($user);
23+
$response = $this->getJson('/api/client/account/ssh-keys')
24+
->assertOk()
25+
->assertJsonPath('object', 'list')
26+
->assertJsonPath('data.0.object', UserSSHKey::RESOURCE_NAME);
27+
28+
$this->assertJsonTransformedWith($response->json('data.0.attributes'), $key);
29+
}
30+
31+
/**
32+
* Test that a user's SSH key can be deleted, and that passing the fingerprint
33+
* of another user's SSH key won't delete that key.
34+
*/
35+
public function testSSHKeyCanBeDeleted()
36+
{
37+
$user = User::factory()->create();
38+
$user2 = User::factory()->create();
39+
40+
$key = UserSSHKey::factory()->for($user)->create();
41+
$key2 = UserSSHKey::factory()->for($user2)->create();
42+
43+
$this->actingAs($user);
44+
$this->deleteJson($this->link($key))->assertNoContent();
45+
46+
$this->assertSoftDeleted($key);
47+
$this->assertNotSoftDeleted($key2);
48+
49+
$this->deleteJson($this->link($key))->assertNoContent();
50+
$this->deleteJson($this->link($key2))->assertNoContent();
51+
52+
$this->assertNotSoftDeleted($key2);
53+
}
54+
55+
public function testDSAKeyIsRejected()
56+
{
57+
$user = User::factory()->create();
58+
$key = UserSSHKey::factory()->dsa()->make();
59+
60+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
61+
'name' => 'Name',
62+
'public_key' => $key->public_key,
63+
])
64+
->assertUnprocessable()
65+
->assertJsonPath('errors.0.detail', 'DSA keys are not supported.');
66+
67+
$this->assertEquals(0, $user->sshKeys()->count());
68+
}
69+
70+
public function testWeakRSAKeyIsRejected()
71+
{
72+
$user = User::factory()->create();
73+
$key = UserSSHKey::factory()->rsa(true)->make();
74+
75+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
76+
'name' => 'Name',
77+
'public_key' => $key->public_key,
78+
])
79+
->assertUnprocessable()
80+
->assertJsonPath('errors.0.detail', 'RSA keys must be at least 2048 bytes in length.');
81+
82+
$this->assertEquals(0, $user->sshKeys()->count());
83+
}
84+
85+
public function testInvalidOrPrivateKeyIsRejected()
86+
{
87+
$user = User::factory()->create();
88+
89+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
90+
'name' => 'Name',
91+
'public_key' => 'invalid',
92+
])
93+
->assertUnprocessable()
94+
->assertJsonPath('errors.0.detail', 'The public key provided is not valid.');
95+
96+
$this->assertEquals(0, $user->sshKeys()->count());
97+
98+
$key = EC::createKey('Ed25519');
99+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
100+
'name' => 'Name',
101+
'public_key' => $key->toString('PKCS8'),
102+
])
103+
->assertUnprocessable()
104+
->assertJsonPath('errors.0.detail', 'The public key provided is not valid.');
105+
}
106+
107+
public function testPublicKeyCanBeStored()
108+
{
109+
$user = User::factory()->create();
110+
$key = UserSSHKey::factory()->make();
111+
112+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
113+
'name' => 'Name',
114+
'public_key' => $key->public_key,
115+
])
116+
->assertOk()
117+
->assertJsonPath('object', UserSSHKey::RESOURCE_NAME)
118+
->assertJsonPath('attributes.public_key', $key->public_key);
119+
120+
$this->assertCount(1, $user->sshKeys);
121+
$this->assertEquals($key->public_key, $user->sshKeys[0]->public_key);
122+
}
123+
124+
public function testPublicKeyThatAlreadyExistsCannotBeAddedASecondTime()
125+
{
126+
$user = User::factory()->create();
127+
$key = UserSSHKey::factory()->for($user)->create();
128+
129+
$this->actingAs($user)->postJson('/api/client/account/ssh-keys', [
130+
'name' => 'Name',
131+
'public_key' => $key->public_key,
132+
])
133+
->assertUnprocessable()
134+
->assertJsonPath('errors.0.detail', 'The public key provided already exists on your account.');
135+
136+
$this->assertEquals(1, $user->sshKeys()->count());
137+
}
138+
}

0 commit comments

Comments
 (0)