From e313dff674c3ca952da90e8f2c9951eb2fcb9752 Mon Sep 17 00:00:00 2001 From: DaneEveritt Date: Sun, 22 May 2022 14:10:01 -0400 Subject: [PATCH] Massively simplify API binding logic Changes the API internals to use normal Laravel binding which automatically supports nested-models and can determine their relationships. This removes a lot of confusingly complex internal logic and replaces it with standard Laravel code. This also removes a deprecated "getModel" method and fully replaces it with a "parameter" method that does stricter type-checking. --- .../Locations/LocationController.php | 12 +-- .../Api/Application/Nests/EggController.php | 28 +---- .../Api/Application/Nests/NestController.php | 4 +- .../Servers/DatabaseController.php | 6 +- .../Servers/ExternalServerController.php | 7 +- .../Application/Servers/ServerController.php | 4 +- .../Servers/ServerDetailsController.php | 8 +- .../Application/Servers/StartupController.php | 4 +- .../Users/ExternalUserController.php | 7 +- .../Api/Client/Servers/ScheduleController.php | 3 +- .../Remote/SftpAuthenticationController.php | 2 +- app/Http/Kernel.php | 7 +- .../Middleware/Api/ApiSubstituteBindings.php | 87 --------------- .../Client/SubstituteClientApiBindings.php | 62 ----------- .../Api/Client/SubstituteClientBindings.php | 36 +++++++ app/Http/Middleware/Api/IsValidJson.php | 9 +- .../Allocations/DeleteAllocationRequest.php | 20 ---- .../Allocations/GetAllocationsRequest.php | 12 --- .../Api/Application/ApplicationApiRequest.php | 101 ++++-------------- .../Locations/DeleteLocationRequest.php | 11 -- .../Locations/GetLocationRequest.php | 11 -- .../Locations/UpdateLocationRequest.php | 10 -- .../Application/Nests/Eggs/GetEggRequest.php | 10 -- .../Application/Nodes/DeleteNodeRequest.php | 12 --- .../Api/Application/Nodes/GetNodeRequest.php | 11 -- .../Application/Nodes/UpdateNodeRequest.php | 4 +- .../Databases/GetServerDatabaseRequest.php | 11 -- .../Servers/GetExternalServerRequest.php | 34 ------ .../UpdateServerBuildConfigurationRequest.php | 2 +- .../Servers/UpdateServerDetailsRequest.php | 2 +- .../Servers/UpdateServerStartupRequest.php | 2 +- .../Application/Users/DeleteUserRequest.php | 11 -- .../Users/GetExternalUserRequest.php | 34 ------ .../Application/Users/UpdateUserRequest.php | 2 +- .../Databases/DeleteDatabaseRequest.php | 7 -- .../Servers/Files/DownloadFileRequest.php | 2 +- app/Models/Allocation.php | 8 ++ app/Models/Database.php | 33 ++++++ app/Models/Location.php | 8 ++ app/Models/Model.php | 14 +++ app/Models/Schedule.php | 8 ++ app/Models/Server.php | 84 +++++++++++---- app/Models/Task.php | 8 ++ app/Models/User.php | 2 +- app/Models/UserSSHKey.php | 2 + app/Providers/RouteServiceProvider.php | 12 ++- routes/api-application.php | 56 +++++----- .../Location/LocationControllerTest.php | 12 --- .../Application/Nests/EggControllerTest.php | 13 --- .../Application/Nests/NestControllerTest.php | 13 --- .../Users/ExternalUserControllerTest.php | 12 --- .../Application/Users/UserControllerTest.php | 12 --- .../Http/IntegrationJsonRequestAssertions.php | 2 +- 53 files changed, 290 insertions(+), 604 deletions(-) delete mode 100644 app/Http/Middleware/Api/ApiSubstituteBindings.php delete mode 100644 app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php create mode 100644 app/Http/Middleware/Api/Client/SubstituteClientBindings.php diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index b7e82396a..e8f166aba 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -75,9 +75,9 @@ class LocationController extends ApplicationApiController /** * Return a single location. */ - public function view(GetLocationRequest $request): array + public function view(GetLocationRequest $request, Location $location): array { - return $this->fractal->item($request->getModel(Location::class)) + return $this->fractal->item($location) ->transformWith($this->getTransformer(LocationTransformer::class)) ->toArray(); } @@ -108,9 +108,9 @@ class LocationController extends ApplicationApiController * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateLocationRequest $request): array + public function update(UpdateLocationRequest $request, Location $location): array { - $location = $this->updateService->handle($request->getModel(Location::class), $request->validated()); + $location = $this->updateService->handle($location, $request->validated()); return $this->fractal->item($location) ->transformWith($this->getTransformer(LocationTransformer::class)) @@ -122,9 +122,9 @@ class LocationController extends ApplicationApiController * * @throws \Pterodactyl\Exceptions\Service\Location\HasActiveNodesException */ - public function delete(DeleteLocationRequest $request): Response + public function delete(DeleteLocationRequest $request, Location $location): Response { - $this->deletionService->handle($request->getModel(Location::class)); + $this->deletionService->handle($location); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Nests/EggController.php b/app/Http/Controllers/Api/Application/Nests/EggController.php index 1eb596f99..83c3f77a0 100644 --- a/app/Http/Controllers/Api/Application/Nests/EggController.php +++ b/app/Http/Controllers/Api/Application/Nests/EggController.php @@ -4,7 +4,6 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nests; use Pterodactyl\Models\Egg; use Pterodactyl\Models\Nest; -use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Transformers\Api\Application\EggTransformer; use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggRequest; use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggsRequest; @@ -12,31 +11,12 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class EggController extends ApplicationApiController { - /** - * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface - */ - private $repository; - - /** - * EggController constructor. - */ - public function __construct(EggRepositoryInterface $repository) - { - parent::__construct(); - - $this->repository = $repository; - } - /** * Return all eggs that exist for a given nest. */ - public function index(GetEggsRequest $request): array + public function index(GetEggsRequest $request, Nest $nest): array { - $eggs = $this->repository->findWhere([ - ['nest_id', '=', $request->getModel(Nest::class)->id], - ]); - - return $this->fractal->collection($eggs) + return $this->fractal->collection($nest->eggs) ->transformWith($this->getTransformer(EggTransformer::class)) ->toArray(); } @@ -44,9 +24,9 @@ class EggController extends ApplicationApiController /** * Return a single egg that exists on the specified nest. */ - public function view(GetEggRequest $request): array + public function view(GetEggRequest $request, Nest $nest, Egg $egg): array { - return $this->fractal->item($request->getModel(Egg::class)) + return $this->fractal->item($egg) ->transformWith($this->getTransformer(EggTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php index b66872d23..232bf026c 100644 --- a/app/Http/Controllers/Api/Application/Nests/NestController.php +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -40,9 +40,9 @@ class NestController extends ApplicationApiController /** * Return information about a single Nest model. */ - public function view(GetNestsRequest $request): array + public function view(GetNestsRequest $request, Nest $nest): array { - return $this->fractal->item($request->getModel(Nest::class)) + return $this->fractal->item($nest) ->transformWith($this->getTransformer(NestTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index fc1756230..2b9564728 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -105,12 +105,10 @@ class DatabaseController extends ApplicationApiController /** * Handle a request to delete a specific server database from the Panel. - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function delete(ServerDatabaseWriteRequest $request): Response + public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response { - $this->databaseManagementService->delete($request->getModel(Database::class)); + $this->databaseManagementService->delete($database); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php b/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php index 0cecb977a..869472f72 100644 --- a/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ExternalServerController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Pterodactyl\Models\Server; use Pterodactyl\Transformers\Api\Application\ServerTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Servers\GetExternalServerRequest; @@ -11,9 +12,11 @@ class ExternalServerController extends ApplicationApiController /** * Retrieve a specific server from the database using its external ID. */ - public function index(GetExternalServerRequest $request): array + public function index(GetExternalServerRequest $request, string $external_id): array { - return $this->fractal->item($request->getServerModel()) + $server = Server::query()->where('external_id', $external_id)->firstOrFail(); + + return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index fb1871f69..a52b46d57 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -86,9 +86,9 @@ class ServerController extends ApplicationApiController /** * Show a single server transformed for the application API. */ - public function view(GetServerRequest $request): array + public function view(GetServerRequest $request, Server $server): array { - return $this->fractal->item($request->getModel(Server::class)) + return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php index 06bbf6bca..bcecf277c 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -42,14 +42,14 @@ class ServerDetailsController extends ApplicationApiController * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function details(UpdateServerDetailsRequest $request): array + public function details(UpdateServerDetailsRequest $request, Server $server): array { - $server = $this->detailsModificationService->returnUpdatedModel()->handle( - $request->getModel(Server::class), + $updated = $this->detailsModificationService->returnUpdatedModel()->handle( + $server, $request->validated() ); - return $this->fractal->item($server) + return $this->fractal->item($updated) ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php index e8300754f..6c854102b 100644 --- a/app/Http/Controllers/Api/Application/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -34,11 +34,11 @@ class StartupController extends ApplicationApiController * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function index(UpdateServerStartupRequest $request): array + public function index(UpdateServerStartupRequest $request, Server $server): array { $server = $this->modificationService ->setUserLevel(User::USER_LEVEL_ADMIN) - ->handle($request->getModel(Server::class), $request->validated()); + ->handle($server, $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Api/Application/Users/ExternalUserController.php b/app/Http/Controllers/Api/Application/Users/ExternalUserController.php index a253b6e19..2a8f4f07e 100644 --- a/app/Http/Controllers/Api/Application/Users/ExternalUserController.php +++ b/app/Http/Controllers/Api/Application/Users/ExternalUserController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Users; +use Pterodactyl\Models\User; use Pterodactyl\Transformers\Api\Application\UserTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Users\GetExternalUserRequest; @@ -11,9 +12,11 @@ class ExternalUserController extends ApplicationApiController /** * Retrieve a specific user from the database using their external ID. */ - public function index(GetExternalUserRequest $request): array + public function index(GetExternalUserRequest $request, string $external_id): array { - return $this->fractal->item($request->getUserModel()) + $user = User::query()->where('external_id', $external_id)->firstOrFail(); + + return $this->fractal->item($user) ->transformWith($this->getTransformer(UserTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index 0bbc6bd73..3e9b822bb 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -52,8 +52,7 @@ class ScheduleController extends ClientApiController */ public function index(ViewScheduleRequest $request, Server $server) { - $schedules = $server->schedule; - $schedules->loadMissing('tasks'); + $schedules = $server->schedules->loadMissing('tasks'); return $this->fractal->collection($schedules) ->transformWith($this->getTransformer(ScheduleTransformer::class)) diff --git a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php index 8a6874906..ceaa83ebe 100644 --- a/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php +++ b/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php @@ -2,8 +2,8 @@ namespace Pterodactyl\Http\Controllers\Api\Remote; -use Pterodactyl\Models\User; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; use Pterodactyl\Models\Permission; diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 2ff7a6260..3e9f1dd19 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -24,7 +24,6 @@ use Pterodactyl\Http\Middleware\MaintenanceMiddleware; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess; -use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Pterodactyl\Http\Middleware\Api\HandleStatelessRequest; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -32,7 +31,7 @@ use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; -use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings; +use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings; use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser; class Kernel extends HttpKernel @@ -75,13 +74,13 @@ class Kernel extends HttpKernel VerifyCsrfToken::class, ], 'application-api' => [ - ApiSubstituteBindings::class, + SubstituteBindings::class, 'api..key:' . ApiKey::TYPE_APPLICATION, AuthenticateApplicationUser::class, AuthenticateIPAccess::class, ], 'client-api' => [ - SubstituteClientApiBindings::class, + SubstituteClientBindings::class, 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, // This is perhaps a little backwards with the Client API, but logically you'd be unable diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php deleted file mode 100644 index 7ade7452a..000000000 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ /dev/null @@ -1,87 +0,0 @@ - Allocation::class, - 'database' => Database::class, - 'egg' => Egg::class, - 'location' => Location::class, - 'nest' => Nest::class, - 'node' => Node::class, - 'server' => Server::class, - 'user' => User::class, - ]; - - /** - * @var \Illuminate\Routing\Router - */ - protected $router; - - /** - * Perform substitution of route parameters without triggering - * a 404 error if a model is not found. - * - * @param \Illuminate\Http\Request $request - * - * @return mixed - */ - public function handle($request, Closure $next) - { - $route = $request->route(); - - foreach (self::$mappings as $key => $model) { - if (!is_null($this->router->getBindingCallback($key))) { - continue; - } - - $this->router->model($key, $model, function () use ($request) { - $request->attributes->set('is_missing_model', true); - }); - } - - $this->router->substituteBindings($route); - - // Attempt to resolve bindings for this route. If one of the models - // cannot be resolved do not immediately return a 404 error. Set a request - // attribute that can be checked in the base API request class to only - // trigger a 404 after validating that the API key making the request is valid - // and even has permission to access the requested resource. - try { - $this->router->substituteImplicitBindings($route); - } catch (ModelNotFoundException $exception) { - $request->attributes->set('is_missing_model', true); - } - - return $next($request); - } - - /** - * Return the registered mappings. - * - * @return array - */ - public static function getMappings() - { - return self::$mappings; - } -} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php deleted file mode 100644 index 31590f28a..000000000 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ /dev/null @@ -1,62 +0,0 @@ -router->bind('server', function ($value) use ($request) { - try { - $column = 'uuidShort'; - if (preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value)) { - $column = 'uuid'; - } - - return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([ - [$column, '=', $value], - ]); - } catch (RecordNotFoundException $ex) { - $request->attributes->set('is_missing_model', true); - - return null; - } - }); - - $this->router->bind('database', function ($value) { - $id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value); - - return Database::query()->where('id', $id)->firstOrFail(); - }); - - $this->router->bind('backup', function ($value) { - return Backup::query()->where('uuid', $value)->firstOrFail(); - }); - - $this->router->bind('user', function ($value) { - return User::query()->where('uuid', $value)->firstOrFail(); - }); - - return parent::handle($request, $next); - } -} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientBindings.php new file mode 100644 index 000000000..38d87bf2f --- /dev/null +++ b/app/Http/Middleware/Api/Client/SubstituteClientBindings.php @@ -0,0 +1,36 @@ +router->bind('server', function ($value) { + return Server::query()->where(strlen($value) === 8 ? 'uuidShort' : 'uuid', $value)->firstOrFail(); + }); + + $this->router->bind('user', function ($value, $route) { + /** @var \Pterodactyl\Models\Subuser $match */ + $match = $route->parameter('server') + ->subusers() + ->whereRelation('user', 'uuid', '=', $value) + ->firstOrFail(); + + return $match->user; + }); + + return parent::handle($request, $next); + } +} diff --git a/app/Http/Middleware/Api/IsValidJson.php b/app/Http/Middleware/Api/IsValidJson.php index c3c8d6c85..5f53b097d 100644 --- a/app/Http/Middleware/Api/IsValidJson.php +++ b/app/Http/Middleware/Api/IsValidJson.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; +use JsonException; use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -18,10 +19,10 @@ class IsValidJson public function handle(Request $request, Closure $next) { if ($request->isJson() && !empty($request->getContent())) { - json_decode($request->getContent(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new BadRequestHttpException(sprintf('The JSON data passed in the request appears to be malformed. err_code: %d err_message: "%s"', json_last_error(), json_last_error_msg())); + try { + json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new BadRequestHttpException('The JSON data passed in the request appears to be malformed: ' . $exception->getMessage()); } } diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php index 9fab4e2c3..900534858 100644 --- a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php @@ -2,8 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Allocation; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -18,22 +16,4 @@ class DeleteAllocationRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested allocation exists and belongs to the node that - * is being passed in the URL. - */ - public function resourceExists(): bool - { - $node = $this->route()->parameter('node'); - $allocation = $this->route()->parameter('allocation'); - - if ($node instanceof Node && $node->exists) { - if ($allocation instanceof Allocation && $allocation->exists && $allocation->node_id === $node->id) { - return true; - } - } - - return false; - } } diff --git a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php index 524e201e1..50afcf960 100644 --- a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; -use Pterodactyl\Models\Node; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,15 +16,4 @@ class GetAllocationsRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::READ; - - /** - * Determine if the node that we are requesting the allocations - * for exists on the Panel. - */ - public function resourceExists(): bool - { - $node = $this->route()->parameter('node'); - - return $node instanceof Node && $node->exists; - } } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index 064a36853..5dc903f53 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -2,14 +2,12 @@ namespace Pterodactyl\Http\Requests\Api\Application; -use Pterodactyl\Models\ApiKey; +use Webmozart\Assert\Assert; use Illuminate\Validation\Validator; +use Illuminate\Database\Eloquent\Model; use Pterodactyl\Services\Acl\Api\AdminAcl; use Illuminate\Foundation\Http\FormRequest; use Pterodactyl\Exceptions\PterodactylException; -use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\Routing\Exception\InvalidParameterException; abstract class ApplicationApiRequest extends FormRequest { @@ -49,15 +47,7 @@ abstract class ApplicationApiRequest extends FormRequest throw new PterodactylException('An ACL resource must be defined on API requests.'); } - return AdminAcl::check($this->key(), $this->resource, $this->permission); - } - - /** - * Determine if the requested resource exists on the server. - */ - public function resourceExists(): bool - { - return true; + return AdminAcl::check($this->attributes->get('api_key'), $this->resource, $this->permission); } /** @@ -68,35 +58,6 @@ abstract class ApplicationApiRequest extends FormRequest return []; } - /** - * Return the API key being used for the request. - */ - public function key(): ApiKey - { - return $this->attributes->get('api_key'); - } - - /** - * Grab a model from the route parameters. If no model is found in the - * binding mappings an exception will be thrown. - * - * @return mixed - * - * @deprecated - * - * @throws \Symfony\Component\Routing\Exception\InvalidParameterException - */ - public function getModel(string $model) - { - $parameterKey = array_get(array_flip(ApiSubstituteBindings::getMappings()), $model); - - if (is_null($parameterKey)) { - throw new InvalidParameterException(); - } - - return $this->route()->parameter($parameterKey); - } - /** * Helper method allowing a developer to easily hook into this logic without having * to remember what the method name is called or where to use it. By default this is @@ -108,50 +69,26 @@ abstract class ApplicationApiRequest extends FormRequest } /** - * Validate that the resource exists and can be accessed prior to booting - * the validator and attempting to use the data. + * Returns the named route parameter and asserts that it is a real model that + * exists in the database. * - * @throws \Illuminate\Auth\Access\AuthorizationException + * @template T of \Illuminate\Database\Eloquent\Model + * + * @param class-string $expect + * + * @return T + * @noinspection PhpUndefinedClassInspection + * @noinspection PhpDocSignatureInspection */ - protected function prepareForValidation() + public function parameter(string $key, string $expect) { - if (!$this->passesAuthorization()) { - $this->failedAuthorization(); - } + $value = $this->route()->parameter($key); - $this->hasValidated = true; - } + Assert::isInstanceOf($value, $expect); + Assert::isInstanceOf($value, Model::class); + Assert::true($value->exists); - /* - * Determine if the request passes the authorization check as well - * as the exists check. - * - * @return bool - * - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - protected function passesAuthorization() - { - // If we have already validated we do not need to call this function - // again. This is needed to work around Laravel's normal auth validation - // that occurs after validating the request params since we are doing auth - // validation in the prepareForValidation() function. - if ($this->hasValidated) { - return true; - } - - if (!parent::passesAuthorization()) { - return false; - } - - // Only let the user know that a resource does not exist if they are - // authenticated to access the endpoint. This avoids exposing that - // an item exists (or does not exist) to the user until they can prove - // that they have permission to know about it. - if ($this->attributes->get('is_missing_model', false) || !$this->resourceExists()) { - throw new NotFoundHttpException(trans('exceptions.api.resource_not_found')); - } - - return true; + /* @var T $value */ + return $value; } } diff --git a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php index ac58314f9..880a58d1a 100644 --- a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; -use Pterodactyl\Models\Location; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,14 +16,4 @@ class DeleteLocationRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested location exists on the Panel. - */ - public function resourceExists(): bool - { - $location = $this->route()->parameter('location'); - - return $location instanceof Location && $location->exists; - } } diff --git a/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php b/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php index f7c10e0c0..dea82db33 100644 --- a/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php @@ -2,17 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; -use Pterodactyl\Models\Location; - class GetLocationRequest extends GetLocationsRequest { - /** - * Determine if the requested location exists on the Panel. - */ - public function resourceExists(): bool - { - $location = $this->route()->parameter('location'); - - return $location instanceof Location && $location->exists; - } } diff --git a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php index c775e2b1b..ce42e6f05 100644 --- a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php @@ -6,16 +6,6 @@ use Pterodactyl\Models\Location; class UpdateLocationRequest extends StoreLocationRequest { - /** - * Determine if the requested location exists on the Panel. - */ - public function resourceExists(): bool - { - $location = $this->route()->parameter('location'); - - return $location instanceof Location && $location->exists; - } - /** * Rules to validate this request against. */ diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php index 80304ab27..e2ae0fc80 100644 --- a/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php +++ b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php @@ -2,8 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nests\Eggs; -use Pterodactyl\Models\Egg; -use Pterodactyl\Models\Nest; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -18,12 +16,4 @@ class GetEggRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::READ; - - /** - * Determine if the requested egg exists for the selected nest. - */ - public function resourceExists(): bool - { - return $this->getModel(Nest::class)->id === $this->getModel(Egg::class)->nest_id; - } } diff --git a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php index eb1d1d9a5..5b9bebf27 100644 --- a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; -use Pterodactyl\Models\Node; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,15 +16,4 @@ class DeleteNodeRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the node being requested for editing exists - * on the Panel before validating the data. - */ - public function resourceExists(): bool - { - $node = $this->route()->parameter('node'); - - return $node instanceof Node && $node->exists; - } } diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php index bbb157035..6d231bc97 100644 --- a/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php @@ -2,17 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; -use Pterodactyl\Models\Node; - class GetNodeRequest extends GetNodesRequest { - /** - * Determine if the requested node exists on the Panel. - */ - public function resourceExists(): bool - { - $node = $this->route()->parameter('node'); - - return $node instanceof Node && $node->exists; - } } diff --git a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php index 2da2d4061..7133bd0b5 100644 --- a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php @@ -12,8 +12,8 @@ class UpdateNodeRequest extends StoreNodeRequest */ public function rules(array $rules = null): array { - $nodeId = $this->getModel(Node::class)->id; + $node = $this->route()->parameter('node')->id; - return parent::rules(Node::getRulesForUpdate($nodeId)); + return parent::rules(Node::getRulesForUpdate($node)); } } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php index 2dff1374e..776162440 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php @@ -16,15 +16,4 @@ class GetServerDatabaseRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::READ; - - /** - * Determine if the requested server database exists. - */ - public function resourceExists(): bool - { - $server = $this->route()->parameter('server'); - $database = $this->route()->parameter('database'); - - return $database->server_id === $server->id; - } } diff --git a/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php b/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php index 902bc60c5..39ec449d2 100644 --- a/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/GetExternalServerRequest.php @@ -2,19 +2,11 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetExternalServerRequest extends ApplicationApiRequest { - /** - * @var \Pterodactyl\Models\Server - */ - private $serverModel; - /** * @var string */ @@ -24,30 +16,4 @@ class GetExternalServerRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::READ; - - /** - * Determine if the requested external user exists. - */ - public function resourceExists(): bool - { - $repository = $this->container->make(ServerRepositoryInterface::class); - - try { - $this->serverModel = $repository->findFirstWhere([ - ['external_id', '=', $this->route()->parameter('external_id')], - ]); - } catch (RecordNotFoundException $exception) { - return false; - } - - return true; - } - - /** - * Return the server model for the requested external server. - */ - public function getServerModel(): Server - { - return $this->serverModel; - } } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 3f534b08a..f043f82b4 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -12,7 +12,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getRulesForUpdate($this->getModel(Server::class)); + $rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); return [ 'allocation' => $rules['allocation_id'], diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php index 06bd22f5f..3540b88cf 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -11,7 +11,7 @@ class UpdateServerDetailsRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getRulesForUpdate($this->getModel(Server::class)); + $rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); return [ 'external_id' => $rules['external_id'], diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php index a873e0198..c9b3c6ad0 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php @@ -23,7 +23,7 @@ class UpdateServerStartupRequest extends ApplicationApiRequest */ public function rules(): array { - $data = Server::getRulesForUpdate($this->getModel(Server::class)); + $data = Server::getRulesForUpdate($this->parameter('server', Server::class)); return [ 'startup' => $data['startup'], diff --git a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php index 56c028a6e..c7592e693 100644 --- a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; -use Pterodactyl\Models\User; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,14 +16,4 @@ class DeleteUserRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested user exists on the Panel. - */ - public function resourceExists(): bool - { - $user = $this->route()->parameter('user'); - - return $user instanceof User && $user->exists; - } } diff --git a/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php b/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php index 5f63d04ba..a3bd7c9cf 100644 --- a/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/GetExternalUserRequest.php @@ -2,19 +2,11 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; -use Pterodactyl\Models\User; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class GetExternalUserRequest extends ApplicationApiRequest { - /** - * @var User - */ - private $userModel; - /** * @var string */ @@ -24,30 +16,4 @@ class GetExternalUserRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::READ; - - /** - * Determine if the requested external user exists. - */ - public function resourceExists(): bool - { - $repository = $this->container->make(UserRepositoryInterface::class); - - try { - $this->userModel = $repository->findFirstWhere([ - ['external_id', '=', $this->route()->parameter('external_id')], - ]); - } catch (RecordNotFoundException $exception) { - return false; - } - - return true; - } - - /** - * Return the user model for the requested external user. - */ - public function getUserModel(): User - { - return $this->userModel; - } } diff --git a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php index b341eaa24..fa2e1291c 100644 --- a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php @@ -11,7 +11,7 @@ class UpdateUserRequest extends StoreUserRequest */ public function rules(array $rules = null): array { - $userId = $this->getModel(User::class)->id; + $userId = $this->parameter('user', User::class)->id; return parent::rules(User::getRulesForUpdate($userId)); } diff --git a/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php index 92b14157a..eb2cbc57e 100644 --- a/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php @@ -2,8 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Database; use Pterodactyl\Models\Permission; use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; @@ -14,9 +12,4 @@ class DeleteDatabaseRequest extends ClientApiRequest implements ClientPermission { return Permission::ACTION_DATABASE_DELETE; } - - public function resourceExists(): bool - { - return $this->getModel(Server::class)->id === $this->getModel(Database::class)->server_id; - } } diff --git a/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php index 0386b8555..c588c9b23 100644 --- a/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php @@ -13,6 +13,6 @@ class DownloadFileRequest extends ClientApiRequest */ public function authorize(): bool { - return $this->user()->can('file.read', $this->getModel(Server::class)); + return $this->user()->can('file.read', $this->parameter('server', Server::class)); } } diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 49673921b..47b560e48 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -80,6 +80,14 @@ class Allocation extends Model 'notes' => 'nullable|string|max:256', ]; + /** + * {@inheritDoc} + */ + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + /** * Return a hashid encoded string to represent the ID of the allocation. * diff --git a/app/Models/Database.php b/app/Models/Database.php index ebe886793..e54d9f5ce 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -2,6 +2,9 @@ namespace Pterodactyl\Models; +use Illuminate\Container\Container; +use Pterodactyl\Contracts\Extensions\HashidsInterface; + /** * @property int $id * @property int $server_id @@ -71,6 +74,36 @@ class Database extends Model 'password' => 'string', ]; + /** + * {@inheritDoc} + */ + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + + /** + * Resolves the database using the ID by checking if the value provided is a HashID + * string value, or just the ID to the database itself. + * + * @param mixed $value + * @param string|null $field + * + * @return \Illuminate\Database\Eloquent\Model|null + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function resolveRouteBinding($value, $field = null) + { + if (is_scalar($value) && ($field ?? $this->getRouteKeyName()) === 'id') { + $value = ctype_digit((string) $value) + ? $value + : Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value); + } + + return $this->where($field ?? $this->getRouteKeyName(), $value)->firstOrFail(); + } + /** * Gets the host database server associated with a database. * diff --git a/app/Models/Location.php b/app/Models/Location.php index bc9a1fb4f..c490e56ca 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -43,6 +43,14 @@ class Location extends Model 'long' => 'string|nullable|between:1,191', ]; + /** + * {@inheritDoc} + */ + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + /** * Gets the nodes in a specified location. * diff --git a/app/Models/Model.php b/app/Models/Model.php index 3fca705fd..23cf5e7e9 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -68,6 +68,20 @@ abstract class Model extends IlluminateModel }); } + /** + * Returns the model key to use for route model binding. By default we'll + * assume every model uses a UUID field for this. If the model does not have + * a UUID and is using a different key it should be specified on the model + * itself. + * + * You may also optionally override this on a per-route basis by declaring + * the key name in the URL definition, like "{user:id}". + */ + public function getRouteKeyName(): string + { + return 'uuid'; + } + /** * Set the model to skip validation when saving. * diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index 82a43c732..81c30768c 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -123,6 +123,14 @@ class Schedule extends Model 'next_run_at' => 'nullable|date', ]; + /** + * {@inheritDoc} + */ + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + /** * Returns the schedule's execution crontab entry as a string. * diff --git a/app/Models/Server.php b/app/Models/Server.php index bd539282a..b71315832 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -9,6 +9,8 @@ use Znck\Eloquent\Traits\BelongsToThrough; use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException; /** + * Pterodactyl\Models\Server. + * * @property int $id * @property string|null $external_id * @property string $uuid @@ -24,33 +26,75 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException; * @property int $disk * @property int $io * @property int $cpu - * @property string $threads + * @property string|null $threads * @property bool $oom_disabled * @property int $allocation_id * @property int $nest_id * @property int $egg_id * @property string $startup * @property string $image - * @property int $allocation_limit - * @property int $database_limit + * @property int|null $allocation_limit + * @property int|null $database_limit * @property int $backup_limit - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property \Pterodactyl\Models\User $user - * @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subusers - * @property \Pterodactyl\Models\Allocation $allocation - * @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations - * @property \Pterodactyl\Models\Node $node + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property \Pterodactyl\Models\Allocation|null $allocation + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Allocation[] $allocations + * @property int|null $allocations_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\AuditLog[] $audits + * @property int|null $audits_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Backup[] $backups + * @property int|null $backups_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Database[] $databases + * @property int|null $databases_count + * @property \Pterodactyl\Models\Egg|null $egg + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Mount[] $mounts + * @property int|null $mounts_count * @property \Pterodactyl\Models\Nest $nest - * @property \Pterodactyl\Models\Egg $egg - * @property \Pterodactyl\Models\EggVariable[]|\Illuminate\Database\Eloquent\Collection $variables - * @property \Pterodactyl\Models\Schedule[]|\Illuminate\Database\Eloquent\Collection $schedule - * @property \Pterodactyl\Models\Database[]|\Illuminate\Database\Eloquent\Collection $databases - * @property \Pterodactyl\Models\Location $location - * @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 + * @property \Pterodactyl\Models\Node $node + * @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications + * @property int|null $notifications_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Schedule[] $schedules + * @property int|null $schedules_count + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Subuser[] $subusers + * @property int|null $subusers_count + * @property \Pterodactyl\Models\ServerTransfer|null $transfer + * @property \Pterodactyl\Models\User $user + * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\EggVariable[] $variables + * @property int|null $variables_count + * + * @method static \Database\Factories\ServerFactory factory(...$parameters) + * @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Server newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Server query() + * @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationLimit($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereBackupLimit($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereCpu($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereDatabaseLimit($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereDescription($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereDisk($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereEggId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereExternalId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereImage($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereIo($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereNestId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereStatus($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereSwap($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereThreads($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereUuid($value) + * @method static \Illuminate\Database\Eloquent\Builder|Server whereUuidShort($value) + * @mixin \Eloquent */ class Server extends Model { @@ -273,7 +317,7 @@ class Server extends Model * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function schedule() + public function schedules() { return $this->hasMany(Schedule::class); } diff --git a/app/Models/Task.php b/app/Models/Task.php index 82ba72370..b02503286 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -105,6 +105,14 @@ class Task extends Model 'continue_on_failure' => 'boolean', ]; + /** + * {@inheritDoc} + */ + public function getRouteKeyName(): string + { + return $this->getKeyName(); + } + /** * Return a hashid encoded string to represent the ID of the task. * diff --git a/app/Models/User.php b/app/Models/User.php index 42ddc774e..196ad18ed 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -201,7 +201,7 @@ class User extends Model implements */ public function toVueObject(): array { - return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray(); + return Collection::make($this->toArray())->except(['id', 'external_id'])->toArray(); } /** diff --git a/app/Models/UserSSHKey.php b/app/Models/UserSSHKey.php index 718c6d1cf..658bac6c0 100644 --- a/app/Models/UserSSHKey.php +++ b/app/Models/UserSSHKey.php @@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at * @property \Pterodactyl\Models\User $user + * * @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|UserSSHKey newQuery() * @method static \Illuminate\Database\Query\Builder|UserSSHKey onlyTrashed() @@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @method static \Illuminate\Database\Query\Builder|UserSSHKey withTrashed() * @method static \Illuminate\Database\Query\Builder|UserSSHKey withoutTrashed() * @mixin \Eloquent + * * @method static \Database\Factories\UserSSHKeyFactory factory(...$parameters) */ class UserSSHKey extends Model diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 188509fce..428b9512a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Providers; use Illuminate\Http\Request; +use Pterodactyl\Models\Database; use Illuminate\Support\Facades\Route; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; @@ -26,6 +27,11 @@ class RouteServiceProvider extends ServiceProvider return preg_match(self::FILE_PATH_REGEX, $request->getPathInfo()) === 1; }); + // This is needed to make use of the "resolveRouteBinding" functionality in the + // model. Without it you'll never trigger that logic flow thus resulting in a 404 + // error because we request databases with a HashID, and not with a normal ID. + Route::model('database', Database::class); + $this->routes(function () { Route::middleware(['web', 'csrf'])->group(function () { Route::middleware('auth')->group(base_path('routes/base.php')); @@ -36,14 +42,18 @@ class RouteServiceProvider extends ServiceProvider Route::middleware('api')->group(function () { Route::middleware(['application-api', 'throttle:api.application']) ->prefix('/api/application') + ->scopeBindings() ->group(base_path('routes/api-application.php')); Route::middleware(['client-api', 'throttle:api.client']) ->prefix('/api/client') + ->scopeBindings() ->group(base_path('routes/api-client.php')); }); - Route::middleware('daemon')->prefix('/api/remote') + Route::middleware('daemon') + ->prefix('/api/remote') + ->scopeBindings() ->group(base_path('routes/api-remote.php')); }); } diff --git a/routes/api-application.php b/routes/api-application.php index 29d3d8aeb..dc6b0e5bb 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -14,13 +14,13 @@ use Pterodactyl\Http\Controllers\Api\Application; Route::group(['prefix' => '/users'], function () { Route::get('/', [Application\Users\UserController::class, 'index'])->name('api.application.users'); - Route::get('/{user}', [Application\Users\UserController::class, 'view'])->name('api.application.users.view'); + Route::get('/{user:id}', [Application\Users\UserController::class, 'view'])->name('api.application.users.view'); Route::get('/external/{external_id}', [Application\Users\ExternalUserController::class, 'index'])->name('api.application.users.external'); Route::post('/', [Application\Users\UserController::class, 'store']); - Route::patch('/{user}', [Application\Users\UserController::class, 'update']); + Route::patch('/{user:id}', [Application\Users\UserController::class, 'update']); - Route::delete('/{user}', [Application\Users\UserController::class, 'delete']); + Route::delete('/{user:id}', [Application\Users\UserController::class, 'delete']); }); /* @@ -34,18 +34,18 @@ Route::group(['prefix' => '/users'], function () { Route::group(['prefix' => '/nodes'], function () { Route::get('/', [Application\Nodes\NodeController::class, 'index'])->name('api.application.nodes'); Route::get('/deployable', Application\Nodes\NodeDeploymentController::class); - Route::get('/{node}', [Application\Nodes\NodeController::class, 'view'])->name('api.application.nodes.view'); - Route::get('/{node}/configuration', Application\Nodes\NodeConfigurationController::class); + Route::get('/{node:id}', [Application\Nodes\NodeController::class, 'view'])->name('api.application.nodes.view'); + Route::get('/{node:id}/configuration', Application\Nodes\NodeConfigurationController::class); Route::post('/', [Application\Nodes\NodeController::class, 'store']); - Route::patch('/{node}', [Application\Nodes\NodeController::class, 'update']); + Route::patch('/{node:id}', [Application\Nodes\NodeController::class, 'update']); - Route::delete('/{node}', [Application\Nodes\NodeController::class, 'delete']); + Route::delete('/{node:id}', [Application\Nodes\NodeController::class, 'delete']); - Route::group(['prefix' => '/{node}/allocations'], function () { + Route::group(['prefix' => '/{node:id}/allocations'], function () { Route::get('/', [Application\Nodes\AllocationController::class, 'index'])->name('api.application.allocations'); Route::post('/', [Application\Nodes\AllocationController::class, 'store']); - Route::delete('/{allocation}', [Application\Nodes\AllocationController::class, 'delete'])->name('api.application.allocations.view'); + Route::delete('/{allocation:id}', [Application\Nodes\AllocationController::class, 'delete'])->name('api.application.allocations.view'); }); }); @@ -59,12 +59,12 @@ Route::group(['prefix' => '/nodes'], function () { */ Route::group(['prefix' => '/locations'], function () { Route::get('/', [Application\Locations\LocationController::class, 'index'])->name('api.applications.locations'); - Route::get('/{location}', [Application\Locations\LocationController::class, 'view'])->name('api.application.locations.view'); + Route::get('/{location:id}', [Application\Locations\LocationController::class, 'view'])->name('api.application.locations.view'); Route::post('/', [Application\Locations\LocationController::class, 'store']); - Route::patch('/{location}', [Application\Locations\LocationController::class, 'update']); + Route::patch('/{location:id}', [Application\Locations\LocationController::class, 'update']); - Route::delete('/{location}', [Application\Locations\LocationController::class, 'delete']); + Route::delete('/{location:id}', [Application\Locations\LocationController::class, 'delete']); }); /* @@ -77,30 +77,30 @@ Route::group(['prefix' => '/locations'], function () { */ Route::group(['prefix' => '/servers'], function () { Route::get('/', [Application\Servers\ServerController::class, 'index'])->name('api.application.servers'); - Route::get('/{server}', [Application\Servers\ServerController::class, 'view'])->name('api.application.servers.view'); + Route::get('/{server:id}', [Application\Servers\ServerController::class, 'view'])->name('api.application.servers.view'); Route::get('/external/{external_id}', [Application\Servers\ExternalServerController::class, 'index'])->name('api.application.servers.external'); - Route::patch('/{server}/details', [Application\Servers\ServerDetailsController::class, 'details'])->name('api.application.servers.details'); - Route::patch('/{server}/build', [Application\Servers\ServerDetailsController::class, 'build'])->name('api.application.servers.build'); - Route::patch('/{server}/startup', [Application\Servers\StartupController::class, 'index'])->name('api.application.servers.startup'); + Route::patch('/{server:id}/details', [Application\Servers\ServerDetailsController::class, 'details'])->name('api.application.servers.details'); + Route::patch('/{server:id}/build', [Application\Servers\ServerDetailsController::class, 'build'])->name('api.application.servers.build'); + Route::patch('/{server:id}/startup', [Application\Servers\StartupController::class, 'index'])->name('api.application.servers.startup'); Route::post('/', [Application\Servers\ServerController::class, 'store']); - Route::post('/{server}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend'); - Route::post('/{server}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend'); - Route::post('/{server}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall'); + Route::post('/{server:id}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend'); + Route::post('/{server:id}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend'); + Route::post('/{server:id}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall'); - Route::delete('/{server}', [Application\Servers\ServerController::class, 'delete']); - Route::delete('/{server}/{force?}', [Application\Servers\ServerController::class, 'delete']); + Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']); + Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']); // Database Management Endpoint - Route::group(['prefix' => '/{server}/databases'], function () { + Route::group(['prefix' => '/{server:id}/databases'], function () { Route::get('/', [Application\Servers\DatabaseController::class, 'index'])->name('api.application.servers.databases'); - Route::get('/{database}', [Application\Servers\DatabaseController::class, 'view'])->name('api.application.servers.databases.view'); + Route::get('/{database:id}', [Application\Servers\DatabaseController::class, 'view'])->name('api.application.servers.databases.view'); Route::post('/', [Application\Servers\DatabaseController::class, 'store']); - Route::post('/{database}/reset-password', [Application\Servers\DatabaseController::class, 'resetPassword']); + Route::post('/{database:id}/reset-password', [Application\Servers\DatabaseController::class, 'resetPassword']); - Route::delete('/{database}', [Application\Servers\DatabaseController::class, 'delete']); + Route::delete('/{database:id}', [Application\Servers\DatabaseController::class, 'delete']); }); }); @@ -114,11 +114,11 @@ Route::group(['prefix' => '/servers'], function () { */ Route::group(['prefix' => '/nests'], function () { Route::get('/', [Application\Nests\NestController::class, 'index'])->name('api.application.nests'); - Route::get('/{nest}', [Application\Nests\NestController::class, 'view'])->name('api.application.nests.view'); + Route::get('/{nest:id}', [Application\Nests\NestController::class, 'view'])->name('api.application.nests.view'); // Egg Management Endpoint - Route::group(['prefix' => '/{nest}/eggs'], function () { + Route::group(['prefix' => '/{nest:id}/eggs'], function () { Route::get('/', [Application\Nests\EggController::class, 'index'])->name('api.application.nests.eggs'); - Route::get('/{egg}', [Application\Nests\EggController::class, 'view'])->name('api.application.nests.eggs.view'); + Route::get('/{egg:id}', [Application\Nests\EggController::class, 'view'])->name('api.application.nests.eggs.view'); }); }); diff --git a/tests/Integration/Api/Application/Location/LocationControllerTest.php b/tests/Integration/Api/Application/Location/LocationControllerTest.php index 106df9f92..ef3ff3e63 100644 --- a/tests/Integration/Api/Application/Location/LocationControllerTest.php +++ b/tests/Integration/Api/Application/Location/LocationControllerTest.php @@ -265,16 +265,4 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase $response = $this->getJson('/api/application/locations/' . $location->id); $this->assertAccessDeniedJson($response); } - - /** - * Test that a location's existence is not exposed unless an API key has permission - * to access the resource. - */ - public function testResourceIsNotExposedWithoutPermissions() - { - $this->createNewDefaultApiKey($this->getApiUser(), ['r_locations' => 0]); - - $response = $this->getJson('/api/application/locations/nil'); - $this->assertAccessDeniedJson($response); - } } diff --git a/tests/Integration/Api/Application/Nests/EggControllerTest.php b/tests/Integration/Api/Application/Nests/EggControllerTest.php index 7fc523658..c513535f6 100644 --- a/tests/Integration/Api/Application/Nests/EggControllerTest.php +++ b/tests/Integration/Api/Application/Nests/EggControllerTest.php @@ -120,17 +120,4 @@ class EggControllerTest extends ApplicationApiIntegrationTestCase $response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs'); $this->assertAccessDeniedJson($response); } - - /** - * Test that a nests's existence is not exposed unless an API key has permission - * to access the resource. - */ - public function testResourceIsNotExposedWithoutPermissions() - { - $egg = Egg::query()->findOrFail(1); - $this->createNewDefaultApiKey($this->getApiUser(), ['r_eggs' => 0]); - - $response = $this->getJson('/api/application/nests/' . $egg->nest_id . '/eggs/nil'); - $this->assertAccessDeniedJson($response); - } } diff --git a/tests/Integration/Api/Application/Nests/NestControllerTest.php b/tests/Integration/Api/Application/Nests/NestControllerTest.php index 58434ec4c..b3e1def6d 100644 --- a/tests/Integration/Api/Application/Nests/NestControllerTest.php +++ b/tests/Integration/Api/Application/Nests/NestControllerTest.php @@ -127,17 +127,4 @@ class NestControllerTest extends ApplicationApiIntegrationTestCase $response = $this->getJson('/api/application/nests/' . $nest->id); $this->assertAccessDeniedJson($response); } - - /** - * Test that a nest's existence is not exposed unless an API key has permission - * to access the resource. - */ - public function testResourceIsNotExposedWithoutPermissions() - { - $nest = $this->repository->find(1); - $this->createNewDefaultApiKey($this->getApiUser(), ['r_nests' => 0]); - - $response = $this->getJson('/api/application/nests/' . $nest->id); - $this->assertAccessDeniedJson($response); - } } diff --git a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php index 983710b16..74e03df54 100644 --- a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php +++ b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php @@ -66,16 +66,4 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase $response = $this->getJson('/api/application/users/external/' . $user->external_id); $this->assertAccessDeniedJson($response); } - - /** - * Test that a users's existence is not exposed unless an API key has permission - * to access the resource. - */ - public function testResourceIsNotExposedWithoutPermissions() - { - $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); - - $response = $this->getJson('/api/application/users/external/nil'); - $this->assertAccessDeniedJson($response); - } } diff --git a/tests/Integration/Api/Application/Users/UserControllerTest.php b/tests/Integration/Api/Application/Users/UserControllerTest.php index f084d2ed2..44a739db2 100644 --- a/tests/Integration/Api/Application/Users/UserControllerTest.php +++ b/tests/Integration/Api/Application/Users/UserControllerTest.php @@ -201,18 +201,6 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase $this->assertAccessDeniedJson($response); } - /** - * Test that a users's existence is not exposed unless an API key has permission - * to access the resource. - */ - public function testResourceIsNotExposedWithoutPermissions() - { - $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); - - $response = $this->getJson('/api/application/users/nil'); - $this->assertAccessDeniedJson($response); - } - /** * Test that a user can be created. */ diff --git a/tests/Traits/Http/IntegrationJsonRequestAssertions.php b/tests/Traits/Http/IntegrationJsonRequestAssertions.php index e26235a92..2658520ee 100644 --- a/tests/Traits/Http/IntegrationJsonRequestAssertions.php +++ b/tests/Traits/Http/IntegrationJsonRequestAssertions.php @@ -20,7 +20,7 @@ trait IntegrationJsonRequestAssertions [ 'code' => 'NotFoundHttpException', 'status' => '404', - 'detail' => 'The requested resource does not exist on this server.', + 'detail' => 'The requested resource could not be found on the server.', ], ], ], true);