diff --git a/app/Contracts/Repository/SubuserRepositoryInterface.php b/app/Contracts/Repository/SubuserRepositoryInterface.php index 766fc1b35..a31023f6b 100644 --- a/app/Contracts/Repository/SubuserRepositoryInterface.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -26,4 +26,13 @@ namespace Pterodactyl\Contracts\Repository; interface SubuserRepositoryInterface extends RepositoryInterface { + /** + * Find a subuser and return with server and permissions relationships. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithServerAndPermissions($id); } diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index cda8864ec..4909ecd1c 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -25,7 +25,9 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Subuser; +use Webmozart\Assert\Assert; class SubuserRepository extends EloquentRepository implements SubuserRepositoryInterface { @@ -36,4 +38,19 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI { return Subuser::class; } + + /** + * {@inheritdoc} + */ + public function getWithServerAndPermissions($id) + { + Assert::numeric($id, 'First argument passed to getWithServerAndPermissions must be numeric, received %s.'); + + $instance = $this->getBuilder()->with(['server', 'permission'])->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } } diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 39c38f18d..8f6e83292 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -177,6 +177,7 @@ class SubuserCreationService return $subuser; } catch (RequestException $exception) { + $this->connection->rollBack(); $response = $exception->getResponse(); $this->writer->warning($exception); diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php new file mode 100644 index 000000000..d06f57bd7 --- /dev/null +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -0,0 +1,108 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Subusers; + +use Illuminate\Log\Writer; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserDeletionService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * SubuserDeletionService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $connection, + DaemonServerRepositoryInterface $daemonRepository, + SubuserRepositoryInterface $repository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Delete a subuser and their associated permissions from the Panel and Daemon. + * + * @param int $subuser + * @return int|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($subuser) + { + $subuser = $this->repository->getWithServerAndPermissions($subuser); + + $this->connection->beginTransaction(); + $response = $this->repository->delete($subuser->id); + + try { + $this->daemonRepository->setNode($subuser->server->node_id)->setAccessServer($subuser->server->uuid) + ->setSubuserKey($subuser->daemonSecret, []); + $this->connection->commit(); + + return $response; + } catch (RequestException $exception) { + $this->connection->rollBack(); + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 398915ac7..b8b2f123e 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -16,7 +16,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), - 'uuid' => $faker->uuid, + 'node_id' => $faker->randomNumber(), + 'uuid' => $faker->unique()->uuid, 'uuidShort' => str_random(8), 'name' => $faker->firstName, 'description' => implode(' ', $faker->sentences()), diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php new file mode 100644 index 000000000..e465eb16a --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -0,0 +1,139 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Services\Subusers\SubuserDeletionService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserDeletionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserDeletionService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->repository = m::mock(SubuserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserDeletionService( + $this->connection, + $this->daemonRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that a subuser is deleted correctly. + */ + public function testSubuserIsDeleted() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); + + $this->daemonRepository->shouldReceive('setNode')->with($subuser->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($subuser->server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, [])->once()->andReturnNull(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($subuser->id); + $this->assertEquals(1, $response); + } + + /** + * Test that an exception caused by the daemon is properly handled. + */ + public function testExceptionIsThrownIfDaemonConnectionFails() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); + + $this->daemonRepository->shouldReceive('setNode->setAccessServer->setSubuserKey')->once()->andThrow($this->exception); + + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + + try { + $this->service->handle($subuser->id); + } catch (DisplayException $exception) { + $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + } + } + +}