Prevent creating a new database via the application API if server is at its limit; closes #2129

This commit is contained in:
Dane Everitt 2020-06-23 20:07:37 -07:00
parent 910a48ee8a
commit b78aa180ea
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
6 changed files with 116 additions and 76 deletions

View file

@ -312,12 +312,12 @@ class ServersController extends Controller
* Creates a new database assigned to a specific server. * Creates a new database assigned to a specific server.
* *
* @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request * @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request
* @param int $server * @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Exception * @throws \Throwable
*/ */
public function newDatabase(StoreServerDatabaseRequest $request, $server) public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
{ {
$this->databaseManagementService->create($server, [ $this->databaseManagementService->create($server, [
'database' => $request->input('database'), 'database' => $request->input('database'),
@ -326,7 +326,7 @@ class ServersController extends Controller
'max_connections' => $request->input('max_connections'), 'max_connections' => $request->input('max_connections'),
]); ]);
return redirect()->route('admin.servers.view.database', $server)->withInput(); return redirect()->route('admin.servers.view.database', $server->id)->withInput();
} }
/** /**

View file

@ -57,13 +57,12 @@ class DatabaseController extends ApplicationApiController
* server. * server.
* *
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array * @return array
*/ */
public function index(GetServerDatabasesRequest $request): array public function index(GetServerDatabasesRequest $request, Server $server): array
{ {
$databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id); return $this->fractal->collection($server->databases)
return $this->fractal->collection($databases)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray(); ->toArray();
} }
@ -72,11 +71,13 @@ class DatabaseController extends ApplicationApiController
* Return a single server database. * Return a single server database.
* *
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return array * @return array
*/ */
public function view(GetServerDatabaseRequest $request): array public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array
{ {
return $this->fractal->item($request->getModel(Database::class)) return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray(); ->toArray();
} }
@ -85,29 +86,31 @@ class DatabaseController extends ApplicationApiController
* Reset the password for a specific server database. * Reset the password for a specific server database.
* *
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request
* @return \Illuminate\Http\Response * @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return \Illuminate\Http\JsonResponse
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function resetPassword(ServerDatabaseWriteRequest $request): Response public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse
{ {
$this->databasePasswordService->handle($request->getModel(Database::class)); $this->databasePasswordService->handle($database);
return response('', 204); return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
} }
/** /**
* Create a new database on the Panel for a given server. * Create a new database on the Panel for a given server.
* *
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* *
* @throws \Exception * @throws \Throwable
*/ */
public function store(StoreServerDatabaseRequest $request): JsonResponse public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
{ {
$server = $request->getModel(Server::class); $database = $this->databaseManagementService->create($server, $request->validated());
$database = $this->databaseManagementService->create($server->id, $request->validated());
return $this->fractal->item($database) return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
@ -117,7 +120,7 @@ class DatabaseController extends ApplicationApiController
'database' => $database->id, 'database' => $database->id,
]), ]),
]) ])
->respond(201); ->respond(Response::HTTP_CREATED);
} }
/** /**

View file

@ -69,9 +69,7 @@ class DatabaseController extends ClientApiController
*/ */
public function index(GetDatabasesRequest $request, Server $server): array public function index(GetDatabasesRequest $request, Server $server): array
{ {
$databases = $this->repository->getDatabasesForServer($server->id); return $this->fractal->collection($server->databases)
return $this->fractal->collection($databases)
->transformWith($this->getTransformer(DatabaseTransformer::class)) ->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray(); ->toArray();
} }
@ -83,6 +81,8 @@ class DatabaseController extends ClientApiController
* @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @return array * @return array
* *
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/ */
public function store(StoreDatabaseRequest $request, Server $server): array public function store(StoreDatabaseRequest $request, Server $server): array

View file

