Skip to content

Commit 97280a6

Browse files
committed
Add support for storing SSH keys on user accounts
1 parent 5705d7d commit 97280a6

File tree

20 files changed

+678
-6
lines changed

20 files changed

+678
-6
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Controllers\Api\Client;
4+
5+
use Illuminate\Http\JsonResponse;
6+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
7+
use Pterodactyl\Transformers\Api\Client\SSHKeyTransformer;
8+
use Pterodactyl\Http\Requests\Api\Client\Account\StoreSSHKeyRequest;
9+
10+
class SSHKeyController extends ClientApiController
11+
{
12+
/**
13+
* Returns all of the SSH keys that have been configured for the logged in
14+
* user account.
15+
*/
16+
public function index(ClientApiRequest $request): array
17+
{
18+
return $this->fractal->collection($request->user()->sshKeys)
19+
->transformWith($this->getTransformer(SSHKeyTransformer::class))
20+
->toArray();
21+
}
22+
23+
/**
24+
* Stores a new SSH key for the authenticated user's account.
25+
*/
26+
public function store(StoreSSHKeyRequest $request): array
27+
{
28+
$model = $request->user()->sshKeys()->create([
29+
'name' => $request->input('name'),
30+
'public_key' => $request->input('public_key'),
31+
'fingerprint' => $request->getKeyFingerprint(),
32+
]);
33+
34+
return $this->fractal->item($model)
35+
->transformWith($this->getTransformer(SSHKeyTransformer::class))
36+
->toArray();
37+
}
38+
39+
/**
40+
* Deletes an SSH key from the user's account.
41+
*/
42+
public function delete(ClientApiRequest $request, string $identifier): JsonResponse
43+
{
44+
$request->user()->sshKeys()->where('fingerprint', $identifier)->delete();
45+
46+
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
47+
}
48+
}

app/Http/Requests/Api/Application/ApplicationApiRequest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pterodactyl\Http\Requests\Api\Application;
44

55
use Pterodactyl\Models\ApiKey;
6+
use Illuminate\Validation\Validator;
67
use Pterodactyl\Services\Acl\Api\AdminAcl;
78
use Illuminate\Foundation\Http\FormRequest;
89
use Pterodactyl\Exceptions\PterodactylException;
@@ -96,6 +97,16 @@ public function getModel(string $model)
9697
return $this->route()->parameter($parameterKey);
9798
}
9899

100+
/**
101+
* Helper method allowing a developer to easily hook into this logic without having
102+
* to remember what the method name is called or where to use it. By default this is
103+
* a no-op.
104+
*/
105+
public function withValidator(Validator $validator): void
106+
{
107+
// do nothing
108+
}
109+
99110
/**
100111
* Validate that the resource exists and can be accessed prior to booting
101112
* the validator and attempting to use the data.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Account;
4+
5+
use Exception;
6+
use phpseclib3\Crypt\DSA;
7+
use phpseclib3\Crypt\RSA;
8+
use Pterodactyl\Models\UserSSHKey;
9+
use Illuminate\Validation\Validator;
10+
use phpseclib3\Crypt\PublicKeyLoader;
11+
use phpseclib3\Crypt\Common\PublicKey;
12+
use phpseclib3\Exception\NoKeyLoadedException;
13+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
14+
15+
class StoreSSHKeyRequest extends ClientApiRequest
16+
{
17+
protected ?PublicKey $key;
18+
19+
/**
20+
* Returns the rules for this request.
21+
*/
22+
public function rules(): array
23+
{
24+
return [
25+
'name' => UserSSHKey::getRulesForField('name'),
26+
'public_key' => UserSSHKey::getRulesForField('public_key'),
27+
];
28+
}
29+
30+
/**
31+
* Check to see if this SSH key has already been added to the user's account
32+
* and if so return an error.
33+
*/
34+
public function withValidator(Validator $validator): void
35+
{
36+
$validator->after(function () {
37+
try {
38+
$this->key = PublicKeyLoader::loadPublicKey($this->input('public_key'));
39+
} catch (NoKeyLoadedException $exception) {
40+
$this->validator->errors()->add('public_key', 'The public key provided is not valid.');
41+
42+
return;
43+
}
44+
45+
if ($this->key instanceof DSA) {
46+
$this->validator->errors()->add('public_key', 'DSA public keys are not supported.');
47+
}
48+
49+
if ($this->key instanceof RSA && $this->key->getLength() < 2048) {
50+
$this->validator->errors()->add('public_key', 'RSA keys must be at 2048 bytes.');
51+
}
52+
53+
$fingerprint = $this->key->getFingerprint('sha256');
54+
if ($this->user()->sshKeys()->where('fingerprint', $fingerprint)->exists()) {
55+
$this->validator->errors()->add('public_key', 'The public key provided already exists on your account.');
56+
}
57+
});
58+
}
59+
60+
/**
61+
* Returns the SHA256 fingerprint of the key provided.
62+
*/
63+
public function getKeyFingerprint(): string
64+
{
65+
if (!$this->key) {
66+
throw new Exception('The public key was not properly loaded for this request.');
67+
}
68+
69+
return $this->key->getFingerprint('sha256');
70+
}
71+
}

app/Models/User.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
use Illuminate\Database\Eloquent\Builder;
1111
use Illuminate\Auth\Passwords\CanResetPassword;
1212
use Pterodactyl\Traits\Helpers\AvailableLanguages;
13+
use Illuminate\Database\Eloquent\Relations\HasMany;
1314
use Illuminate\Foundation\Auth\Access\Authorizable;
1415
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
1516
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
1617
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
1718
use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
1819

