From b15679d3bb04ad45c40ff58ecb53c6dea41d929a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 10:49:36 -0800 Subject: [PATCH 01/23] Add base logic for audit logging --- app/Models/AuditLog.php | 120 ++++++++++++++++++ app/Models/Server.php | 26 ++++ ...1_01_17_102401_create_audit_logs_table.php | 31 +++++ 3 files changed, 177 insertions(+) create mode 100644 app/Models/AuditLog.php create mode 100644 database/migrations/2021_01_17_102401_create_audit_logs_table.php diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php new file mode 100644 index 00000000..fdf166e6 --- /dev/null +++ b/app/Models/AuditLog.php @@ -0,0 +1,120 @@ + 'required|uuid', + 'action' => 'required|string', + 'device' => 'required|array', + 'device.ip_address' => 'ip', + 'device.user_agent' => 'string', + 'metadata' => 'array', + ]; + + /** + * @var string + */ + protected $table = 'audit_logs'; + + /** + * @var bool + */ + protected $immutableDates = true; + + /** + * @var string[] + */ + protected $casts = [ + 'device' => 'array', + 'metadata' => 'array', + ]; + + /** + * @var string[] + */ + protected $guarded = [ + 'id', + 'created_at', + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function server() + { + return $this->belongsTo(Server::class); + } + + /** + * Creates a new AuditLog model and returns it, attaching device information and the + * currently authenticated user if available. This model is not saved at this point, so + * you can always make modifications to it as needed before saving. + * + * @param string $action + * @param array $metadata + * @param bool $isSystem + * @return $this + */ + public static function factory(string $action, array $metadata, bool $isSystem = false) + { + /** @var \Illuminate\Http\Request $request */ + $request = Container::getInstance()->make('request'); + if (! $isSystem || ! $request instanceof Request) { + $request = null; + } + + return (new self())->fill([ + 'uuid' => Uuid::uuid4()->toString(), + 'is_system' => $isSystem, + 'user_id' => $request->user() ? $request->user()->id : null, + 'server_id' => null, + 'action' => $action, + 'device' => $request ? [ + 'ip_address' => $request->getClientIp(), + 'user_agent' => $request->userAgent(), + ] : [], + 'metadata' => $metadata, + ]); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index aace86d0..b617db0a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -50,6 +50,7 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property \Pterodactyl\Models\ServerTransfer $transfer * @property \Pterodactyl\Models\Backup[]|\Illuminate\Database\Eloquent\Collection $backups * @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts + * @property \Pterodactyl\Models\AuditLog[] $audits */ class Server extends Model { @@ -326,4 +327,29 @@ class Server extends Model { return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id'); } + + /** + * Saves an audit entry to the database for the server. + * + * @param string $action + * @param array $metadata + * @return \Pterodactyl\Models\AuditLog + */ + public function audit(string $action, array $metadata): AuditLog + { + $model = AuditLog::factory($action, $metadata)->fill([ + 'server_id' => $this->id, + ]); + $model->save(); + + return $model; + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function audits() + { + return $this->hasMany(AuditLog::class); + } } diff --git a/database/migrations/2021_01_17_102401_create_audit_logs_table.php b/database/migrations/2021_01_17_102401_create_audit_logs_table.php new file mode 100644 index 00000000..7586c1b8 --- /dev/null +++ b/database/migrations/2021_01_17_102401_create_audit_logs_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('audit_logs'); + } +} From ccecaa6694333d5758ddfff91c1a6cc933549674 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 11:46:08 -0800 Subject: [PATCH 02/23] Add basic auditing for filesystem actions Specifically skipping read actions since there isn't much to say there, and it generally wouldn't be very helpful (plus, likely to generate lots of logs). --- .../Api/Client/Servers/BackupController.php | 25 ++- .../Api/Client/Servers/FileController.php | 143 ++++++++++++------ app/Models/AuditLog.php | 18 ++- app/Models/Server.php | 29 +++- ...1_01_17_102401_create_audit_logs_table.php | 13 +- 5 files changed, 164 insertions(+), 64 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index 23daf1bc..5ba8475e 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Pterodactyl\Models\Backup; use Pterodactyl\Models\Server; +use Pterodactyl\Models\AuditLog; use Illuminate\Http\JsonResponse; use Pterodactyl\Services\Backups\DeleteBackupService; use Pterodactyl\Repositories\Eloquent\BackupRepository; @@ -61,6 +62,7 @@ class BackupController extends ClientApiController public function index(GetBackupsRequest $request, Server $server) { $limit = min($request->query('per_page') ?? 20, 50); + return $this->fractal->collection($server->backups()->paginate($limit)) ->transformWith($this->getTransformer(BackupTransformer::class)) ->toArray(); @@ -77,11 +79,18 @@ class BackupController extends ClientApiController */ public function store(StoreBackupRequest $request, Server $server) { - $backup = $this->initiateBackupService - ->setIgnoredFiles( - explode(PHP_EOL, $request->input('ignored') ?? '') - ) - ->handle($server, $request->input('name')); + /** @var \Pterodactyl\Models\Backup $backup */ + $backup = $server->audit(AuditLog::ACTION_SERVER_BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) { + $backup = $this->initiateBackupService + ->setIgnoredFiles( + explode(PHP_EOL, $request->input('ignored') ?? '') + ) + ->handle($server, $request->input('name')); + + $model->metadata = ['backup_uuid' => $backup->uuid]; + + return $backup; + }); return $this->fractal->item($backup) ->transformWith($this->getTransformer(BackupTransformer::class)) @@ -116,8 +125,10 @@ class BackupController extends ClientApiController */ public function delete(DeleteBackupRequest $request, Server $server, Backup $backup) { - $this->deleteBackupService->handle($backup); + $server->audit(AuditLog::ACTION_SERVER_BACKUP_DELETED, function () use ($backup) { + $this->deleteBackupService->handle($backup); + }); - return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT); + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } } diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 317115b2..782d948f 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Carbon\CarbonImmutable; use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Pterodactyl\Models\AuditLog; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Collection; use Pterodactyl\Services\Nodes\NodeJWTService; use Illuminate\Contracts\Routing\ResponseFactory; use Pterodactyl\Repositories\Wings\DaemonFileRepository; @@ -87,18 +87,15 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\Response * - * @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function contents(GetFileContentsRequest $request, Server $server): Response { - return new Response( - $this->fileRepository->setServer($server)->getContent( - $request->get('file'), config('pterodactyl.files.max_edit_size') - ), - Response::HTTP_OK, - ['Content-Type' => 'text/plain'] + $response = $this->fileRepository->setServer($server)->getContent( + $request->get('file'), config('pterodactyl.files.max_edit_size') ); + + return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']); } /** @@ -109,17 +106,21 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return array * - * @throws \Exception + * @throws \Throwable */ public function download(GetFileContentsRequest $request, Server $server) { - $token = $this->jwtService - ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) - ->setClaims([ - 'file_path' => rawurldecode($request->get('file')), - 'server_uuid' => $server->uuid, - ]) - ->handle($server->node, $request->user()->id . $server->uuid); + $token = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DOWNLOAD, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = ['file' => $request->get('file')]; + + return $this->jwtService + ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) + ->setClaims([ + 'file_path' => rawurldecode($request->get('file')), + 'server_uuid' => $server->uuid, + ]) + ->handle($server->node, $request->user()->id . $server->uuid); + }); return [ 'object' => 'signed_url', @@ -140,11 +141,20 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function write(WriteFileContentRequest $request, Server $server): JsonResponse { - $this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent()); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = [ + 'file' => $request->get('file'), + 'sub_action' => 'write_content', + ]; + + $this->fileRepository + ->setServer($server) + ->putContent($request->get('file'), $request->getContent()); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -156,13 +166,20 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function create(CreateFolderRequest $request, Server $server): JsonResponse { - $this->fileRepository - ->setServer($server) - ->createDirectory($request->input('name'), $request->input('root', '/')); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = [ + 'file' => $request->input('root', '/') . $request->input('name'), + 'sub_action' => 'create_folder', + ]; + + $this->fileRepository + ->setServer($server) + ->createDirectory($request->input('name'), $request->input('root', '/')); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -174,13 +191,17 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function rename(RenameFileRequest $request, Server $server): JsonResponse { - $this->fileRepository - ->setServer($server) - ->renameFiles($request->input('root'), $request->input('files')); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_RENAME, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')]; + + $this->fileRepository + ->setServer($server) + ->renameFiles($request->input('root'), $request->input('files')); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -192,13 +213,19 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function copy(CopyFileRequest $request, Server $server): JsonResponse { - $this->fileRepository - ->setServer($server) - ->copyFile($request->input('location')); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = [ + 'file' => $request->input('location'), + 'sub_action' => 'copy_file', + ]; + $this->fileRepository + ->setServer($server) + ->copyFile($request->input('location')); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -208,17 +235,21 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return array * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function compress(CompressFilesRequest $request, Server $server): array { - // Allow up to five minutes for this request to process before timing out. - set_time_limit(300); + $file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_COMPRESS, function (AuditLog $audit, Server $server) use ($request) { + // Allow up to five minutes for this request to process before timing out. + set_time_limit(300); - $file = $this->fileRepository->setServer($server) - ->compressFiles( - $request->input('root'), $request->input('files') - ); + $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')]; + + return $this->fileRepository->setServer($server) + ->compressFiles( + $request->input('root'), $request->input('files') + ); + }); return $this->fractal->item($file) ->transformWith($this->getTransformer(FileObjectTransformer::class)) @@ -230,15 +261,19 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse { - // Allow up to five minutes for this request to process before timing out. - set_time_limit(300); + $file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DECOMPRESS, function (AuditLog $audit, Server $server) use ($request) { + // Allow up to five minutes for this request to process before timing out. + set_time_limit(300); - $this->fileRepository->setServer($server) - ->decompressFile($request->input('root'), $request->input('file')); + $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('file')]; + + $this->fileRepository->setServer($server) + ->decompressFile($request->input('root'), $request->input('file')); + }); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } @@ -250,14 +285,18 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function delete(DeleteFileRequest $request, Server $server): JsonResponse { - $this->fileRepository->setServer($server) - ->deleteFiles( - $request->input('root'), $request->input('files') - ); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DELETE, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')]; + + $this->fileRepository->setServer($server) + ->deleteFiles( + $request->input('root'), $request->input('files') + ); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -288,11 +327,15 @@ class FileController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Throwable */ public function pull(PullFileRequest $request, Server $server): JsonResponse { - $this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory')); + $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_PULL, function (AuditLog $audit, Server $server) use ($request) { + $audit->metadata = ['directory' => $request->input('directory'), 'url' => $request->input('url')]; + + $this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory')); + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php index fdf166e6..7a056326 100644 --- a/app/Models/AuditLog.php +++ b/app/Models/AuditLog.php @@ -28,6 +28,18 @@ class AuditLog extends Model const ACTION_USER_AUTH_FAILED = 'user:auth.failed'; const ACTION_USER_AUTH_PASSWORD_CHANGED = 'user:auth.password-changed'; + const ACTION_SERVER_FILESYSTEM_DOWNLOAD = 'server:filesystem.download'; + const ACTION_SERVER_FILESYSTEM_WRITE = 'server:filesystem.write'; + const ACTION_SERVER_FILESYSTEM_DELETE = 'server:filesystem.delete'; + const ACTION_SERVER_FILESYSTEM_RENAME = 'server:filesystem.rename'; + const ACTION_SERVER_FILESYSTEM_COMPRESS = 'server:filesystem.compress'; + const ACTION_SERVER_FILESYSTEM_DECOMPRESS = 'server:filesystem.decompress'; + const ACTION_SERVER_FILESYSTEM_PULL = 'server:filesystem.pull'; + + const ACTION_SERVER_BACKUP_STARTED = 'server:backup.started'; + const ACTION_SERVER_BACKUP_FAILED = 'server:backup.failed'; + const ACTION_SERVER_BACKUP_COMPELTED = 'server:backup.completed'; + const ACTION_SERVER_BACKUP_DELETED = 'server:backup.deleted'; const ACTION_SERVER_BACKUP_RESTORE_STARTED = 'server:backup.restore.started'; const ACTION_SERVER_BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed'; const ACTION_SERVER_BACKUP_RESTORE_FAILED = 'server:backup.restore.failed'; @@ -38,7 +50,7 @@ class AuditLog extends Model public static $validationRules = [ 'uuid' => 'required|uuid', 'action' => 'required|string', - 'device' => 'required|array', + 'device' => 'array', 'device.ip_address' => 'ip', 'device.user_agent' => 'string', 'metadata' => 'array', @@ -100,14 +112,14 @@ class AuditLog extends Model { /** @var \Illuminate\Http\Request $request */ $request = Container::getInstance()->make('request'); - if (! $isSystem || ! $request instanceof Request) { + if ($isSystem || ! $request instanceof Request) { $request = null; } return (new self())->fill([ 'uuid' => Uuid::uuid4()->toString(), 'is_system' => $isSystem, - 'user_id' => $request->user() ? $request->user()->id : null, + 'user_id' => ($request && $request->user()) ? $request->user()->id : null, 'server_id' => null, 'action' => $action, 'device' => $request ? [ diff --git a/app/Models/Server.php b/app/Models/Server.php index b617db0a..b65ef662 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Models; +use Closure; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Query\JoinClause; use Znck\Eloquent\Traits\BelongsToThrough; @@ -335,7 +336,7 @@ class Server extends Model * @param array $metadata * @return \Pterodactyl\Models\AuditLog */ - public function audit(string $action, array $metadata): AuditLog + public function newAuditEvent(string $action, array $metadata): AuditLog { $model = AuditLog::factory($action, $metadata)->fill([ 'server_id' => $this->id, @@ -345,6 +346,32 @@ class Server extends Model return $model; } + /** + * Stores a new audit event for a server by using a transaction. If the transaction + * fails for any reason everything executed within will be rolled back. The callback + * passed in will receive the AuditLog model before it is saved and the second argument + * will be the current server instance. The callback should modify the audit entry as + * needed before finishing, any changes will be persisted. + * + * The response from the callback is returned to the caller. + * + * @param string $action + * @param \Closure $callback + * @return mixed + * @throws \Throwable + */ + public function audit(string $action, Closure $callback) + { + $model = $this->newAuditEvent($action, []); + + return $this->getConnection()->transaction(function () use ($callback, &$model) { + $response = $callback($model, $this); + $model->save(); + + return $response; + }); + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ diff --git a/database/migrations/2021_01_17_102401_create_audit_logs_table.php b/database/migrations/2021_01_17_102401_create_audit_logs_table.php index 7586c1b8..a13274c0 100644 --- a/database/migrations/2021_01_17_102401_create_audit_logs_table.php +++ b/database/migrations/2021_01_17_102401_create_audit_logs_table.php @@ -1,8 +1,8 @@ id(); - $table->timestamps(); + $table->char('uuid', 36); + $table->boolean('is_system')->default(false); + $table->bigInteger('user_id')->nullable(); + $table->bigInteger('server_id')->nullable(); + $table->string('action'); + $table->json('device'); + $table->json('metadata'); + $table->timestamp('created_at', 0); }); } From 291c65275ae9e83bee7db6d3c187a9ec345e853a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 11:52:44 -0800 Subject: [PATCH 03/23] Update audit design --- .../Api/Client/Servers/FileController.php | 19 +++++++------------ app/Models/AuditLog.php | 4 +++- ...1_01_17_102401_create_audit_logs_table.php | 8 ++++++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 782d948f..2a2d96e0 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -146,10 +146,8 @@ class FileController extends ClientApiController public function write(WriteFileContentRequest $request, Server $server): JsonResponse { $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { - $audit->metadata = [ - 'file' => $request->get('file'), - 'sub_action' => 'write_content', - ]; + $audit->subaction = 'write_content'; + $audit->metadata = ['file' => $request->get('file')]; $this->fileRepository ->setServer($server) @@ -171,10 +169,8 @@ class FileController extends ClientApiController public function create(CreateFolderRequest $request, Server $server): JsonResponse { $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { - $audit->metadata = [ - 'file' => $request->input('root', '/') . $request->input('name'), - 'sub_action' => 'create_folder', - ]; + $audit->subaction = 'create_folder'; + $audit->metadata = ['file' => $request->input('root', '/') . $request->input('name')]; $this->fileRepository ->setServer($server) @@ -218,10 +214,9 @@ class FileController extends ClientApiController public function copy(CopyFileRequest $request, Server $server): JsonResponse { $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { - $audit->metadata = [ - 'file' => $request->input('location'), - 'sub_action' => 'copy_file', - ]; + $audit->subaction = 'copy_file'; + $audit->metadata = ['file' => $request->input('location')]; + $this->fileRepository ->setServer($server) ->copyFile($request->input('location')); diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php index 7a056326..e5a9204e 100644 --- a/app/Models/AuditLog.php +++ b/app/Models/AuditLog.php @@ -13,6 +13,7 @@ use Illuminate\Container\Container; * @property int|null $user_id * @property int|null $server_id * @property string $action + * @property string|null $subaction * @property array $device * @property array $metadata * @property \Carbon\CarbonImmutable $created_at @@ -49,7 +50,8 @@ class AuditLog extends Model */ public static $validationRules = [ 'uuid' => 'required|uuid', - 'action' => 'required|string', + 'action' => 'required|string|max:191', + 'subaction' => 'nullable|string|max:191', 'device' => 'array', 'device.ip_address' => 'ip', 'device.user_agent' => 'string', diff --git a/database/migrations/2021_01_17_102401_create_audit_logs_table.php b/database/migrations/2021_01_17_102401_create_audit_logs_table.php index a13274c0..f67e7d64 100644 --- a/database/migrations/2021_01_17_102401_create_audit_logs_table.php +++ b/database/migrations/2021_01_17_102401_create_audit_logs_table.php @@ -17,12 +17,16 @@ class CreateAuditLogsTable extends Migration $table->id(); $table->char('uuid', 36); $table->boolean('is_system')->default(false); - $table->bigInteger('user_id')->nullable(); - $table->bigInteger('server_id')->nullable(); + $table->unsignedInteger('user_id')->nullable(); + $table->unsignedInteger('server_id')->nullable(); $table->string('action'); + $table->string('subaction')->nullable(); $table->json('device'); $table->json('metadata'); $table->timestamp('created_at', 0); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); }); } From bfc6f34c5048ebcf9d4867af4d09ac01a1048746 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 15:22:02 -0800 Subject: [PATCH 04/23] Audit when a backup is successful or fails --- .../Remote/Backups/BackupStatusController.php | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php index b11c07ad..8880c240 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Backups; use Carbon\CarbonImmutable; use Pterodactyl\Models\Backup; +use Pterodactyl\Models\AuditLog; use Illuminate\Http\JsonResponse; use League\Flysystem\AwsS3v3\AwsS3Adapter; use Pterodactyl\Http\Controllers\Controller; @@ -44,7 +45,7 @@ class BackupStatusController extends Controller * @param string $backup * @return \Illuminate\Http\JsonResponse * - * @throws \Exception + * @throws \Throwable */ public function __invoke(ReportBackupCompleteRequest $request, string $backup) { @@ -57,21 +58,28 @@ class BackupStatusController extends Controller ); } - $successful = $request->input('successful') ? true : false; + $action = $request->input('successful') + ? AuditLog::ACTION_SERVER_BACKUP_COMPELTED + : AuditLog::ACTION_SERVER_BACKUP_FAILED; - $model->fill([ - 'is_successful' => $successful, - 'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null, - 'bytes' => $successful ? $request->input('size') : 0, - 'completed_at' => CarbonImmutable::now(), - ])->save(); + $model->server->audit($action, function (AuditLog $audit) use ($model, $request) { + $audit->metadata = ['backup_uuid' => $model->uuid]; - // Check if we are using the s3 backup adapter. If so, make sure we mark the backup as - // being completed in S3 correctly. - $adapter = $this->backupManager->adapter(); - if ($adapter instanceof AwsS3Adapter) { - $this->completeMultipartUpload($model, $adapter, $successful); - } + $successful = $request->input('successful') ? true : false; + $model->fill([ + 'is_successful' => $successful, + 'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null, + 'bytes' => $successful ? $request->input('size') : 0, + 'completed_at' => CarbonImmutable::now(), + ])->save(); + + // Check if we are using the s3 backup adapter. If so, make sure we mark the backup as + // being completed in S3 correctly. + $adapter = $this->backupManager->adapter(); + if ($adapter instanceof AwsS3Adapter) { + $this->completeMultipartUpload($model, $adapter, $successful); + } + }); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } From 4c29be2e54f1bb11e0a73e979a80f8d3b3b5bc0a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 15:25:49 -0800 Subject: [PATCH 05/23] Adjust some naming real quick --- .../Api/Client/Servers/BackupController.php | 6 ++-- .../Api/Client/Servers/FileController.php | 18 +++++----- .../Remote/Backups/BackupStatusController.php | 4 +-- app/Models/AuditLog.php | 33 ++++++++----------- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index 5ba8475e..c18aac3a 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -80,7 +80,7 @@ class BackupController extends ClientApiController public function store(StoreBackupRequest $request, Server $server) { /** @var \Pterodactyl\Models\Backup $backup */ - $backup = $server->audit(AuditLog::ACTION_SERVER_BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) { + $backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) { $backup = $this->initiateBackupService ->setIgnoredFiles( explode(PHP_EOL, $request->input('ignored') ?? '') @@ -125,7 +125,9 @@ class BackupController extends ClientApiController */ public function delete(DeleteBackupRequest $request, Server $server, Backup $backup) { - $server->audit(AuditLog::ACTION_SERVER_BACKUP_DELETED, function () use ($backup) { + $server->audit(AuditLog::SERVER__BACKUP_DELETED, function (AuditLog $audit) use ($backup) { + $audit->metadata = ['backup_uuid' => $backup->uuid]; + $this->deleteBackupService->handle($backup); }); diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 2a2d96e0..df0fdded 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -110,7 +110,7 @@ class FileController extends ClientApiController */ public function download(GetFileContentsRequest $request, Server $server) { - $token = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DOWNLOAD, function (AuditLog $audit, Server $server) use ($request) { + $token = $server->audit(AuditLog::SERVER__FILESYSTEM_DOWNLOAD, function (AuditLog $audit, Server $server) use ($request) { $audit->metadata = ['file' => $request->get('file')]; return $this->jwtService @@ -145,7 +145,7 @@ class FileController extends ClientApiController */ public function write(WriteFileContentRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { $audit->subaction = 'write_content'; $audit->metadata = ['file' => $request->get('file')]; @@ -168,7 +168,7 @@ class FileController extends ClientApiController */ public function create(CreateFolderRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { $audit->subaction = 'create_folder'; $audit->metadata = ['file' => $request->input('root', '/') . $request->input('name')]; @@ -191,7 +191,7 @@ class FileController extends ClientApiController */ public function rename(RenameFileRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_RENAME, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_RENAME, function (AuditLog $audit, Server $server) use ($request) { $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')]; $this->fileRepository @@ -213,7 +213,7 @@ class FileController extends ClientApiController */ public function copy(CopyFileRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_WRITE, function (AuditLog $audit, Server $server) use ($request) { $audit->subaction = 'copy_file'; $audit->metadata = ['file' => $request->input('location')]; @@ -234,7 +234,7 @@ class FileController extends ClientApiController */ public function compress(CompressFilesRequest $request, Server $server): array { - $file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_COMPRESS, function (AuditLog $audit, Server $server) use ($request) { + $file = $server->audit(AuditLog::SERVER__FILESYSTEM_COMPRESS, function (AuditLog $audit, Server $server) use ($request) { // Allow up to five minutes for this request to process before timing out. set_time_limit(300); @@ -260,7 +260,7 @@ class FileController extends ClientApiController */ public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse { - $file = $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DECOMPRESS, function (AuditLog $audit, Server $server) use ($request) { + $file = $server->audit(AuditLog::SERVER__FILESYSTEM_DECOMPRESS, function (AuditLog $audit, Server $server) use ($request) { // Allow up to five minutes for this request to process before timing out. set_time_limit(300); @@ -284,7 +284,7 @@ class FileController extends ClientApiController */ public function delete(DeleteFileRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_DELETE, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_DELETE, function (AuditLog $audit, Server $server) use ($request) { $audit->metadata = ['root' => $request->input('root'), 'files' => $request->input('files')]; $this->fileRepository->setServer($server) @@ -326,7 +326,7 @@ class FileController extends ClientApiController */ public function pull(PullFileRequest $request, Server $server): JsonResponse { - $server->audit(AuditLog::ACTION_SERVER_FILESYSTEM_PULL, function (AuditLog $audit, Server $server) use ($request) { + $server->audit(AuditLog::SERVER__FILESYSTEM_PULL, function (AuditLog $audit, Server $server) use ($request) { $audit->metadata = ['directory' => $request->input('directory'), 'url' => $request->input('url')]; $this->fileRepository->setServer($server)->pull($request->input('url'), $request->input('directory')); diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php index 8880c240..fd53103c 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php @@ -59,8 +59,8 @@ class BackupStatusController extends Controller } $action = $request->input('successful') - ? AuditLog::ACTION_SERVER_BACKUP_COMPELTED - : AuditLog::ACTION_SERVER_BACKUP_FAILED; + ? AuditLog::SERVER__BACKUP_COMPELTED + : AuditLog::SERVER__BACKUP_FAILED; $model->server->audit($action, function (AuditLog $audit) use ($model, $request) { $audit->metadata = ['backup_uuid' => $model->uuid]; diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php index e5a9204e..8126f9f1 100644 --- a/app/Models/AuditLog.php +++ b/app/Models/AuditLog.php @@ -25,25 +25,20 @@ class AuditLog extends Model { const UPDATED_AT = null; - const ACTION_USER_AUTH_LOGIN = 'user:auth.login'; - const ACTION_USER_AUTH_FAILED = 'user:auth.failed'; - const ACTION_USER_AUTH_PASSWORD_CHANGED = 'user:auth.password-changed'; - - const ACTION_SERVER_FILESYSTEM_DOWNLOAD = 'server:filesystem.download'; - const ACTION_SERVER_FILESYSTEM_WRITE = 'server:filesystem.write'; - const ACTION_SERVER_FILESYSTEM_DELETE = 'server:filesystem.delete'; - const ACTION_SERVER_FILESYSTEM_RENAME = 'server:filesystem.rename'; - const ACTION_SERVER_FILESYSTEM_COMPRESS = 'server:filesystem.compress'; - const ACTION_SERVER_FILESYSTEM_DECOMPRESS = 'server:filesystem.decompress'; - const ACTION_SERVER_FILESYSTEM_PULL = 'server:filesystem.pull'; - - const ACTION_SERVER_BACKUP_STARTED = 'server:backup.started'; - const ACTION_SERVER_BACKUP_FAILED = 'server:backup.failed'; - const ACTION_SERVER_BACKUP_COMPELTED = 'server:backup.completed'; - const ACTION_SERVER_BACKUP_DELETED = 'server:backup.deleted'; - const ACTION_SERVER_BACKUP_RESTORE_STARTED = 'server:backup.restore.started'; - const ACTION_SERVER_BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed'; - const ACTION_SERVER_BACKUP_RESTORE_FAILED = 'server:backup.restore.failed'; + const SERVER__FILESYSTEM_DOWNLOAD = 'server:filesystem.download'; + const SERVER__FILESYSTEM_WRITE = 'server:filesystem.write'; + const SERVER__FILESYSTEM_DELETE = 'server:filesystem.delete'; + const SERVER__FILESYSTEM_RENAME = 'server:filesystem.rename'; + const SERVER__FILESYSTEM_COMPRESS = 'server:filesystem.compress'; + const SERVER__FILESYSTEM_DECOMPRESS = 'server:filesystem.decompress'; + const SERVER__FILESYSTEM_PULL = 'server:filesystem.pull'; + const SERVER__BACKUP_STARTED = 'server:backup.started'; + const SERVER__BACKUP_FAILED = 'server:backup.failed'; + const SERVER__BACKUP_COMPELTED = 'server:backup.completed'; + const SERVER__BACKUP_DELETED = 'server:backup.deleted'; + const SERVER__BACKUP_RESTORE_STARTED = 'server:backup.restore.started'; + const SERVER__BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed'; + const SERVER__BACKUP_RESTORE_FAILED = 'server:backup.restore.failed'; /** * @var string[] From a75a347d6547a87be9e83759592bf8aa24e2875b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 15:51:56 -0800 Subject: [PATCH 06/23] Remove suspended & installing fields, replace with single status field --- .../Admin/Servers/ServerViewController.php | 2 +- .../Controllers/Admin/ServersController.php | 4 +- .../Remote/SftpAuthenticationController.php | 6 +- .../Admin/Servers/ServerInstalled.php | 2 +- .../Server/AuthenticateServerAccess.php | 2 +- .../Server/AccessingValidServer.php | 4 +- app/Models/Server.php | 22 +++++--- .../Servers/ReinstallServerService.php | 2 +- .../ServerConfigurationStructureService.php | 4 +- .../Servers/ServerCreationService.php | 2 +- app/Services/Servers/SuspensionService.php | 7 +-- .../Api/Application/ServerTransformer.php | 4 +- .../Api/Client/ServerTransformer.php | 4 +- database/factories/ModelFactory.php | 3 +- ...52623_add_generic_server_status_column.php | 55 +++++++++++++++++++ resources/views/admin/servers/index.blade.php | 4 +- .../servers/partials/navigation.blade.php | 2 +- .../views/admin/servers/view/index.blade.php | 8 +-- .../views/admin/servers/view/manage.blade.php | 4 +- .../Client/Server/SettingsControllerTest.php | 6 +- .../Servers/ServerCreationServiceTest.php | 2 +- .../StartupModificationServiceTest.php | 2 +- .../Servers/SuspensionServiceTest.php | 15 ++--- .../Server/AccessingValidServerTest.php | 8 +-- 24 files changed, 115 insertions(+), 59 deletions(-) create mode 100644 database/migrations/2021_01_17_152623_add_generic_server_status_column.php diff --git a/app/Http/Controllers/Admin/Servers/ServerViewController.php b/app/Http/Controllers/Admin/Servers/ServerViewController.php index 5c2440b2..64c2b7f4 100644 --- a/app/Http/Controllers/Admin/Servers/ServerViewController.php +++ b/app/Http/Controllers/Admin/Servers/ServerViewController.php @@ -207,7 +207,7 @@ class ServerViewController extends Controller */ public function manage(Request $request, Server $server) { - if ($server->installed > 1) { + if ($server->status === Server::STATUS_INSTALL_FAILED) { throw new DisplayException( 'This server is in a failed install state and cannot be recovered. Please delete and re-create the server.' ); diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index bec5ac4a..29016f79 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -228,12 +228,12 @@ class ServersController extends Controller */ public function toggleInstall(Server $server) { - if ($server->installed > 1) { + if ($server->status === Server::STATUS_INSTALL_FAILED) { throw new DisplayException(trans('admin/server.exceptions.marked_as_failed')); } $this->repository->update($server->id, [ - 'installed' => ! $server->installed, + 'status' => $server->isInstalled() ? Server::STATUS_INSTALLING : null, ], true, true); $this->alert->success(trans('admin/server.alerts.install_toggled'))->flash(); diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php index cab532e8..efa6fe48 100644 --- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php +++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php @@ -118,10 +118,8 @@ class SftpAuthenticationController extends Controller // Remember, for security purposes, only reveal the existence of the server to people that // have provided valid credentials, and have permissions to know about it. - if ($server->installed !== 1 || $server->suspended) { - throw new BadRequestHttpException( - 'Server is not installed or is currently suspended.' - ); + if ($server->isSuspended() || !$server->isInstalled()) { + throw new BadRequestHttpException('Server is not installed or is currently suspended.'); } return new JsonResponse([ diff --git a/app/Http/Middleware/Admin/Servers/ServerInstalled.php b/app/Http/Middleware/Admin/Servers/ServerInstalled.php index 2f0a384f..69c7d548 100644 --- a/app/Http/Middleware/Admin/Servers/ServerInstalled.php +++ b/app/Http/Middleware/Admin/Servers/ServerInstalled.php @@ -29,7 +29,7 @@ class ServerInstalled ); } - if ($server->installed !== 1) { + if (! $server->isInstalled()) { throw new HttpException( Response::HTTP_FORBIDDEN, 'Access to this resource is not allowed due to the current installation state.' ); diff --git a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php index e9eaa143..902a5e4d 100644 --- a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php +++ b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php @@ -64,7 +64,7 @@ class AuthenticateServerAccess } } - if ($server->suspended && ! $request->routeIs('api:client:server.resources')) { + if ($server->isSuspended() && ! $request->routeIs('api:client:server.resources')) { throw new BadRequestHttpException( 'This server is currently suspended and the functionality requested is unavailable.' ); diff --git a/app/Http/Middleware/Server/AccessingValidServer.php b/app/Http/Middleware/Server/AccessingValidServer.php index 2491414c..77a9e0be 100644 --- a/app/Http/Middleware/Server/AccessingValidServer.php +++ b/app/Http/Middleware/Server/AccessingValidServer.php @@ -63,7 +63,7 @@ class AccessingValidServer $isApiRequest = $request->expectsJson() || $request->is(...$this->config->get('pterodactyl.json_routes', [])); $server = $this->repository->getByUuid($attributes instanceof Server ? $attributes->uuid : $attributes); - if ($server->suspended) { + if ($server->isSuspended()) { if ($isApiRequest) { throw new AccessDeniedHttpException('Server is suspended and cannot be accessed.'); } @@ -73,7 +73,7 @@ class AccessingValidServer // Servers can have install statuses other than 1 or 0, so don't check // for a bool-type operator here. - if ($server->installed !== 1) { + if (! $server->isInstalled()) { if ($isApiRequest) { throw new ConflictHttpException('Server is still completing the installation process.'); } diff --git a/app/Models/Server.php b/app/Models/Server.php index b65ef662..775fbf3c 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -15,8 +15,8 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property int $node_id * @property string $name * @property string $description + * @property string|null $status * @property bool $skip_scripts - * @property bool $suspended * @property int $owner_id * @property int $memory * @property int $swap @@ -30,7 +30,6 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property int $egg_id * @property string $startup * @property string $image - * @property int $installed * @property int $allocation_limit * @property int $database_limit * @property int $backup_limit @@ -64,9 +63,9 @@ class Server extends Model */ const RESOURCE_NAME = 'server'; - const STATUS_INSTALLING = 0; - const STATUS_INSTALLED = 1; - const STATUS_INSTALL_FAILED = 2; + const STATUS_INSTALLING = 'installing'; + const STATUS_INSTALL_FAILED = 'install_failed'; + const STATUS_SUSPENDED = 'suspended'; /** * The table associated with the model. @@ -82,6 +81,7 @@ class Server extends Model * @var array */ protected $attributes = [ + 'status' => self::STATUS_INSTALLING, 'oom_disabled' => true, ]; @@ -104,7 +104,7 @@ class Server extends Model * * @var array */ - protected $guarded = ['id', 'installed', self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; + protected $guarded = ['id', self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; /** * @var array @@ -115,6 +115,7 @@ class Server extends Model 'name' => 'required|string|min:1|max:191', 'node_id' => 'required|exists:nodes,id', 'description' => 'string', + 'status' => 'nullable|string', 'memory' => 'required|numeric|min:0', 'swap' => 'required|numeric|min:-1', 'io' => 'required|numeric|between:10,1000', @@ -142,7 +143,6 @@ class Server extends Model protected $casts = [ 'node_id' => 'integer', 'skip_scripts' => 'boolean', - 'suspended' => 'boolean', 'owner_id' => 'integer', 'memory' => 'integer', 'swap' => 'integer', @@ -153,7 +153,6 @@ class Server extends Model 'allocation_id' => 'integer', 'nest_id' => 'integer', 'egg_id' => 'integer', - 'installed' => 'integer', 'database_limit' => 'integer', 'allocation_limit' => 'integer', 'backup_limit' => 'integer', @@ -176,7 +175,12 @@ class Server extends Model */ public function isInstalled(): bool { - return $this->installed === 1; + return $this->status !== self::STATUS_INSTALLING && $this->status !== self::STATUS_INSTALL_FAILED; + } + + public function isSuspended(): bool + { + return $this->status === self::STATUS_SUSPENDED; } /** diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index 6f5b5608..b922464c 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -44,7 +44,7 @@ class ReinstallServerService public function handle(Server $server) { return $this->connection->transaction(function () use ($server) { - $server->forceFill(['installed' => Server::STATUS_INSTALLING])->save(); + $server->fill(['status' => Server::STATUS_INSTALLING])->save(); $this->daemonServerRepository->setServer($server)->reinstall(); diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index b942a270..1bffdb54 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -60,7 +60,7 @@ class ServerConfigurationStructureService { return [ 'uuid' => $server->uuid, - 'suspended' => $server->suspended, + 'suspended' => $server->isSuspended(), 'environment' => $this->environment->handle($server), 'invocation' => $server->startup, 'skip_egg_scripts' => $server->skip_scripts, @@ -137,7 +137,7 @@ class ServerConfigurationStructureService 'skip_scripts' => $server->skip_scripts, ], 'rebuild' => false, - 'suspended' => (int)$server->suspended, + 'suspended' => $server->isSuspended() ? 1 : 0, ]; } } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 76d371ab..7c11dab9 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -229,8 +229,8 @@ class ServerCreationService 'node_id' => Arr::get($data, 'node_id'), 'name' => Arr::get($data, 'name'), 'description' => Arr::get($data, 'description') ?? '', + 'status' => Server::STATUS_INSTALLING, 'skip_scripts' => Arr::get($data, 'skip_scripts') ?? isset($data['skip_scripts']), - 'suspended' => false, 'owner_id' => Arr::get($data, 'owner_id'), 'memory' => Arr::get($data, 'memory'), 'swap' => Arr::get($data, 'swap'), diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 87fd0a33..3f3035cb 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -6,7 +6,6 @@ use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Repositories\Wings\DaemonServerRepository; -use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Pterodactyl\Exceptions\Http\Server\ServerTransferringException; class SuspensionService @@ -54,7 +53,7 @@ class SuspensionService // Nothing needs to happen if we're suspending the server and it is already // suspended in the database. Additionally, nothing needs to happen if the server // is not suspended and we try to un-suspend the instance. - if ($isSuspending === $server->suspended) { + if ($isSuspending === $server->isSuspended()) { return; } @@ -63,9 +62,9 @@ class SuspensionService throw new ServerTransferringException; } - $this->connection->transaction(function () use ($action, $server) { + $this->connection->transaction(function () use ($action, $server, $isSuspending) { $server->update([ - 'suspended' => $action === self::ACTION_SUSPEND, + 'status' => $isSuspending ? Server::STATUS_SUSPENDED : null, ]); // Only send the suspension request to wings if the server is not currently being transferred. diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 10c343d8..70a34624 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -66,7 +66,7 @@ class ServerTransformer extends BaseTransformer 'identifier' => $server->uuidShort, 'name' => $server->name, 'description' => $server->description, - 'suspended' => (bool) $server->suspended, + 'suspended' => $server->isSuspended(), 'limits' => [ 'memory' => $server->memory, 'swap' => $server->swap, @@ -88,7 +88,7 @@ class ServerTransformer extends BaseTransformer 'container' => [ 'startup_command' => $server->startup, 'image' => $server->image, - 'installed' => (int) $server->installed === 1, + 'installed' => $server->isInstalled() ? 1 : 0, 'environment' => $this->environmentService->handle($server), ], $server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at), diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 0673f9b5..79102bb3 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -70,8 +70,8 @@ class ServerTransformer extends BaseClientTransformer 'allocations' => $server->allocation_limit, 'backups' => $server->backup_limit, ], - 'is_suspended' => $server->suspended, - 'is_installing' => $server->installed !== 1, + 'is_suspended' => $server->isSuspended(), + 'is_installing' => ! $server->isInstalled(), 'is_transferring' => ! is_null($server->transfer), ]; } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 4997a9b6..f48c8e6e 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -27,14 +27,13 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) { 'name' => $faker->firstName, 'description' => implode(' ', $faker->sentences()), 'skip_scripts' => 0, - 'suspended' => 0, + 'status' => null, 'memory' => 512, 'swap' => 0, 'disk' => 512, 'io' => 500, 'cpu' => 0, 'oom_disabled' => 0, - 'installed' => 1, 'database_limit' => null, 'allocation_limit' => null, 'created_at' => Carbon::now(), diff --git a/database/migrations/2021_01_17_152623_add_generic_server_status_column.php b/database/migrations/2021_01_17_152623_add_generic_server_status_column.php new file mode 100644 index 00000000..536b5642 --- /dev/null +++ b/database/migrations/2021_01_17_152623_add_generic_server_status_column.php @@ -0,0 +1,55 @@ +string('status')->nullable()->after('description'); + }); + + DB::transaction(function () { + DB::update('UPDATE servers SET `status` = \'suspended\' WHERE `suspended` = 1'); + DB::update('UPDATE servers SET `status` = \'installing\' WHERE `installed` = 0'); + DB::update('UPDATE servers SET `status` = \'install_failed\' WHERE `installed` = 2'); + }); + + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('suspended'); + $table->dropColumn('installed'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->unsignedTinyInteger('suspended')->default(0); + $table->unsignedTinyInteger('installed')->default(0); + }); + + DB::transaction(function () { + DB::update('UPDATE servers SET `suspended` = 1 WHERE `status` = \'suspended\''); + DB::update('UPDATE servers SET `installed` = 1 WHERE `status` IS NULL'); + DB::update('UPDATE servers SET `installed` = 2 WHERE `status` = \'install_failed\''); + }); + + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +} diff --git a/resources/views/admin/servers/index.blade.php b/resources/views/admin/servers/index.blade.php index ce039944..e152158c 100644 --- a/resources/views/admin/servers/index.blade.php +++ b/resources/views/admin/servers/index.blade.php @@ -57,9 +57,9 @@ {{ $server->allocation->alias }}:{{ $server->allocation->port }} - @if($server->suspended) + @if($server->isSuspended()) Suspended - @elseif(! $server->installed) + @elseif(! $server->isInstalled()) Installing @else Active diff --git a/resources/views/admin/servers/partials/navigation.blade.php b/resources/views/admin/servers/partials/navigation.blade.php index 0474787d..964eac8e 100644 --- a/resources/views/admin/servers/partials/navigation.blade.php +++ b/resources/views/admin/servers/partials/navigation.blade.php @@ -8,7 +8,7 @@