Add server database management support to API.

This commit is contained in:
Dane Everitt 2018-01-25 22:34:53 -06:00
parent 2bd691efad
commit de07b3cc7f
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 257 additions and 54 deletions

View file

@ -46,6 +46,14 @@ class DisplayException extends PterodactylException
return $this->level;
}
/**
* @return int
*/
public function getStatusCode()
{
return Response::HTTP_BAD_REQUEST;
}
/**
* Render the exception to the user by adding a flashed message to the session
* and then redirecting them back to the page that they came from. If the

View file

@ -3,8 +3,8 @@
namespace Pterodactyl\Extensions\Spatie\Fractalistic;
use League\Fractal\TransformerAbstract;
use Spatie\Fractal\Fractal as SpatieFractal;
use League\Fractal\Serializer\JsonApiSerializer;
use Spatie\Fractalistic\Fractal as SpatieFractal;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;

View file

@ -2,13 +2,32 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Servers;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Transformers\Api\Application\ServerDatabaseTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest;
class DatabaseController extends ApplicationApiController
{
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $databaseManagementService;
/**
* @var \Pterodactyl\Services\Databases\DatabasePasswordService
*/
private $databasePasswordService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
@ -17,12 +36,19 @@ class DatabaseController extends ApplicationApiController
/**
* DatabaseController constructor.
*
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Services\Databases\DatabasePasswordService $databasePasswordService
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
*/
public function __construct(DatabaseRepositoryInterface $repository)
{
public function __construct(
DatabaseManagementService $databaseManagementService,
DatabasePasswordService $databasePasswordService,
DatabaseRepositoryInterface $repository
) {
parent::__construct();
$this->databaseManagementService = $databaseManagementService;
$this->databasePasswordService = $databasePasswordService;
$this->repository = $repository;
}
@ -30,10 +56,11 @@ class DatabaseController extends ApplicationApiController
* Return a listing of all databases currently available to a single
* server.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array
*/
public function index(Server $server): array
public function index(GetServerDatabasesRequest $request, Server $server): array
{
$databases = $this->repository->getDatabasesForServer($server->id);
@ -41,4 +68,73 @@ class DatabaseController extends ApplicationApiController
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray();
}
/**
* Return a single server database.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return array
*/
public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array
{
return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray();
}
/**
* Reset the password for a specific server database.
*
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response
{
$this->databasePasswordService->handle($database, str_random(24));
return response('', 204);
}
/**
* Create a new database on the Panel for a given server.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
{
$database = $this->databaseManagementService->create($server->id, $request->validated());
return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->respond(201);
}
/**
* Delete a specific database from the Panel.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response
{
$this->databaseManagementService->delete($database->id);
return response('', 204);
}
}

View file

@ -3,11 +3,8 @@
namespace Pterodactyl\Http\Middleware\Api;
use Closure;
use ReflectionMethod;
use Illuminate\Container\Container;
use Illuminate\Routing\ImplicitRouteBinding;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ApiSubstituteBindings extends SubstituteBindings
{
@ -24,47 +21,18 @@ class ApiSubstituteBindings extends SubstituteBindings
$route = $request->route();
$this->router->substituteBindings($route);
$this->resolveForRoute($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);
}
/**
* Resolve the implicit route bindings for the given route. This function
* overrides Laravel's default inn \Illuminate\Routing\ImplictRouteBinding
* to not throw a 404 error when a model is not found.
*
* If a model is not found using the provided information, the binding is
* replaced with null which is then checked in the form requests on API
* routes. This avoids a potential imformation leak on API routes for
* unauthenticated users.
*
* @param \Illuminate\Routing\Route $route
*/
protected function resolveForRoute($route)
{
$parameters = $route->parameters();
// Avoid having to copy and paste the entirety of that class into this middleware
// by using reflection to access a protected method.
$reflection = new ReflectionMethod(ImplicitRouteBinding::class, 'getParameterName');
$reflection->setAccessible(true);
foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
if (! $parameterName = $reflection->invokeArgs(null, [$parameter->name, $parameters])) {
continue;
}
$parameterValue = $parameters[$parameterName];
if ($parameterValue instanceof UrlRoutable) {
continue;
}
// Try to find an existing model, if one is not found simply bind the
// parameter as null.
$instance = Container::getInstance()->make($parameter->getClass()->name);
$route->setParameter($parameterName, $instance->resolveRouteBinding($parameterValue));
}
}
}

View file

@ -73,7 +73,7 @@ abstract class ApplicationApiRequest extends FormRequest
return $this->attributes->get('api_key');
}
/**
/*
* Determine if the request passes the authorization check as well
* as the exists check.
*
@ -81,18 +81,24 @@ abstract class ApplicationApiRequest extends FormRequest
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
/**
* @return bool
*/
protected function passesAuthorization()
{
$passes = parent::passesAuthorization();
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 ($passes && ! $this->resourceExists()) {
if ($this->attributes->get('is_missing_model', false) || ! $this->resourceExists()) {
throw new NotFoundHttpException('The requested resource does not exist on this server.');
}
return $passes;
return true;
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class GetServerDatabaseRequest extends ApplicationApiRequest
{
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
/**
* Determine if the requested server database exists.
*
* @return bool
*/
public function resourceExists(): bool
{
$server = $this->route()->parameter('server');
$database = $this->route()->parameter('database');
return $database->server_id === $server->id;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class GetServerDatabasesRequest extends ApplicationApiRequest
{
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
}

View file

@ -0,0 +1,13 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Pterodactyl\Services\Acl\Api\AdminAcl;
class ServerDatabaseWriteRequest extends GetServerDatabasesRequest
{
/**
* @var int
*/
protected $permission = AdminAcl::WRITE;
}

View file

@ -0,0 +1,61 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerDatabaseRequest extends ApplicationApiRequest
{
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVER_DATABASES;
/**
* @var int
*/
protected $permission = AdminAcl::WRITE;
/**
* Validation rules for database creation.
*
* @return array
*/
public function rules(): array
{
return [
'database' => 'required|string|min:1|max:24',
'remote' => 'required|string|min:1',
'host' => 'required|integer|exists:database_hosts,id',
];
}
/**
* Return data formatted in the correct format for the service to consume.
*
* @return array
*/
public function validated()
{
return [
'database' => $this->input('database'),
'remote' => $this->input('remote'),
'database_host_id' => $this->input('host'),
];
}
/**
* Format error messages in a more understandable format for API output.
*
* @return array
*/
public function attributes()
{
return [
'host' => 'Database Host Server ID',
'remote' => 'Remote Connection String',
'database' => 'Database Name',
];
}
}

View file

@ -89,7 +89,7 @@ Route::group(['prefix' => '/servers'], function () {
Route::get('/{database}', 'Servers\DatabaseController@view')->name('api.application.servers.databases.view');
Route::post('/', 'Servers\DatabaseController@store');
Route::patch('/{database}', 'Servers\DatabaseController@update');
Route::post('/{database}/reset-password', 'Servers\DatabaseController@resetPassword');
Route::delete('/{database}', 'Servers\DatabaseController@delete');
});