1920
/**
21+
* \Pterodactyl\Models\User.
22+
*
2023
* @property int $id
2124
* @property string|null $external_id
2225
* @property string $uuid
@@ -38,6 +41,37 @@
3841
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
3942
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
4043
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
44+
* @property string|null $remember_token
45+
* @property int|null $api_keys_count
46+
* @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
47+
* @property int|null $notifications_count
48+
* @property int|null $recovery_tokens_count
49+
* @property int|null $servers_count
50+
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\UserSSHKey[] $sshKeys
51+
* @property int|null $ssh_keys_count
52+
*
53+
* @method static \Database\Factories\UserFactory factory(...$parameters)
54+
* @method static Builder|User newModelQuery()
55+
* @method static Builder|User newQuery()
56+
* @method static Builder|User query()
57+
* @method static Builder|User whereCreatedAt($value)
58+
* @method static Builder|User whereEmail($value)
59+
* @method static Builder|User whereExternalId($value)
60+
* @method static Builder|User whereGravatar($value)
61+
* @method static Builder|User whereId($value)
62+
* @method static Builder|User whereLanguage($value)
63+
* @method static Builder|User whereNameFirst($value)
64+
* @method static Builder|User whereNameLast($value)
65+
* @method static Builder|User wherePassword($value)
66+
* @method static Builder|User whereRememberToken($value)
67+
* @method static Builder|User whereRootAdmin($value)
68+
* @method static Builder|User whereTotpAuthenticatedAt($value)
69+
* @method static Builder|User whereTotpSecret($value)
70+
* @method static Builder|User whereUpdatedAt($value)
71+
* @method static Builder|User whereUseTotp($value)
72+
* @method static Builder|User whereUsername($value)
73+
* @method static Builder|User whereUuid($value)
74+
* @mixin \Eloquent
4175
*/
4276
class User extends Model implements
4377
AuthenticatableContract,
@@ -225,6 +259,11 @@ public function recoveryTokens()
225259
return $this->hasMany(RecoveryToken::class);
226260
}
227261

262+
public function sshKeys(): HasMany
263+
{
264+
return $this->hasMany(UserSSHKey::class);
265+
}
266+
228267
/**
229268
* Returns all of the servers that a user can access by way of being the owner of the
230269
* server, or because they are assigned as a subuser for that server.

app/Models/UserSSHKey.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Pterodactyl\Models;
4+
5+
use Illuminate\Database\Eloquent\SoftDeletes;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
/**
9+
* \Pterodactyl\Models\UserSSHKey.
10+
*
11+
* @property int $id
12+
* @property int $user_id
13+
* @property string $name
14+
* @property string $fingerprint
15+
* @property string $public_key
16+
* @property \Illuminate\Support\Carbon|null $created_at
17+
* @property \Illuminate\Support\Carbon|null $updated_at
18+
* @property \Illuminate\Support\Carbon|null $deleted_at
19+
* @property \Pterodactyl\Models\User $user
20+
*
21+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newModelQuery()
22+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newQuery()
23+
* @method static \Illuminate\Database\Query\Builder|UserSSHKey onlyTrashed()
24+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey query()
25+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereCreatedAt($value)
26+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereDeletedAt($value)
27+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereFingerprint($value)
28+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereId($value)
29+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereName($value)
30+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey wherePublicKey($value)
31+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereUpdatedAt($value)
32+
* @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey whereUserId($value)
33+
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withTrashed()
34+
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withoutTrashed()
35+
* @mixin \Eloquent
36+
*/
37+
class UserSSHKey extends Model
38+
{
39+
use SoftDeletes;
40+
41+
public const RESOURCE_NAME = 'ssh_key';
42+
43+
protected $table = 'user_ssh_keys';
44+
45+
protected $fillable = [
46+
'name',
47+
'public_key',
48+
'fingerprint',
49+
];
50+
51+
public static $validationRules = [
52+
'name' => ['required', 'string'],
53+
'fingerprint' => ['required', 'string'],
54+
'public_key' => ['required', 'string'],
55+
];
56+
57+
public function user(): BelongsTo
58+
{
59+
return $this->belongsTo(User::class);
60+
}
61+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Pterodactyl\Transformers\Api\Client;
4+
5+
use Pterodactyl\Models\UserSSHKey;
6+
7+
class SSHKeyTransformer extends BaseClientTransformer
8+
{
9+
public function getResourceName(): string
10+
{
11+
return UserSSHKey::RESOURCE_NAME;
12+
}
13+
14+
/**
15+
* Return's a user's SSH key in an API response format.
16+
*/
17+
public function transform(UserSSHKey $model): array
18+
{
19+
return [
20+
'name' => $model->name,
21+
'fingerprint' => $model->fingerprint,
22+
'public_key' => $model->public_key,
23+
'created_at' => $model->created_at->toIso8601String(),
24+
];
25+
}
26+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"league/flysystem-aws-s3-v3": "~1.0.29",
3131
"league/flysystem-memory": "~1.0.2",
3232
"matriphe/iso-639": "~1.2.0",
33+
"phpseclib/phpseclib": "~3.0",
3334
"pragmarx/google2fa": "~5.0.0",
3435
"predis/predis": "~1.1.10",
3536
"prologue/alerts": "~0.4.8",

0 commit comments

Comments
 (0)