From b15679d3bb04ad45c40ff58ecb53c6dea41d929a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 10:49:36 -0800 Subject: [PATCH 01/24] 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 000000000..fdf166e68 --- /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 aace86d0b..b617db0a2 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 000000000..7586c1b8c --- /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/24] 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 23daf1bc6..5ba8475ed 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 317115b29..782d948f5 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 fdf166e68..7a0563260 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 b617db0a2..b65ef662e 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 7586c1b8c..a13274c06 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/24] 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 782d948f5..2a2d96e06 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 7a0563260..e5a9204e8 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 a13274c06..f67e7d647 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/24] 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 b11c07ad8..8880c2405 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/24] 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 5ba8475ed..c18aac3ac 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 2a2d96e06..df0fdded6 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 8880c2405..fd53103c8 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 e5a9204e8..8126f9f19 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/24] 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 5c2440b24..64c2b7f49 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 bec5ac4aa..29016f792 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 cab532e81..efa6fe48c 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 2f0a384f3..69c7d5488 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 e9eaa143e..902a5e4df 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 2491414c7..77a9e0beb 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 b65ef662e..775fbf3c3 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 6f5b56083..b922464ca 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 b942a270a..1bffdb547 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 76d371ab2..7c11dab9b 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 87fd0a334..3f3035cb0 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 10c343d8c..70a346247 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 0673f9b57..79102bb33 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 4997a9b6f..f48c8e6e7 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 000000000..536b5642b --- /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 ce039944f..e152158ce 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 0474787de..964eac8e3 100644 --- a/resources/views/admin/servers/partials/navigation.blade.php +++ b/resources/views/admin/servers/partials/navigation.blade.php @@ -8,7 +8,7 @@