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); }); }