@ -2,6 +2,21 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $server_id
* @property int $database_host_id
* @property string $database
* @property string $username
* @property string $remote
* @property string $password
* @property int $max_connections
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Server $server
* @property \Pterodactyl\Models\DatabaseHost $host
*/
class Database extends Model class Database extends Model
{ {
/** /**

View file

@ -3,19 +3,22 @@
namespace Pterodactyl\Services\Databases; namespace Pterodactyl\Services\Databases;
use Exception; use Exception;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database; use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities; use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DatabaseManagementService class DatabaseManagementService
{ {
/** /**
* @var \Illuminate\Database\DatabaseManager * @var \Illuminate\Database\ConnectionInterface
*/ */
private $database; private $connection;
/** /**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection * @var \Pterodactyl\Extensions\DynamicDatabaseConnection
@ -33,84 +36,113 @@ class DatabaseManagementService
private $repository; private $repository;
/** /**
* Determines if the service should validate the user's ability to create an additional
* database for this server. In almost all cases this should be true, but to keep things
* flexible you can also set it to false and create more databases than the server is
* allocated.
*
* @var bool * @var bool
*/ */
protected $useRandomHost = false; protected $validateDatabaseLimit = true;
/** /**
* CreationService constructor. * CreationService constructor.
* *
* @param \Illuminate\Database\DatabaseManager $database * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/ */
public function __construct( public function __construct(
DatabaseManager $database, ConnectionInterface $connection,
DynamicDatabaseConnection $dynamic, DynamicDatabaseConnection $dynamic,
DatabaseRepositoryInterface $repository, DatabaseRepositoryInterface $repository,
Encrypter $encrypter Encrypter $encrypter
) { ) {
$this->database = $database; $this->connection = $connection;
$this->dynamic = $dynamic; $this->dynamic = $dynamic;
$this->encrypter = $encrypter; $this->encrypter = $encrypter;
$this->repository = $repository; $this->repository = $repository;
} }
/**
* Set wether or not this class should validate that the server has enough slots
* left before creating the new database.
*
* @param bool $validate
* @return $this
*/
public function setValidateDatabaseLimit(bool $validate): self
{
$this->validateDatabaseLimit = $validate;
return $this;
}
/** /**
* Create a new database that is linked to a specific host. * Create a new database that is linked to a specific host.
* *
* @param int $server * @param \Pterodactyl\Models\Server $server
* @param array $data * @param array $data
* @return \Pterodactyl\Models\Database * @return \Pterodactyl\Models\Database
* *
* @throws \Exception * @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/ */
public function create($server, array $data) public function create(Server $server, array $data)
{ {
$data['server_id'] = $server; if (! config('pterodactyl.client_features.databases.enabled')) {
$data['database'] = sprintf('s%d_%s', $server, $data['database']); throw new DatabaseClientFeatureNotEnabledException;
$data['username'] = sprintf('u%d_%s', $server, str_random(10)); }
$data['password'] = $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24) if ($this->validateDatabaseLimit) {
); // If the server has a limit assigned and we've already reached that limit, throw back
// an exception and kill the process.
if (! is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
throw new TooManyDatabasesException;
}
}
$data = array_merge($data, [
'server_id' => $server->id,
'database' => sprintf('s%d_%s', $server->id, $data['database']),
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
),
]);
$database = null;
$this->database->beginTransaction();
try { try {
$database = $this->repository->createIfNotExists($data); return $this->connection->transaction(function () use ($data, &$database) {
$this->dynamic->set('dynamic', $data['database_host_id']); $database = $this->repository->createIfNotExists($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database); $this->repository->createDatabase($database->database);
$this->repository->createUser( $this->repository->createUser(
$database->username, $database->username, $database->remote, $this->encrypter->decrypt($database->password), $database->max_connections
$database->remote, );
$this->encrypter->decrypt($database->password), $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
$database->max_connections $this->repository->flush();
);
$this->repository->assignUserToDatabase(
$database->database,
$database->username,
$database->remote
);
$this->repository->flush();
$this->database->commit(); return $database;
} catch (Exception $ex) { });
} catch (Exception $exception) {
try { try {
if (isset($database) && $database instanceof Database) { if ($database instanceof Database) {
$this->repository->dropDatabase($database->database); $this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote); $this->repository->dropUser($database->username, $database->remote);
$this->repository->flush(); $this->repository->flush();
} }
} catch (Exception $exTwo) { } catch (Exception $exception) {
// ignore an exception // Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
} }
$this->database->rollBack(); throw $exception;
throw $ex;
} }
return $database;
} }
/** /**

View file

@ -6,9 +6,7 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database; use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException; use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DeployServerDatabaseService class DeployServerDatabaseService
{ {
@ -49,20 +47,12 @@ class DeployServerDatabaseService
* @param array $data * @param array $data
* @return \Pterodactyl\Models\Database * @return \Pterodactyl\Models\Database
* *
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
* @throws \Exception
*/ */
public function handle(Server $server, array $data): Database public function handle(Server $server, array $data): Database
{ {
if (! config('pterodactyl.client_features.databases.enabled')) {
throw new DatabaseClientFeatureNotEnabledException;
}
$databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]);
if (! is_null($server->database_limit) && $databases >= $server->database_limit) {
throw new TooManyDatabasesException;
}
$allowRandom = config('pterodactyl.client_features.databases.allow_random'); $allowRandom = config('pterodactyl.client_features.databases.allow_random');
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
['node_id', '=', $server->node_id], ['node_id', '=', $server->node_id],
@ -81,7 +71,7 @@ class DeployServerDatabaseService
$host = $hosts->random(); $host = $hosts->random();
return $this->managementService->create($server->id, [ return $this->managementService->create($server, [
'database_host_id' => $host->id, 'database_host_id' => $host->id,
'database' => array_get($data, 'database'), 'database' => array_get($data, 'database'),
'remote' => array_get($data, 'remote'), 'remote' => array_get($data, 'remote'),