From 20beb2f2808f4beb07a9e1601c14f908b7c9aa06 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 1 Dec 2017 20:10:06 -0600 Subject: [PATCH 1/9] Fix error causing tasks to be un-deletable. closes #786 --- .php_cs | 2 +- .../Controllers/Server/Tasks/TaskManagementController.php | 4 +--- app/Http/Middleware/Server/ScheduleBelongsToServer.php | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.php_cs b/.php_cs index c2bbfe8c6..c854af47c 100644 --- a/.php_cs +++ b/.php_cs @@ -51,5 +51,5 @@ return PhpCsFixer\Config::create() 'equal' => false, 'identical' => false, 'less_and_greater' => false, - ] + ], ])->setRiskyAllowed(true)->setFinder($finder); diff --git a/app/Http/Controllers/Server/Tasks/TaskManagementController.php b/app/Http/Controllers/Server/Tasks/TaskManagementController.php index e61a560fa..9e6782e93 100644 --- a/app/Http/Controllers/Server/Tasks/TaskManagementController.php +++ b/app/Http/Controllers/Server/Tasks/TaskManagementController.php @@ -149,8 +149,6 @@ class TaskManagementController extends Controller * * @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request * @return \Illuminate\Http\RedirectResponse - * - * @throws \Illuminate\Auth\Access\AuthorizationException */ public function update(ScheduleCreationFormRequest $request): RedirectResponse { @@ -177,7 +175,7 @@ class TaskManagementController extends Controller */ public function delete(Request $request): Response { - $server = $request->attributes->get('server_data.model'); + $server = $request->attributes->get('server'); $schedule = $request->attributes->get('schedule'); $this->authorize('delete-schedule', $server); diff --git a/app/Http/Middleware/Server/ScheduleBelongsToServer.php b/app/Http/Middleware/Server/ScheduleBelongsToServer.php index f9f40bf3b..26da5f843 100644 --- a/app/Http/Middleware/Server/ScheduleBelongsToServer.php +++ b/app/Http/Middleware/Server/ScheduleBelongsToServer.php @@ -49,7 +49,7 @@ class ScheduleBelongsToServer $scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0); $schedule = $this->repository->getScheduleWithTasks($scheduleId); - if (object_get($schedule, 'server_id') !== $server->id) { + if ($schedule->server_id !== $server->id) { throw new NotFoundHttpException; } From 975597b4d0c3c2975bae0d295227726ed9acbd80 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 3 Dec 2017 14:00:47 -0600 Subject: [PATCH 2/9] Implement changes to administrative user revocation, closes #733 --- CHANGELOG.md | 4 + .../Daemon/ServerRepositoryInterface.php | 4 +- .../DaemonKeyRepositoryInterface.php | 20 +++ .../Connection/DaemonConnectionException.php | 7 -- app/Http/Controllers/Admin/UserController.php | 29 +++-- .../Controllers/Base/AccountController.php | 34 +----- app/Http/Middleware/AdminAuthenticate.php | 2 + app/Http/Middleware/DaemonAuthenticate.php | 2 + .../Server/AuthenticateAsSubuser.php | 3 +- app/Http/Requests/Admin/UserFormRequest.php | 8 +- app/Repositories/Daemon/ServerRepository.php | 8 +- .../Eloquent/DaemonKeyRepository.php | 26 ++++ .../RevokeMultipleDaemonKeysService.php | 91 ++++++++++++++ app/Services/Users/UserUpdateService.php | 56 ++++++--- resources/lang/en/exceptions.php | 3 + .../pterodactyl/admin/users/view.blade.php | 10 +- .../Base/AccountControllerTest.php | 71 ++++++----- .../RevokeMultipleDaemonKeysServiceTest.php | 115 ++++++++++++++++++ .../Services/Users/UserUpdateServiceTest.php | 90 +++++++++++--- 19 files changed, 458 insertions(+), 125 deletions(-) create mode 100644 app/Services/DaemonKeys/RevokeMultipleDaemonKeysService.php create mode 100644 tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 027de647d..807185eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.2]` — Fixes a bug that would throw a red page of death when submitting an invalid egg variable value for a server in the Admin CP. * `[beta.2]` — Someone found a `@todo` that I never `@todid` and thus database hosts could not be created without being linked to a node. This is fixed... * `[beta.2]` — Fixes bug that caused incorrect rendering of CPU usage on server graphs due to missing variable. +* `[beta.2]` — Fixes bug causing schedules to be un-deletable. + +### Changed +* Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. ## v0.7.0-beta.2 (Derelict Dermodactylus) ### Fixed diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index 6b7a86d45..19806ac49 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -78,8 +78,10 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface /** * Revoke an access key on the daemon before the time is expired. * - * @param string $key + * @param string|array $key * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ public function revokeAccessKey($key); } diff --git a/app/Contracts/Repository/DaemonKeyRepositoryInterface.php b/app/Contracts/Repository/DaemonKeyRepositoryInterface.php index 572f18c24..4f2156ad5 100644 --- a/app/Contracts/Repository/DaemonKeyRepositoryInterface.php +++ b/app/Contracts/Repository/DaemonKeyRepositoryInterface.php @@ -24,7 +24,9 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\User; use Pterodactyl\Models\DaemonKey; +use Illuminate\Support\Collection; interface DaemonKeyRepositoryInterface extends RepositoryInterface { @@ -59,4 +61,22 @@ interface DaemonKeyRepositoryInterface extends RepositoryInterface * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getKeyWithServer($key); + + /** + * Get all of the keys for a specific user including the information needed + * from their server relation for revocation on the daemon. + * + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection + */ + public function getKeysForRevocation(User $user): Collection; + + /** + * Delete an array of daemon keys from the database. Used primarily in + * conjunction with getKeysForRevocation. + * + * @param array $ids + * @return bool|int + */ + public function deleteKeys(array $ids); } diff --git a/app/Exceptions/Http/Connection/DaemonConnectionException.php b/app/Exceptions/Http/Connection/DaemonConnectionException.php index 5718af63f..2e602f80e 100644 --- a/app/Exceptions/Http/Connection/DaemonConnectionException.php +++ b/app/Exceptions/Http/Connection/DaemonConnectionException.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Http\Connection; diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 20594f333..ad5dead01 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Admin; @@ -160,10 +153,30 @@ class UserController extends Controller * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function update(UserFormRequest $request, User $user) { - $this->updateService->handle($user->id, $request->normalize()); + $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); + $data = $this->updateService->handle($user, $request->normalize()); + + if (! empty($data->get('exceptions'))) { + foreach ($data->get('exceptions') as $node => $exception) { + /** @var \GuzzleHttp\Exception\RequestException $exception */ + /** @var \GuzzleHttp\Psr7\Response|null $response */ + $response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null; + $message = trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ]); + + $this->alert->danger(trans('exceptions.users.node_revocation_failed', [ + 'node' => $node, + 'error' => $message, + 'link' => route('admin.nodes.view', $node), + ]))->flash(); + } + } + $this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash(); return redirect()->route('admin.users.view', $user->id); diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index fea7f09dd..b6a433bb4 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -1,30 +1,8 @@ - * Some Modifications (c) 2015 Dylan Seidt . - * - * 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\Http\Controllers\Base; +use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\UserUpdateService; @@ -48,10 +26,8 @@ class AccountController extends Controller * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Services\Users\UserUpdateService $updateService */ - public function __construct( - AlertsMessageBag $alert, - UserUpdateService $updateService - ) { + public function __construct(AlertsMessageBag $alert, UserUpdateService $updateService) + { $this->alert = $alert; $this->updateService = $updateService; } @@ -74,6 +50,7 @@ class AccountController extends Controller * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function update(AccountDataFormRequest $request) { @@ -86,7 +63,8 @@ class AccountController extends Controller $data = $request->only(['name_first', 'name_last', 'username']); } - $this->updateService->handle($request->user()->id, $data); + $this->updateService->setUserLevel(User::USER_LEVEL_USER); + $this->updateService->handle($request->user(), $data); $this->alert->success(trans('base.account.details_updated'))->flash(); return redirect()->route('account'); diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index c9b51bdc2..6307669c3 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -21,6 +21,8 @@ class AdminAuthenticate * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function handle(Request $request, Closure $next) { diff --git a/app/Http/Middleware/DaemonAuthenticate.php b/app/Http/Middleware/DaemonAuthenticate.php index 775a7783c..6bd2908cf 100644 --- a/app/Http/Middleware/DaemonAuthenticate.php +++ b/app/Http/Middleware/DaemonAuthenticate.php @@ -46,6 +46,8 @@ class DaemonAuthenticate * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function handle(Request $request, Closure $next) { diff --git a/app/Http/Middleware/Server/AuthenticateAsSubuser.php b/app/Http/Middleware/Server/AuthenticateAsSubuser.php index 8ec07d94d..8f8a158ca 100644 --- a/app/Http/Middleware/Server/AuthenticateAsSubuser.php +++ b/app/Http/Middleware/Server/AuthenticateAsSubuser.php @@ -47,9 +47,8 @@ class AuthenticateAsSubuser * @param \Closure $next * @return mixed * - * @throws \Illuminate\Auth\AuthenticationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function handle(Request $request, Closure $next) { diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index e65a57f97..d705c86ed 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -19,7 +19,11 @@ class UserFormRequest extends AdminFormRequest public function rules() { if ($this->method() === 'PATCH') { - return User::getUpdateRulesForId($this->route()->parameter('user')->id); + $rules = User::getUpdateRulesForId($this->route()->parameter('user')->id); + + return array_merge($rules, [ + 'ignore_connection_error' => 'sometimes|nullable|boolean', + ]); } return User::getCreateRules(); @@ -30,7 +34,7 @@ class UserFormRequest extends AdminFormRequest if ($this->method === 'PATCH') { return array_merge( $this->intersect('password'), - $this->only(['email', 'username', 'name_first', 'name_last', 'root_admin']) + $this->only(['email', 'username', 'name_first', 'name_last', 'root_admin', 'ignore_connection_error']) ); } diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 3515b26e4..f21ec197a 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -107,7 +107,13 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa */ public function revokeAccessKey($key) { - Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string, received %s.'); + if (is_array($key)) { + return $this->getHttpClient()->request('POST', 'keys', [ + 'json' => $key, + ]); + } + + Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string or array, received %s.'); return $this->getHttpClient()->request('DELETE', 'keys/' . $key); } diff --git a/app/Repositories/Eloquent/DaemonKeyRepository.php b/app/Repositories/Eloquent/DaemonKeyRepository.php index 533128f46..97fe0c2bb 100644 --- a/app/Repositories/Eloquent/DaemonKeyRepository.php +++ b/app/Repositories/Eloquent/DaemonKeyRepository.php @@ -24,8 +24,10 @@ namespace Pterodactyl\Repositories\Eloquent; +use Pterodactyl\Models\User; use Webmozart\Assert\Assert; use Pterodactyl\Models\DaemonKey; +use Illuminate\Support\Collection; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; @@ -83,4 +85,28 @@ class DaemonKeyRepository extends EloquentRepository implements DaemonKeyReposit return $instance; } + + /** + * Get all of the keys for a specific user including the information needed + * from their server relation for revocation on the daemon. + * + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection + */ + public function getKeysForRevocation(User $user): Collection + { + return $this->getBuilder()->with('server:id,uuid,node_id')->where('user_id', $user->id)->get($this->getColumns()); + } + + /** + * Delete an array of daemon keys from the database. Used primarily in + * conjunction with getKeysForRevocation. + * + * @param array $ids + * @return bool|int + */ + public function deleteKeys(array $ids) + { + return $this->getBuilder()->whereIn('id', $ids)->delete(); + } } diff --git a/app/Services/DaemonKeys/RevokeMultipleDaemonKeysService.php b/app/Services/DaemonKeys/RevokeMultipleDaemonKeysService.php new file mode 100644 index 000000000..93b8b2041 --- /dev/null +++ b/app/Services/DaemonKeys/RevokeMultipleDaemonKeysService.php @@ -0,0 +1,91 @@ +daemonRepository = $daemonRepository; + $this->repository = $repository; + } + + /** + * Grab all of the keys that exist for a single user and delete them from all + * daemon's that they are assigned to. If connection fails, this function will + * return an error. + * + * @param \Pterodactyl\Models\User $user + * @param bool $ignoreConnectionErrors + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function handle(User $user, bool $ignoreConnectionErrors = false) + { + $keys = $this->repository->getKeysForRevocation($user); + + $keys->groupBy('server.node_id')->each(function ($group, $node) use ($ignoreConnectionErrors) { + try { + $this->daemonRepository->setNode($node)->revokeAccessKey(collect($group)->pluck('secret')->toArray()); + } catch (RequestException $exception) { + if (! $ignoreConnectionErrors) { + throw new DaemonConnectionException($exception); + } + + $this->setConnectionException($node, $exception); + } + + $this->repository->deleteKeys(collect($group)->pluck('id')->toArray()); + }); + } + + /** + * Returns an array of exceptions that were returned by the handle function. + * + * @return RequestException[] + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Add an exception for a node to the array. + * + * @param int $node + * @param \GuzzleHttp\Exception\RequestException $exception + */ + protected function setConnectionException(int $node, RequestException $exception) + { + $this->exceptions[$node] = $exception; + } +} diff --git a/app/Services/Users/UserUpdateService.php b/app/Services/Users/UserUpdateService.php index dbebe4d95..9e755dd9f 100644 --- a/app/Services/Users/UserUpdateService.php +++ b/app/Services/Users/UserUpdateService.php @@ -1,59 +1,79 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Users; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService; class UserUpdateService { + use HasUserLevels; + /** * @var \Illuminate\Contracts\Hashing\Hasher */ - protected $hasher; + private $hasher; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $repository; + private $repository; + + /** + * @var \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService + */ + private $revocationService; /** * UpdateService constructor. * - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService $revocationService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( Hasher $hasher, + RevokeMultipleDaemonKeysService $revocationService, UserRepositoryInterface $repository ) { $this->hasher = $hasher; $this->repository = $repository; + $this->revocationService = $revocationService; } /** - * Update the user model instance. + * Update the user model instance. If the user has been removed as an administrator + * revoke all of the authentication tokens that have beenn assigned to their account. * - * @param int $id - * @param array $data - * @return mixed + * @param \Pterodactyl\Models\User $user + * @param array $data + * @return \Illuminate\Support\Collection * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function handle($id, array $data) + public function handle(User $user, array $data): Collection { - if (isset($data['password'])) { + if (array_has($data, 'password')) { $data['password'] = $this->hasher->make($data['password']); } - return $this->repository->update($id, $data); + if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { + if (array_get($data, 'root_admin', 0) == 0 && $user->root_admin) { + $this->revocationService->handle($user, array_get($data, 'ignore_connection_error', false)); + } + } else { + unset($data['root_admin']); + } + + return collect([ + 'model' => $this->repository->update($user->id, $data), + 'exceptions' => $this->revocationService->getExceptions(), + ]); } } diff --git a/resources/lang/en/exceptions.php b/resources/lang/en/exceptions.php index e5bee177d..f32b9c71a 100644 --- a/resources/lang/en/exceptions.php +++ b/resources/lang/en/exceptions.php @@ -59,4 +59,7 @@ return [ 'locations' => [ 'has_nodes' => 'Cannot delete a location that has active nodes attached to it.', ], + 'users' => [ + 'node_revocation_failed' => 'Failed to revoke keys on Node #:node. :error', + ], ]; diff --git a/resources/themes/pterodactyl/admin/users/view.blade.php b/resources/themes/pterodactyl/admin/users/view.blade.php index 1c1946ecb..61604a0aa 100644 --- a/resources/themes/pterodactyl/admin/users/view.blade.php +++ b/resources/themes/pterodactyl/admin/users/view.blade.php @@ -66,10 +66,11 @@
-
- +
+
+

Leave blank to keep this user's password the same. User will not receive any notification if password is changed.

@@ -90,6 +91,11 @@

Setting this to 'Yes' gives a user full administrative access.

+
+ + +

If checked, any errors thrown while revoking keys across nodes will be ignored. You should avoid this checkbox if possible as any non-revoked keys could continue to be active for up to 24 hours after this account is changed. If you are needing to revoke account permissions immediately and are facing node issues, you should check this box and then restart any nodes that failed to be updated to clear out any stored keys.

+
diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php index 896726d52..e2fbb0774 100644 --- a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php @@ -1,43 +1,24 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; -use Tests\TestCase; +use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; -use Tests\Assertions\ControllerAssertionsTrait; use Pterodactyl\Services\Users\UserUpdateService; +use Tests\Unit\Http\Controllers\ControllerTestCase; use Pterodactyl\Http\Controllers\Base\AccountController; use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; -class AccountControllerTest extends TestCase +class AccountControllerTest extends ControllerTestCase { - use ControllerAssertionsTrait; - /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ protected $alert; /** - * @var \Pterodactyl\Http\Controllers\Base\AccountController - */ - protected $controller; - - /** - * @var \Pterodactyl\Http\Requests\Base\AccountDataFormRequest - */ - protected $request; - - /** - * @var \Pterodactyl\Services\Users\UserUpdateService + * @var \Pterodactyl\Services\Users\UserUpdateService|\Mockery\Mock */ protected $updateService; @@ -49,10 +30,7 @@ class AccountControllerTest extends TestCase parent::setUp(); $this->alert = m::mock(AlertsMessageBag::class); - $this->request = m::mock(AccountDataFormRequest::class); $this->updateService = m::mock(UserUpdateService::class); - - $this->controller = new AccountController($this->alert, $this->updateService); } /** @@ -60,7 +38,7 @@ class AccountControllerTest extends TestCase */ public function testIndexController() { - $response = $this->controller->index(); + $response = $this->getController()->index(); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.account', $response); @@ -71,14 +49,17 @@ class AccountControllerTest extends TestCase */ public function testUpdateControllerForPassword() { + $this->setRequestMockClass(AccountDataFormRequest::class); + $user = $this->setRequestUser(); + $this->request->shouldReceive('input')->with('do_action')->andReturn('password'); $this->request->shouldReceive('input')->with('new_password')->once()->andReturn('test-password'); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); - $this->updateService->shouldReceive('handle')->with(1, ['password' => 'test-password'])->once()->andReturnNull(); + $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull(); + $this->updateService->shouldReceive('handle')->with($user, ['password' => 'test-password'])->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); - $response = $this->controller->update($this->request); + $response = $this->getController()->update($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account', $response); } @@ -88,14 +69,17 @@ class AccountControllerTest extends TestCase */ public function testUpdateControllerForEmail() { + $this->setRequestMockClass(AccountDataFormRequest::class); + $user = $this->setRequestUser(); + $this->request->shouldReceive('input')->with('do_action')->andReturn('email'); $this->request->shouldReceive('input')->with('new_email')->once()->andReturn('test@example.com'); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); - $this->updateService->shouldReceive('handle')->with(1, ['email' => 'test@example.com'])->once()->andReturnNull(); + $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull(); + $this->updateService->shouldReceive('handle')->with($user, ['email' => 'test@example.com'])->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); - $response = $this->controller->update($this->request); + $response = $this->getController()->update($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account', $response); } @@ -105,17 +89,30 @@ class AccountControllerTest extends TestCase */ public function testUpdateControllerForIdentity() { + $this->setRequestMockClass(AccountDataFormRequest::class); + $user = $this->setRequestUser(); + $this->request->shouldReceive('input')->with('do_action')->andReturn('identity'); $this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([ 'test_data' => 'value', ]); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); - $this->updateService->shouldReceive('handle')->with(1, ['test_data' => 'value'])->once()->andReturnNull(); + $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull(); + $this->updateService->shouldReceive('handle')->with($user, ['test_data' => 'value'])->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); - $response = $this->controller->update($this->request); + $response = $this->getController()->update($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account', $response); } + + /** + * Return an instance of the controller for testing. + * + * @return \Pterodactyl\Http\Controllers\Base\AccountController + */ + private function getController(): AccountController + { + return new AccountController($this->alert, $this->updateService); + } } diff --git a/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php b/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php new file mode 100644 index 000000000..950824cb3 --- /dev/null +++ b/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php @@ -0,0 +1,115 @@ +daemonRepository = m::mock(ServerRepositoryInterface::class); + $this->repository = m::mock(DaemonKeyRepositoryInterface::class); + } + + /** + * Test that keys can be successfully revoked. + */ + public function testSuccessfulKeyRevocation() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(); + $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); + $key->setRelation('server', $server); + + $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf(); + $this->daemonRepository->shouldReceive('revokeAccessKey')->with([$key->secret])->once()->andReturnNull(); + + $this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull(); + + $this->getService()->handle($user); + $this->assertTrue(true); + } + + /** + * Test that an exception thrown by a call to the daemon is handled. + * + * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function testExceptionThrownFromDaemonCallIsHandled() + { + $this->configureExceptionMock(); + + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(); + $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); + $key->setRelation('server', $server); + + $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); + $this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock()); + + $this->getService()->handle($user); + } + + /** + * Test that the behavior for handling exceptions that should not be thrown + * immediately is working correctly and adds them to the array. + */ + public function testIgnoredExceptionsAreHandledProperly() + { + $this->configureExceptionMock(); + + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(); + $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); + $key->setRelation('server', $server); + + $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); + $this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock()); + + $this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull(); + + $service = $this->getService(); + $service->handle($user, true); + $this->assertNotEmpty($service->getExceptions()); + $this->assertArrayHasKey($server->node_id, $service->getExceptions()); + $this->assertSame(array_get($service->getExceptions(), $server->node_id), $this->getExceptionMock()); + $this->assertTrue(true); + } + + /** + * Return an instance of the service for testing. + * + * @return \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService + */ + private function getService(): RevokeMultipleDaemonKeysService + { + return new RevokeMultipleDaemonKeysService($this->repository, $this->daemonRepository); + } +} diff --git a/tests/Unit/Services/Users/UserUpdateServiceTest.php b/tests/Unit/Services/Users/UserUpdateServiceTest.php index 923381e29..9d794c69f 100644 --- a/tests/Unit/Services/Users/UserUpdateServiceTest.php +++ b/tests/Unit/Services/Users/UserUpdateServiceTest.php @@ -1,36 +1,32 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Users; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; use Illuminate\Contracts\Hashing\Hasher; use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService; class UserUpdateServiceTest extends TestCase { /** - * @var \Illuminate\Contracts\Hashing\Hasher + * @var \Illuminate\Contracts\Hashing\Hasher|\Mockery\Mock */ - protected $hasher; + private $hasher; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $repository; /** - * @var \Pterodactyl\Services\Users\UserUpdateService + * @var \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService|\Mockery\Mock */ - protected $service; + private $revocationService; /** * Setup tests. @@ -41,8 +37,7 @@ class UserUpdateServiceTest extends TestCase $this->hasher = m::mock(Hasher::class); $this->repository = m::mock(UserRepositoryInterface::class); - - $this->service = new UserUpdateService($this->hasher, $this->repository); + $this->revocationService = m::mock(RevokeMultipleDaemonKeysService::class); } /** @@ -50,9 +45,14 @@ class UserUpdateServiceTest extends TestCase */ public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed() { - $this->repository->shouldReceive('update')->with(1, ['test-data' => 'value'])->once()->andReturnNull(); + $user = factory(User::class)->make(); + $this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]); + $this->repository->shouldReceive('update')->with($user->id, ['test-data' => 'value'])->once()->andReturnNull(); - $this->assertNull($this->service->handle(1, ['test-data' => 'value'])); + $response = $this->getService()->handle($user, ['test-data' => 'value']); + $this->assertInstanceOf(Collection::class, $response); + $this->assertTrue($response->has('model')); + $this->assertTrue($response->has('exceptions')); } /** @@ -60,9 +60,61 @@ class UserUpdateServiceTest extends TestCase */ public function testUpdateUserAndHashPasswordIfProvided() { + $user = factory(User::class)->make(); $this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass'); - $this->repository->shouldReceive('update')->with(1, ['password' => 'enc_pass'])->once()->andReturnNull(); + $this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]); + $this->repository->shouldReceive('update')->with($user->id, ['password' => 'enc_pass'])->once()->andReturnNull(); - $this->assertNull($this->service->handle(1, ['password' => 'raw_pass'])); + $response = $this->getService()->handle($user, ['password' => 'raw_pass']); + $this->assertInstanceOf(Collection::class, $response); + $this->assertTrue($response->has('model')); + $this->assertTrue($response->has('exceptions')); + } + + /** + * Test that an admin can revoke a user's administrative status. + */ + public function testAdministrativeUserRevokingAdminStatus() + { + $user = factory(User::class)->make(['root_admin' => true]); + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_ADMIN); + + $this->revocationService->shouldReceive('handle')->with($user, false)->once()->andReturnNull(); + $this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]); + $this->repository->shouldReceive('update')->with($user->id, ['root_admin' => false])->once()->andReturnNull(); + + $response = $service->handle($user, ['root_admin' => false]); + $this->assertInstanceOf(Collection::class, $response); + $this->assertTrue($response->has('model')); + $this->assertTrue($response->has('exceptions')); + } + + /** + * Test that a normal user is unable to set an administrative status for themselves. + */ + public function testNormalUserShouldNotRevokeAdminStatus() + { + $user = factory(User::class)->make(['root_admin' => false]); + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_USER); + + $this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]); + $this->repository->shouldReceive('update')->with($user->id, [])->once()->andReturnNull(); + + $response = $service->handle($user, ['root_admin' => true]); + $this->assertInstanceOf(Collection::class, $response); + $this->assertTrue($response->has('model')); + $this->assertTrue($response->has('exceptions')); + } + + /** + * Return an instance of the service for testing. + * + * @return \Pterodactyl\Services\Users\UserUpdateService + */ + private function getService(): UserUpdateService + { + return new UserUpdateService($this->hasher, $this->revocationService, $this->repository); } } From df7a8579291088e6acb6f6504114f36c8fad7964 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 3 Dec 2017 14:06:27 -0600 Subject: [PATCH 3/9] Add star next to names of root admin accounts --- CHANGELOG.md | 3 +++ resources/themes/pterodactyl/admin/users/index.blade.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 807185eb3..8e193cb4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Changed * Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. +### Added +* Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. + ## v0.7.0-beta.2 (Derelict Dermodactylus) ### Fixed * `[beta.1]` — Fixes a CORS header issue due to a wrong API endpoint being provided in the administrative node listing. diff --git a/resources/themes/pterodactyl/admin/users/index.blade.php b/resources/themes/pterodactyl/admin/users/index.blade.php index 0c4289f32..dd95f222b 100644 --- a/resources/themes/pterodactyl/admin/users/index.blade.php +++ b/resources/themes/pterodactyl/admin/users/index.blade.php @@ -53,7 +53,7 @@ @foreach ($users as $user) {{ $user->id }} - {{ $user->email }} + {{ $user->email }} @if($user->root_admin)@endif {{ $user->name_last }}, {{ $user->name_first }} {{ $user->username }} From 285485d7b02c405ff4900bdabc00be06c48ce1c9 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 3 Dec 2017 14:29:14 -0600 Subject: [PATCH 4/9] Change how API keys are validated (#771) --- CHANGELOG.md | 3 + .../Repository/ApiKeyRepositoryInterface.php | 10 + app/Http/Controllers/Base/APIController.php | 27 +-- app/Http/Kernel.php | 18 +- .../Middleware/API/AuthenticateIPAccess.php | 40 ++++ app/Http/Middleware/API/AuthenticateKey.php | 68 ++++++ .../API/HasPermissionToResource.php | 58 +++++ app/Http/Middleware/API/SetSessionDriver.php | 52 +++++ app/Http/Middleware/HMACAuthorization.php | 209 ------------------ app/Models/APIKey.php | 15 +- app/Providers/RouteServiceProvider.php | 14 +- .../Eloquent/ApiKeyRepository.php | 16 ++ app/Services/Api/KeyCreationService.php | 43 +--- database/factories/ModelFactory.php | 19 ++ ...122708_MigratePubPrivFormatToSingleKey.php | 59 +++++ resources/lang/en/base.php | 2 +- resources/lang/en/strings.php | 2 +- .../pterodactyl/base/api/index.blade.php | 9 +- .../Controllers/Base/APIControllerTest.php | 75 +++---- .../API/AuthenticateIPAccessTest.php | 82 +++++++ .../Middleware/API/AuthenticateKeyTest.php | 90 ++++++++ .../API/HasPermissionToResourceTest.php | 109 +++++++++ .../Middleware/API/SetSessionDriverTest.php | 69 ++++++ .../Services/Api/KeyCreationServiceTest.php | 68 +++--- 24 files changed, 774 insertions(+), 383 deletions(-) create mode 100644 app/Http/Middleware/API/AuthenticateIPAccess.php create mode 100644 app/Http/Middleware/API/AuthenticateKey.php create mode 100644 app/Http/Middleware/API/HasPermissionToResource.php create mode 100644 app/Http/Middleware/API/SetSessionDriver.php delete mode 100644 app/Http/Middleware/HMACAuthorization.php create mode 100644 database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php create mode 100644 tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php create mode 100644 tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php create mode 100644 tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php create mode 100644 tests/Unit/Http/Middleware/API/SetSessionDriverTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e193cb4b..4b3680121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Added * Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. +### Changed +* API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. + ## v0.7.0-beta.2 (Derelict Dermodactylus) ### Fixed * `[beta.1]` — Fixes a CORS header issue due to a wrong API endpoint being provided in the administrative node listing. diff --git a/app/Contracts/Repository/ApiKeyRepositoryInterface.php b/app/Contracts/Repository/ApiKeyRepositoryInterface.php index 5b8f638ef..2fce09cd2 100644 --- a/app/Contracts/Repository/ApiKeyRepositoryInterface.php +++ b/app/Contracts/Repository/ApiKeyRepositoryInterface.php @@ -9,6 +9,16 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\APIKey; + interface ApiKeyRepositoryInterface extends RepositoryInterface { + /** + * Load permissions for a key onto the model. + * + * @param \Pterodactyl\Models\APIKey $model + * @param bool $refresh + * @return \Pterodactyl\Models\APIKey + */ + public function loadPermissions(APIKey $model, bool $refresh = false): APIKey; } diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 4bf89012f..c73661777 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -1,27 +1,4 @@ - * Some Modifications (c) 2015 Dylan Seidt . - * - * 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\Http\Controllers\Base; @@ -120,7 +97,7 @@ class APIController extends Controller 'memo' => $request->input('memo'), ], $request->input('permissions', []), $adminPermissions); - $this->alert->success(trans('base.api.index.keypair_created', ['token' => $secret]))->flash(); + $this->alert->success(trans('base.api.index.keypair_created'))->flash(); return redirect()->route('account.api'); } @@ -136,7 +113,7 @@ class APIController extends Controller { $this->repository->deleteWhere([ ['user_id', '=', $request->user()->id], - ['public', '=', $key], + ['token', '=', $key], ]); return response('', 204); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 79c6e9ecf..13a7b29c0 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -11,17 +11,20 @@ use Pterodactyl\Http\Middleware\EncryptCookies; use Pterodactyl\Http\Middleware\VerifyCsrfToken; use Pterodactyl\Http\Middleware\VerifyReCaptcha; use Pterodactyl\Http\Middleware\AdminAuthenticate; -use Pterodactyl\Http\Middleware\HMACAuthorization; use Illuminate\Routing\Middleware\ThrottleRequests; use Pterodactyl\Http\Middleware\LanguageMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Pterodactyl\Http\Middleware\API\AuthenticateKey; use Illuminate\Routing\Middleware\SubstituteBindings; use Pterodactyl\Http\Middleware\AccessingValidServer; +use Pterodactyl\Http\Middleware\API\SetSessionDriver; use Illuminate\View\Middleware\ShareErrorsFromSession; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; +use Pterodactyl\Http\Middleware\API\HasPermissionToResource; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; @@ -42,10 +45,6 @@ class Kernel extends HttpKernel EncryptCookies::class, AddQueuedCookiesToResponse::class, TrimStrings::class, - - /* - * Custom middleware applied to all routes. - */ TrustProxies::class, ]; @@ -66,9 +65,11 @@ class Kernel extends HttpKernel RequireTwoFactorAuthentication::class, ], 'api' => [ - HMACAuthorization::class, 'throttle:60,1', - 'bindings', + SubstituteBindings::class, + SetSessionDriver::class, + AuthenticateKey::class, + AuthenticateIPAccess::class, ], 'daemon' => [ SubstituteBindings::class, @@ -95,6 +96,9 @@ class Kernel extends HttpKernel 'bindings' => SubstituteBindings::class, 'recaptcha' => VerifyReCaptcha::class, + // API specific middleware. + 'api..user_level' => HasPermissionToResource::class, + // Server specific middleware (used for authenticating access to resources) // // These are only used for individual server authentication, and not gloabl diff --git a/app/Http/Middleware/API/AuthenticateIPAccess.php b/app/Http/Middleware/API/AuthenticateIPAccess.php new file mode 100644 index 000000000..aa0af7e2e --- /dev/null +++ b/app/Http/Middleware/API/AuthenticateIPAccess.php @@ -0,0 +1,40 @@ +attributes->get('api_key'); + + if (is_null($model->allowed_ips) || empty($model->allowed_ips)) { + return $next($request); + } + + $find = new IP($request->ip()); + foreach ($model->allowed_ips as $ip) { + if (Range::parse($ip)->contains($find)) { + return $next($request); + } + } + + throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); + } +} diff --git a/app/Http/Middleware/API/AuthenticateKey.php b/app/Http/Middleware/API/AuthenticateKey.php new file mode 100644 index 000000000..eb82682cc --- /dev/null +++ b/app/Http/Middleware/API/AuthenticateKey.php @@ -0,0 +1,68 @@ +auth = $auth; + $this->repository = $repository; + } + + /** + * Handle an API request by verifying that the provided API key + * is in a valid format, and the route being accessed is allowed + * for the given key. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function handle(Request $request, Closure $next) + { + if (is_null($request->bearerToken())) { + throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); + } + + try { + $model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]); + } catch (RecordNotFoundException $exception) { + throw new AccessDeniedHttpException; + } + + $this->auth->guard()->loginUsingId($model->user_id); + $request->attributes->set('api_key', $model); + + return $next($request); + } +} diff --git a/app/Http/Middleware/API/HasPermissionToResource.php b/app/Http/Middleware/API/HasPermissionToResource.php new file mode 100644 index 000000000..1d99ffbf7 --- /dev/null +++ b/app/Http/Middleware/API/HasPermissionToResource.php @@ -0,0 +1,58 @@ +repository = $repository; + } + + /** + * Determine if an API key has permission to access the given route. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string $role + * @return mixed + */ + public function handle(Request $request, Closure $next, string $role = 'admin') + { + /** @var \Pterodactyl\Models\APIKey $model */ + $model = $request->attributes->get('api_key'); + + if ($role === 'admin' && ! $request->user()->root_admin) { + throw new NotFoundHttpException; + } + + $this->repository->loadPermissions($model); + $routeKey = str_replace(['api.', 'admin.'], '', $request->route()->getName()); + + $count = $model->getRelation('permissions')->filter(function ($permission) use ($routeKey) { + return $routeKey === str_replace('-', '.', $permission->permission); + })->count(); + + if ($count === 1) { + return $next($request); + } + + throw new AccessDeniedHttpException('Cannot access resource without required `' . $routeKey . '` permission.'); + } +} diff --git a/app/Http/Middleware/API/SetSessionDriver.php b/app/Http/Middleware/API/SetSessionDriver.php new file mode 100644 index 000000000..9cc5d60e3 --- /dev/null +++ b/app/Http/Middleware/API/SetSessionDriver.php @@ -0,0 +1,52 @@ +app = $app; + $this->config = $config; + } + + /** + * Set the session for API calls to only last for the one request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + if ($this->app->environment() !== 'production') { + $this->app->make(LaravelDebugbar::class)->disable(); + } + + $this->config->set('session.driver', 'array'); + + return $next($request); + } +} diff --git a/app/Http/Middleware/HMACAuthorization.php b/app/Http/Middleware/HMACAuthorization.php deleted file mode 100644 index fa048b59f..000000000 --- a/app/Http/Middleware/HMACAuthorization.php +++ /dev/null @@ -1,209 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Middleware; - -use Auth; -use Crypt; -use Config; -use Closure; -use Debugbar; -use IPTools\IP; -use IPTools\Range; -use Illuminate\Http\Request; -use Pterodactyl\Models\APIKey; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; // 400 -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // 403 - -class HMACAuthorization -{ - /** - * The algorithm to use for handling HMAC encryption. - * - * @var string - */ - const HMAC_ALGORITHM = 'sha256'; - - /** - * Stored values from the Authorization header. - * - * @var array - */ - protected $token = []; - - /** - * The eloquent model for the API key. - * - * @var \Pterodactyl\Models\APIKey - */ - protected $key; - - /** - * The illuminate request object. - * - * @var \Illuminate\Http\Request - */ - private $request; - - /** - * Construct class instance. - */ - public function __construct() - { - Debugbar::disable(); - Config::set('session.driver', 'array'); - } - - /** - * Handle an incoming request for the API. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - */ - public function handle(Request $request, Closure $next) - { - $this->request = $request; - - $this->checkBearer(); - $this->validateRequest(); - $this->validateIPAccess(); - $this->validateContents(); - - Auth::loginUsingId($this->key()->user_id); - - return $next($request); - } - - /** - * Checks that the Bearer token is provided and in a valid format. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function checkBearer() - { - if (empty($this->request()->bearerToken())) { - throw new BadRequestHttpException('Request was missing required Authorization header.'); - } - - $token = explode('.', $this->request()->bearerToken()); - if (count($token) !== 2) { - throw new BadRequestHttpException('The Authorization header passed was not in a validate public/private key format.'); - } - - $this->token = [ - 'public' => $token[0], - 'hash' => $token[1], - ]; - } - - /** - * Determine if the request contains a valid public API key - * as well as permissions for the resource. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function validateRequest() - { - $this->key = APIKey::where('public', $this->public())->first(); - if (! $this->key) { - throw new AccessDeniedHttpException('Unable to identify requester. Authorization token is invalid.'); - } - } - - /** - * Determine if the requesting IP address is allowed to use this API key. - * - * @return bool - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function validateIPAccess() - { - if (! is_null($this->key()->allowed_ips)) { - foreach (json_decode($this->key()->allowed_ips) as $ip) { - if (Range::parse($ip)->contains(new IP($this->request()->ip()))) { - return true; - } - } - - throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); - } - - return true; - } - - /** - * Determine if the HMAC sent in the request matches the one generated - * on the panel side. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - */ - protected function validateContents() - { - if (! hash_equals(base64_decode($this->hash()), $this->generateSignature())) { - throw new BadRequestHttpException('The HMAC for the request was invalid.'); - } - } - - /** - * Generate a HMAC from the request and known API secret key. - * - * @return string - */ - protected function generateSignature() - { - $content = urldecode($this->request()->fullUrl()) . $this->request()->getContent(); - - return hash_hmac(self::HMAC_ALGORITHM, $content, Crypt::decrypt($this->key()->secret), true); - } - - /** - * Return the public key passed in the Authorization header. - * - * @return string - */ - protected function public() - { - return $this->token['public']; - } - - /** - * Return the base64'd HMAC sent in the Authorization header. - * - * @return string - */ - protected function hash() - { - return $this->token['hash']; - } - - /** - * Return the API Key model. - * - * @return \Pterodactyl\Models\APIKey - */ - protected function key() - { - return $this->key; - } - - /** - * Return the Illuminate Request object. - * - * @return \Illuminate\Http\Request - */ - private function request() - { - return $this->request; - } -} diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index cbfc996d7..0542fa022 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -19,6 +19,8 @@ class APIKey extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + const KEY_LENGTH = 32; + /** * The table associated with the model. * @@ -26,13 +28,6 @@ class APIKey extends Model implements CleansAttributes, ValidableContract */ protected $table = 'api_keys'; - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = ['secret']; - /** * Cast values to correct type. * @@ -57,8 +52,7 @@ class APIKey extends Model implements CleansAttributes, ValidableContract protected static $applicationRules = [ 'memo' => 'required', 'user_id' => 'required', - 'secret' => 'required', - 'public' => 'required', + 'token' => 'required', ]; /** @@ -68,8 +62,7 @@ class APIKey extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'user_id' => 'exists:users,id', - 'public' => 'string|size:16', - 'secret' => 'string', + 'token' => 'string|size:32', 'memo' => 'nullable|string|max:500', 'allowed_ips' => 'nullable|json', 'expires_at' => 'nullable|datetime', diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 57ae43fad..a0f902859 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -29,13 +29,9 @@ class RouteServiceProvider extends ServiceProvider */ public function map() { - Route::middleware(['api'])->prefix('/api/user') - ->namespace($this->namespace . '\API\User') - ->group(base_path('routes/api.php')); - - Route::middleware(['api'])->prefix('/api/admin') - ->namespace($this->namespace . '\API\Admin') - ->group(base_path('routes/api-admin.php')); +// Route::middleware(['api'])->prefix('/api/user') +// ->namespace($this->namespace . '\API\User') +// ->group(base_path('routes/api.php')); Route::middleware(['web', 'auth', 'csrf']) ->namespace($this->namespace . '\Base') @@ -53,6 +49,10 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); + Route::middleware(['api', 'api..user_level:admin'])->prefix('/api/admin') + ->namespace($this->namespace . '\API\Admin') + ->group(base_path('routes/api-admin.php')); + Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\API\Remote') ->group(base_path('routes/api-remote.php')); diff --git a/app/Repositories/Eloquent/ApiKeyRepository.php b/app/Repositories/Eloquent/ApiKeyRepository.php index facac39c8..107e0b6c9 100644 --- a/app/Repositories/Eloquent/ApiKeyRepository.php +++ b/app/Repositories/Eloquent/ApiKeyRepository.php @@ -21,4 +21,20 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt { return APIKey::class; } + + /** + * Load permissions for a key onto the model. + * + * @param \Pterodactyl\Models\APIKey $model + * @param bool $refresh + * @return \Pterodactyl\Models\APIKey + */ + public function loadPermissions(APIKey $model, bool $refresh = false): APIKey + { + if (! $model->relationLoaded('permissions') || $refresh) { + $model->load('permissions'); + } + + return $model; + } } diff --git a/app/Services/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 1a4312542..891a32438 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -1,60 +1,42 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Api; +use Pterodactyl\Models\APIKey; use Illuminate\Database\ConnectionInterface; -use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyCreationService { - const PUB_CRYPTO_LENGTH = 16; - const PRIV_CRYPTO_LENGTH = 64; - /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; + private $connection; /** * @var \Pterodactyl\Services\Api\PermissionService */ - protected $permissionService; + private $permissionService; /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ - protected $repository; + private $repository; /** * ApiKeyService constructor. * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Services\Api\PermissionService $permissionService */ public function __construct( ApiKeyRepositoryInterface $repository, ConnectionInterface $connection, - Encrypter $encrypter, PermissionService $permissionService ) { $this->repository = $repository; $this->connection = $connection; - $this->encrypter = $encrypter; $this->permissionService = $permissionService; } @@ -64,24 +46,17 @@ class KeyCreationService * @param array $data * @param array $permissions * @param array $administrative - * @return string + * @return \Pterodactyl\Models\APIKey * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function handle(array $data, array $permissions, array $administrative = []) + public function handle(array $data, array $permissions, array $administrative = []): APIKey { - $publicKey = str_random(self::PUB_CRYPTO_LENGTH); - $secretKey = str_random(self::PRIV_CRYPTO_LENGTH); + $token = str_random(APIKey::KEY_LENGTH); + $data = array_merge($data, ['token' => $token]); - // Start a Transaction $this->connection->beginTransaction(); - - $data = array_merge($data, [ - 'public' => $publicKey, - 'secret' => $this->encrypter->encrypt($secretKey), - ]); - $instance = $this->repository->create($data, true, true); $nodes = $this->permissionService->getPermissions(); @@ -115,6 +90,6 @@ class KeyCreationService $this->connection->commit(); - return $secretKey; + return $instance; } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 1df550e0e..e117b59bb 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -221,3 +221,22 @@ $factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker\Generator 'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(), ]; }); + +$factory->define(Pterodactyl\Models\APIKey::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'user_id' => $faker->randomNumber(), + 'token' => str_random(Pterodactyl\Models\APIKey::KEY_LENGTH), + 'memo' => 'Test Function Key', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), + ]; +}); + +$factory->define(Pterodactyl\Models\APIPermission::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'key_id' => $faker->randomNumber(), + 'permission' => mb_strtolower($faker->word), + ]; +}); diff --git a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php new file mode 100644 index 000000000..c2947ee07 --- /dev/null +++ b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php @@ -0,0 +1,59 @@ +get()->each(function ($item) { + try { + $decrypted = Crypt::decrypt($item->secret); + } catch (DecryptException $exception) { + $decrypted = str_random(32); + } finally { + DB::table('api_keys')->where('id', $item->id)->update([ + 'secret' => $decrypted, + ]); + } + }); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('public'); + $table->string('secret', 32)->change(); + }); + + DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))'); + } + + /** + * Reverse the migrations. + */ + public function down() + { + DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`'); + + Schema::table('api_keys', function (Blueprint $table) { + $table->char('public', 16)->after('user_id'); + }); + + DB::transaction(function () { + DB::table('api_keys')->get()->each(function ($item) { + DB::table('api_keys')->where('id', $item->id)->update([ + 'public' => str_random(16), + 'secret' => Crypt::encrypt($item->secret), + ]); + }); + }); + } +} diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 9c7bd9d0d..579ac3410 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -33,7 +33,7 @@ return [ 'header_sub' => 'Manage your API access keys.', 'list' => 'API Keys', 'create_new' => 'Create New API key', - 'keypair_created' => 'An API Key-Pair has been generated. Your API secret token is :token. Please take note of this key as it will not be displayed again.', + 'keypair_created' => 'An API key has been successfully generated and is listed below.', ], 'new' => [ 'header' => 'New API Key', diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index d98a7d01a..c9983e643 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -32,7 +32,7 @@ return [ 'memo' => 'Memo', 'created' => 'Created', 'expires' => 'Expires', - 'public_key' => 'Public key', + 'public_key' => 'Token', 'api_access' => 'Api Access', 'never' => 'never', 'sign_out' => 'Sign out', diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index a07657c96..0d8fc0a3e 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -20,12 +20,7 @@ @section('content')
-
- API functionality is disabled in this beta release. -
-
-

@lang('base.api.index.list')

@@ -44,7 +39,7 @@ @foreach ($keys as $key) - {{ $key->public }} + {{ $key->token }} {{ $key->memo }} {{ (new Carbon($key->created_at))->toDayDateTimeString() }} @@ -57,7 +52,7 @@ @endif - + @endforeach diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/APIControllerTest.php index 055cbd84f..579fb43bb 100644 --- a/tests/Unit/Http/Controllers/Base/APIControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/APIControllerTest.php @@ -1,54 +1,34 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; -use Tests\TestCase; -use Illuminate\Http\Request; use Pterodactyl\Models\User; +use Pterodactyl\Models\APIKey; use Prologue\Alerts\AlertsMessageBag; -use Tests\Assertions\ControllerAssertionsTrait; use Pterodactyl\Services\Api\KeyCreationService; +use Tests\Unit\Http\Controllers\ControllerTestCase; use Pterodactyl\Http\Controllers\Base\APIController; use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class APIControllerTest extends TestCase +class APIControllerTest extends ControllerTestCase { - use ControllerAssertionsTrait; - /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ protected $alert; /** - * @var \Pterodactyl\Http\Controllers\Base\APIController - */ - protected $controller; - - /** - * @var \Pterodactyl\Services\Api\KeyCreationService + * @var \Pterodactyl\Services\Api\KeyCreationService|\Mockery\Mock */ protected $keyService; /** - * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock */ protected $repository; - /** - * @var \Illuminate\Http\Request - */ - protected $request; - /** * Setup tests. */ @@ -59,9 +39,6 @@ class APIControllerTest extends TestCase $this->alert = m::mock(AlertsMessageBag::class); $this->keyService = m::mock(KeyCreationService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - $this->request = m::mock(Request::class); - - $this->controller = new APIController($this->alert, $this->repository, $this->keyService); } /** @@ -69,12 +46,11 @@ class APIControllerTest extends TestCase */ public function testIndexController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('findWhere')->with([['user_id', '=', $model->id]])->once()->andReturn(['testkeys']); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.index', $response); $this->assertViewHasKey('keys', $response); @@ -88,10 +64,9 @@ class APIControllerTest extends TestCase */ public function testCreateController($admin) { - $model = factory(User::class)->make(['root_admin' => $admin]); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => $admin])); - $response = $this->controller->create($this->request); + $response = $this->getController()->create($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.new', $response); $this->assertViewHasKey('permissions.user', $response); @@ -111,8 +86,9 @@ class APIControllerTest extends TestCase */ public function testStoreController($admin) { - $this->request = m::mock(ApiKeyFormRequest::class); - $model = factory(User::class)->make(['root_admin' => $admin]); + $this->setRequestMockClass(ApiKeyFormRequest::class); + $model = $this->setRequestUser(factory(User::class)->make(['root_admin' => $admin])); + $keyModel = factory(APIKey::class)->make(); if ($admin) { $this->request->shouldReceive('input')->with('admin_permissions', [])->once()->andReturn(['admin.permission']); @@ -127,12 +103,12 @@ class APIControllerTest extends TestCase 'user_id' => $model->id, 'allowed_ips' => null, 'memo' => null, - ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn('testToken'); + ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn($keyModel); - $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created', ['token' => 'testToken']))->once()->andReturnSelf() - ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created'))->once()->andReturnSelf(); + $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); - $response = $this->controller->store($this->request); + $response = $this->getController()->store($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.api', $response); } @@ -142,15 +118,14 @@ class APIControllerTest extends TestCase */ public function testRevokeController() { - $model = factory(User::class)->make(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $model = $this->setRequestUser(); $this->repository->shouldReceive('deleteWhere')->with([ ['user_id', '=', $model->id], - ['public', '=', 'testKey123'], + ['token', '=', 'testKey123'], ])->once()->andReturnNull(); - $response = $this->controller->revoke($this->request, 'testKey123'); + $response = $this->getController()->revoke($this->request, 'testKey123'); $this->assertIsResponse($response); $this->assertEmpty($response->getContent()); $this->assertResponseCodeEquals(204, $response); @@ -165,4 +140,14 @@ class APIControllerTest extends TestCase { return [[0], [1]]; } + + /** + * Return an instance of the controller with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Controllers\Base\APIController + */ + private function getController(): APIController + { + return new APIController($this->alert, $this->repository, $this->keyService); + } } diff --git a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php new file mode 100644 index 000000000..cd122f7cb --- /dev/null +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -0,0 +1,82 @@ +make(['allowed_ips' => []]); + $this->setRequestAttribute('api_key', $model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test middleware works correctly when a valid IP accesses + * and there is an IP restriction. + */ + public function testWithValidIP() + { + $model = factory(APIKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $this->setRequestAttribute('api_key', $model); + + $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.1'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a CIDR range can be used. + */ + public function testValidIPAganistCIDRRange() + { + $model = factory(APIKey::class)->make(['allowed_ips' => ['192.168.1.1/28']]); + $this->setRequestAttribute('api_key', $model); + + $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('192.168.1.15'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an exception is thrown when an invalid IP address + * tries to connect and there is an IP restriction. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testWithInvalidIP() + { + $model = factory(APIKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $this->setRequestAttribute('api_key', $model); + + $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.2'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Return an instance of the middleware to be used when testing. + * + * @return \Pterodactyl\Http\Middleware\API\AuthenticateIPAccess + */ + private function getMiddleware(): AuthenticateIPAccess + { + return new AuthenticateIPAccess(); + } +} diff --git a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php new file mode 100644 index 000000000..e2032b588 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php @@ -0,0 +1,90 @@ +auth = m::mock(AuthManager::class); + $this->repository = m::mock(ApiKeyRepositoryInterface::class); + } + + /** + * Test that a missing bearer token will throw an exception. + */ + public function testMissingBearerTokenThrowsException() + { + $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } catch (HttpException $exception) { + $this->assertEquals(401, $exception->getStatusCode()); + $this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders()); + } + } + + /** + * Test that an invalid API token throws an exception. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testInvalidTokenThrowsException() + { + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); + $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a valid token can continue past the middleware. + */ + public function testValidToken() + { + $model = factory(APIKey::class)->make(); + + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->token); + $this->repository->shouldReceive('findFirstWhere')->with([['token', '=', $model->token]])->once()->andReturn($model); + + $this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->assertEquals($model, $this->request->attributes->get('api_key')); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\AuthenticateKey + */ + private function getMiddleware(): AuthenticateKey + { + return new AuthenticateKey($this->repository, $this->auth); + } +} diff --git a/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php b/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php new file mode 100644 index 000000000..7ef6c3830 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php @@ -0,0 +1,109 @@ +repository = m::mock(ApiKeyRepositoryInterface::class); + } + + /** + * Test that a non-admin user cannot access admin level routes. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function testNonAdminAccessingAdminLevel() + { + $model = factory(APIKey::class)->make(); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test non-admin accessing non-admin route. + */ + public function testAccessingAllowedRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'user.test-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user'); + } + + /** + * Test admin accessing administrative route. + */ + public function testAccessingAllowedAdminRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'test-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => true])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.admin.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test a user accessing a disallowed route. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testAccessingDisallowedRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'user.other-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user'); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\HasPermissionToResource + */ + private function getMiddleware(): HasPermissionToResource + { + return new HasPermissionToResource($this->repository); + } +} diff --git a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php new file mode 100644 index 000000000..dd9fa3e04 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php @@ -0,0 +1,69 @@ +appMock = m::mock(Application::class); + $this->config = m::mock(Repository::class); + } + + /** + * Test that a production environment does not try to disable debug bar. + */ + public function testProductionEnvironment() + { + $this->appMock->shouldReceive('environment')->withNoArgs()->once()->andReturn('production'); + $this->config->shouldReceive('set')->with('session.driver', 'array')->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a local environment does disable debug bar. + */ + public function testLocalEnvironment() + { + $this->appMock->shouldReceive('environment')->withNoArgs()->once()->andReturn('local'); + $this->appMock->shouldReceive('make')->with(LaravelDebugbar::class)->once()->andReturnSelf(); + $this->appMock->shouldReceive('disable')->withNoArgs()->once()->andReturnNull(); + + $this->config->shouldReceive('set')->with('session.driver', 'array')->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\SetSessionDriver + */ + private function getMiddleware(): SetSessionDriver + { + return new SetSessionDriver($this->appMock, $this->config); + } +} diff --git a/tests/Unit/Services/Api/KeyCreationServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php index 159d64255..49e0a2fde 100644 --- a/tests/Unit/Services/Api/KeyCreationServiceTest.php +++ b/tests/Unit/Services/Api/KeyCreationServiceTest.php @@ -12,8 +12,8 @@ namespace Tests\Unit\Services\Api; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\APIKey; use Illuminate\Database\ConnectionInterface; -use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Api\PermissionService; use Pterodactyl\Services\Api\KeyCreationService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; @@ -23,45 +23,30 @@ class KeyCreationServiceTest extends TestCase use PHPMock; /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** - * @var \Illuminate\Contracts\Encryption\Encrypter + * @var \Pterodactyl\Services\Api\PermissionService|\Mockery\Mock */ - protected $encrypter; + private $permissionService; /** - * @var \Pterodactyl\Services\Api\PermissionService + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock */ - protected $permissions; + private $repository; /** - * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + * Setup tests. */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Api\KeyCreationService - */ - protected $service; - public function setUp() { parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->encrypter = m::mock(Encrypter::class); - $this->permissions = m::mock(PermissionService::class); + $this->permissionService = m::mock(PermissionService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - - $this->service = new KeyCreationService( - $this->repository, - $this->connection, - $this->encrypter, - $this->permissions - ); } /** @@ -69,37 +54,48 @@ class KeyCreationServiceTest extends TestCase */ public function testKeyIsCreated() { + $model = factory(APIKey::class)->make(); + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturn('random_string'); + ->expects($this->exactly(1))->with(APIKey::KEY_LENGTH)->willReturn($model->token); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with('random_string')->once()->andReturn('encrypted-secret'); $this->repository->shouldReceive('create')->with([ 'test-data' => 'test', - 'public' => 'random_string', - 'secret' => 'encrypted-secret', - ], true, true)->once()->andReturn((object) ['id' => 1]); + 'token' => $model->token, + ], true, true)->once()->andReturn($model); - $this->permissions->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ + $this->permissionService->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ '_user' => ['server' => ['list', 'multiple-dash-test']], 'server' => ['create', 'admin-dash-test'], ]); - $this->permissions->shouldReceive('create')->with(1, 'user.server-list')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'user.server-multiple-dash-test')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'server-create')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'server-admin-dash-test')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'user.server-list')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'user.server-multiple-dash-test')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'server-create')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'server-admin-dash-test')->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->handle( + $response = $this->getService()->handle( ['test-data' => 'test'], ['invalid-node', 'server-list', 'server-multiple-dash-test'], ['invalid-node', 'server-create', 'server-admin-dash-test'] ); $this->assertNotEmpty($response); - $this->assertEquals('random_string', $response); + $this->assertInstanceOf(APIKey::class, $response); + $this->assertSame($model, $response); + } + + /** + * Return an instance of the service with mocked dependencies for testing. + * + * @return \Pterodactyl\Services\Api\KeyCreationService + */ + private function getService(): KeyCreationService + { + return new KeyCreationService($this->repository, $this->connection, $this->permissionService); } } From 3f6d782ce1c3f7d1516cde0057123e23a3314398 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 4 Dec 2017 18:43:19 -0600 Subject: [PATCH 5/9] Fix forgotten migration that caused node deletions to not be cascaded to all allocations. closes #795 --- CHANGELOG.md | 1 + ...84012_DropAllocationsWhenNodeIsDeleted.php | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 database/migrations/2017_12_04_184012_DropAllocationsWhenNodeIsDeleted.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e193cb4b..41f5d4358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.2]` — Someone found a `@todo` that I never `@todid` and thus database hosts could not be created without being linked to a node. This is fixed... * `[beta.2]` — Fixes bug that caused incorrect rendering of CPU usage on server graphs due to missing variable. * `[beta.2]` — Fixes bug causing schedules to be un-deletable. +* `[beta.2]` — Fixes bug that prevented the deletion of nodes due to an allocation deletion cascade issue with the SQL schema. ### Changed * Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. diff --git a/database/migrations/2017_12_04_184012_DropAllocationsWhenNodeIsDeleted.php b/database/migrations/2017_12_04_184012_DropAllocationsWhenNodeIsDeleted.php new file mode 100644 index 000000000..d28109598 --- /dev/null +++ b/database/migrations/2017_12_04_184012_DropAllocationsWhenNodeIsDeleted.php @@ -0,0 +1,32 @@ +dropForeign(['node_id']); + + $table->foreign('node_id')->references('id')->on('nodes')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropForeign(['node_id']); + + $table->foreign('node_id')->references('id')->on('nodes'); + }); + } +} From 4b7187a576eae3679b2a0fa37f8e7b6b8b209c92 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 4 Dec 2017 19:01:04 -0600 Subject: [PATCH 6/9] Fix broken password reset form --- resources/themes/pterodactyl/auth/passwords/reset.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/themes/pterodactyl/auth/passwords/reset.blade.php b/resources/themes/pterodactyl/auth/passwords/reset.blade.php index 51632fbf3..08968cd62 100644 --- a/resources/themes/pterodactyl/auth/passwords/reset.blade.php +++ b/resources/themes/pterodactyl/auth/passwords/reset.blade.php @@ -68,6 +68,7 @@
{!! csrf_field() !!} +
From e085b8e109f27a8dc1eb36cb89b75bd5fac94100 Mon Sep 17 00:00:00 2001 From: Anand Capur Date: Tue, 5 Dec 2017 07:26:29 -0800 Subject: [PATCH 7/9] enable php 7.2 in travis and fix repository (#797) This was really amusing to watch @arcdigital attempt to do. --- .travis.yml | 10 +++++++--- app/Repositories/Eloquent/LocationRepository.php | 5 ----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 277c25df2..d07b2cbfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ language: php dist: trusty php: - - '7.0' - - '7.1' -# - '7.2' + - 7.0 + - 7.1 + - 7.2 +matrix: + fast_finish: true + allow_failures: + - php: 7.2 sudo: false cache: directories: diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 04243b8a8..b893a0588 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -18,11 +18,6 @@ class LocationRepository extends EloquentRepository implements LocationRepositor { use Searchable; - /** - * @var string - */ - protected $searchTerm; - /** * {@inheritdoc} */ From 75eb506dab5180bb33821729de254a68b52a9a63 Mon Sep 17 00:00:00 2001 From: Franco Sanllehi Date: Thu, 14 Dec 2017 00:03:54 -0300 Subject: [PATCH 8/9] Solution for "SteamInternal_ContextInit" (#802) Add required package to apt install Solution for: error=/home/container/garrysmod/bin/server_srv.so: undefined symbol: SteamInternal_ContextInit --- database/seeds/eggs/source-engine/egg-garrys-mod.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/seeds/eggs/source-engine/egg-garrys-mod.json b/database/seeds/eggs/source-engine/egg-garrys-mod.json index a6334b76f..3b64be3ff 100644 --- a/database/seeds/eggs/source-engine/egg-garrys-mod.json +++ b/database/seeds/eggs/source-engine/egg-garrys-mod.json @@ -17,7 +17,7 @@ }, "scripts": { "installation": { - "script": "#!\/bin\/bash\n# Garry's Mod Installation Script\n#\n# Server Files: \/mnt\/server\napt -y update\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\n\ncd \/tmp\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\n\nmkdir -p \/mnt\/server\/steamcmd\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\ncd \/mnt\/server\/steamcmd\n\n# SteamCMD fails otherwise for some reason, even running as root.\n# This is changed at the end of the install process anyways.\nchown -R root:root \/mnt\n\nexport HOME=\/mnt\/server\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\n\nmkdir -p \/mnt\/server\/.steam\/sdk32\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so", + "script": "#!\/bin\/bash\n# Garry's Mod Installation Script\n#\n# Server Files: \/mnt\/server\napt -y update\napt -y --no-install-recommends install curl lib32gcc1 lib32stdc++6 ca-certificates\n\ncd \/tmp\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\n\nmkdir -p \/mnt\/server\/steamcmd\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\ncd \/mnt\/server\/steamcmd\n\n# SteamCMD fails otherwise for some reason, even running as root.\n# This is changed at the end of the install process anyways.\nchown -R root:root \/mnt\n\nexport HOME=\/mnt\/server\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\n\nmkdir -p \/mnt\/server\/.steam\/sdk32\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so", "container": "ubuntu:16.04", "entrypoint": "bash" } @@ -42,4 +42,4 @@ "rules": "required|string|alpha_num|size:32" } ] -} \ No newline at end of file +} From f9df463d323fef7e884d11f39cfb7c3c4760008f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 14 Dec 2017 21:05:26 -0600 Subject: [PATCH 9/9] Implement a better management interface for Settings (#809) --- .env.travis | 1 + .../SettingsRepositoryInterface.php | 32 +++++ app/Http/Controllers/Admin/BaseController.php | 62 +--------- .../Admin/Settings/AdvancedController.php | 91 ++++++++++++++ .../Admin/Settings/IndexController.php | 89 +++++++++++++ .../Admin/Settings/MailController.php | 112 +++++++++++++++++ .../RequireTwoFactorAuthentication.php | 18 +-- .../Settings/AdvancedSettingsFormRequest.php | 42 +++++++ .../Settings/BaseSettingsFormRequest.php | 36 ++++++ .../Settings/MailSettingsFormRequest.php | 44 +++++++ app/Models/Setting.php | 39 ++++++ app/Providers/AppServiceProvider.php | 5 - app/Providers/RepositoryServiceProvider.php | 5 + app/Providers/SettingsServiceProvider.php | 101 +++++++++++++++ .../Eloquent/SettingsRepository.php | 96 ++++++++++++++ app/Traits/Helpers/AvailableLanguages.php | 56 +++++++++ composer.json | 1 + composer.lock | 97 +++++++-------- config/app.php | 6 +- config/pterodactyl.php | 14 ++- config/recaptcha.php | 6 +- config/settings.php | 113 ----------------- ...220426_MigrateSettingsTableToNewFormat.php | 30 +++++ phpunit.xml | 1 + .../pterodactyl/js/frontend/server.socket.js | 3 + .../pterodactyl/admin/settings.blade.php | 69 ----------- .../admin/settings/advanced.blade.php | 117 ++++++++++++++++++ .../admin/settings/index.blade.php | 75 +++++++++++ .../pterodactyl/admin/settings/mail.blade.php | 108 ++++++++++++++++ .../pterodactyl/layouts/admin.blade.php | 4 +- .../themes/pterodactyl/layouts/auth.blade.php | 4 +- .../pterodactyl/layouts/error.blade.php | 4 +- .../pterodactyl/layouts/master.blade.php | 4 +- .../partials/admin/settings/nav.blade.php | 13 ++ .../pterodactyl/server/console.blade.php | 2 +- .../notifications/email-plain.blade.php | 2 +- .../vendor/notifications/email.blade.php | 6 +- routes/admin.php | 18 ++- tests/Traits/Http/RequestMockHelpers.php | 50 ++++++-- .../RequireTwoFactorAuthenticationTest.php | 81 ++++++------ 40 files changed, 1274 insertions(+), 383 deletions(-) create mode 100644 app/Contracts/Repository/SettingsRepositoryInterface.php create mode 100644 app/Http/Controllers/Admin/Settings/AdvancedController.php create mode 100644 app/Http/Controllers/Admin/Settings/IndexController.php create mode 100644 app/Http/Controllers/Admin/Settings/MailController.php create mode 100644 app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php create mode 100644 app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php create mode 100644 app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php create mode 100644 app/Models/Setting.php create mode 100644 app/Providers/SettingsServiceProvider.php create mode 100644 app/Repositories/Eloquent/SettingsRepository.php create mode 100644 app/Traits/Helpers/AvailableLanguages.php delete mode 100644 config/settings.php create mode 100644 database/migrations/2017_12_12_220426_MigrateSettingsTableToNewFormat.php delete mode 100644 resources/themes/pterodactyl/admin/settings.blade.php create mode 100644 resources/themes/pterodactyl/admin/settings/advanced.blade.php create mode 100644 resources/themes/pterodactyl/admin/settings/index.blade.php create mode 100644 resources/themes/pterodactyl/admin/settings/mail.blade.php create mode 100644 resources/themes/pterodactyl/partials/admin/settings/nav.blade.php diff --git a/.env.travis b/.env.travis index 22a0c3047..f1d4f5698 100644 --- a/.env.travis +++ b/.env.travis @@ -16,3 +16,4 @@ MAIL_DRIVER=array QUEUE_DRIVER=sync HASHIDS_SALT=test123 +APP_ENVIRONMENT_ONLY=true diff --git a/app/Contracts/Repository/SettingsRepositoryInterface.php b/app/Contracts/Repository/SettingsRepositoryInterface.php new file mode 100644 index 000000000..dbf87f744 --- /dev/null +++ b/app/Contracts/Repository/SettingsRepositoryInterface.php @@ -0,0 +1,32 @@ +. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Admin; -use Krucas\Settings\Settings; -use Prologue\Alerts\AlertsMessageBag; +use Illuminate\View\View; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Http\Requests\Admin\BaseFormRequest; use Pterodactyl\Services\Helpers\SoftwareVersionService; class BaseController extends Controller { - /** - * @var \Prologue\Alerts\AlertsMessageBag - */ - protected $alert; - - /** - * @var \Krucas\Settings\Settings - */ - protected $settings; - /** * @var \Pterodactyl\Services\Helpers\SoftwareVersionService */ - protected $version; + private $version; /** * BaseController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Krucas\Settings\Settings $settings * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version */ - public function __construct( - AlertsMessageBag $alert, - Settings $settings, - SoftwareVersionService $version - ) { - $this->alert = $alert; - $this->settings = $settings; + public function __construct(SoftwareVersionService $version) + { $this->version = $version; } @@ -54,34 +28,8 @@ class BaseController extends Controller * * @return \Illuminate\View\View */ - public function getIndex() + public function index(): View { return view('admin.index', ['version' => $this->version]); } - - /** - * Return the admin settings view. - * - * @return \Illuminate\View\View - */ - public function getSettings() - { - return view('admin.settings'); - } - - /** - * Handle settings post request. - * - * @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request - * @return \Illuminate\Http\RedirectResponse - */ - public function postSettings(BaseFormRequest $request) - { - $this->settings->set('company', $request->input('company')); - $this->settings->set('2fa', $request->input('2fa')); - - $this->alert->success('Settings have been successfully updated.')->flash(); - - return redirect()->route('admin.settings'); - } } diff --git a/app/Http/Controllers/Admin/Settings/AdvancedController.php b/app/Http/Controllers/Admin/Settings/AdvancedController.php new file mode 100644 index 000000000..f8249acd3 --- /dev/null +++ b/app/Http/Controllers/Admin/Settings/AdvancedController.php @@ -0,0 +1,91 @@ +alert = $alert; + $this->config = $config; + $this->kernel = $kernel; + $this->settings = $settings; + } + + /** + * Render advanced Panel settings UI. + * + * @return \Illuminate\View\View + */ + public function index(): View + { + $showRecaptchaWarning = false; + if ( + $this->config->get('recaptcha._shipped_secret_key') === $this->config->get('recaptcha.secret_key') + || $this->config->get('recaptcha._shipped_website_key') === $this->config->get('recaptcha.website_key') + ) { + $showRecaptchaWarning = true; + } + + return view('admin.settings.advanced', [ + 'showRecaptchaWarning' => $showRecaptchaWarning, + ]); + } + + /** + * @param \Pterodactyl\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function update(AdvancedSettingsFormRequest $request): RedirectResponse + { + foreach ($request->normalize() as $key => $value) { + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + $this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); + + return redirect()->route('admin.settings.advanced'); + } +} diff --git a/app/Http/Controllers/Admin/Settings/IndexController.php b/app/Http/Controllers/Admin/Settings/IndexController.php new file mode 100644 index 000000000..604684da4 --- /dev/null +++ b/app/Http/Controllers/Admin/Settings/IndexController.php @@ -0,0 +1,89 @@ +alert = $alert; + $this->kernel = $kernel; + $this->settings = $settings; + $this->versionService = $versionService; + } + + /** + * Render the UI for basic Panel settings. + * + * @return \Illuminate\View\View + */ + public function index(): View + { + return view('admin.settings.index', [ + 'version' => $this->versionService, + 'languages' => $this->getAvailableLanguages(true), + ]); + } + + /** + * Handle settings update. + * + * @param \Pterodactyl\Http\Requests\Admin\Settings\BaseSettingsFormRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function update(BaseSettingsFormRequest $request): RedirectResponse + { + foreach ($request->normalize() as $key => $value) { + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + $this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); + + return redirect()->route('admin.settings'); + } +} diff --git a/app/Http/Controllers/Admin/Settings/MailController.php b/app/Http/Controllers/Admin/Settings/MailController.php new file mode 100644 index 000000000..0e5a1d737 --- /dev/null +++ b/app/Http/Controllers/Admin/Settings/MailController.php @@ -0,0 +1,112 @@ +alert = $alert; + $this->config = $config; + $this->encrypter = $encrypter; + $this->kernel = $kernel; + $this->settings = $settings; + } + + /** + * Render UI for editing mail settings. This UI should only display if + * the server is configured to send mail using SMTP. + * + * @return \Illuminate\View\View + */ + public function index(): View + { + return view('admin.settings.mail', [ + 'disabled' => $this->config->get('mail.driver') !== 'smtp', + ]); + } + + /** + * Handle request to update SMTP mail settings. + * + * @param \Pterodactyl\Http\Requests\Admin\Settings\MailSettingsFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function update(MailSettingsFormRequest $request): RedirectResponse + { + if ($this->config->get('mail.driver') !== 'smtp') { + throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.'); + } + + $values = $request->normalize(); + if (array_get($values, 'mail:password') === '!e') { + $values['mail:password'] = ''; + } + + foreach ($values as $key => $value) { + if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && ! empty($value)) { + $value = $this->encrypter->encrypt($value); + } + + $this->settings->set('settings::' . $key, $value); + } + + $this->kernel->call('queue:restart'); + $this->alert->success('Mail settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); + + return redirect()->route('admin.settings.mail'); + } +} diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index bc5ff70ee..266983471 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -11,7 +11,6 @@ namespace Pterodactyl\Http\Middleware; use Closure; use Illuminate\Http\Request; -use Krucas\Settings\Settings; use Prologue\Alerts\AlertsMessageBag; class RequireTwoFactorAuthentication @@ -25,11 +24,6 @@ class RequireTwoFactorAuthentication */ private $alert; - /** - * @var \Krucas\Settings\Settings - */ - private $settings; - /** * The names of routes that should be accessable without 2FA enabled. * @@ -56,12 +50,10 @@ class RequireTwoFactorAuthentication * RequireTwoFactorAuthentication constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Krucas\Settings\Settings $settings */ - public function __construct(AlertsMessageBag $alert, Settings $settings) + public function __construct(AlertsMessageBag $alert) { $this->alert = $alert; - $this->settings = $settings; } /** @@ -81,10 +73,7 @@ class RequireTwoFactorAuthentication return $next($request); } - switch ((int) $this->settings->get('2fa', 0)) { - case self::LEVEL_NONE: - return $next($request); - break; + switch ((int) config('pterodactyl.auth.2fa_required')) { case self::LEVEL_ADMIN: if (! $request->user()->root_admin || $request->user()->use_totp) { return $next($request); @@ -95,6 +84,9 @@ class RequireTwoFactorAuthentication return $next($request); } break; + case self::LEVEL_NONE: + default: + return $next($request); } $this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash(); diff --git a/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php new file mode 100644 index 000000000..a80d8dab9 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php @@ -0,0 +1,42 @@ + 'required|in:true,false', + 'recaptcha:secret_key' => 'required|string|max:255', + 'recaptcha:website_key' => 'required|string|max:255', + 'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60', + 'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60', + 'pterodactyl:console:count' => 'required|integer|min:1', + 'pterodactyl:console:frequency' => 'required|integer|min:10', + ]; + } + + /** + * @return array + */ + public function attributes() + { + return [ + 'recaptcha:enabled' => 'reCAPTCHA Enabled', + 'recaptcha:secret_key' => 'reCAPTCHA Secret Key', + 'recaptcha:website_key' => 'reCAPTCHA Website Key', + 'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout', + 'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout', + 'pterodactyl:console:count' => 'Console Message Count', + 'pterodactyl:console:frequency' => 'Console Frequency Tick', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php new file mode 100644 index 000000000..0b02561dd --- /dev/null +++ b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php @@ -0,0 +1,36 @@ + 'required|string|max:255', + 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', + 'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))], + ]; + } + + /** + * @return array + */ + public function attributes() + { + return [ + 'app:name' => 'Company Name', + 'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication', + 'app:locale' => 'Default Language', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php new file mode 100644 index 000000000..269c2f6c7 --- /dev/null +++ b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php @@ -0,0 +1,44 @@ + 'required|string', + 'mail:port' => 'required|integer|between:1,65535', + 'mail:encryption' => 'present|string|in:"",tls,ssl', + 'mail:username' => 'string|max:255', + 'mail:password' => 'string|max:255', + 'mail:from:address' => 'required|string|email', + 'mail:from:name' => 'string|max:255', + ]; + } + + /** + * Override the default normalization function for this type of request + * as we need to accept empty values on the keys. + * + * @param array $only + * @return array + */ + public function normalize($only = []) + { + $keys = array_flip(array_keys($this->rules())); + + if (empty($this->input('mail:password'))) { + unset($keys['mail:password']); + } + + return $this->only(array_flip($keys)); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 000000000..90d41f3d4 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,39 @@ + 'required|string|between:1,255', + 'value' => 'string', + ]; +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b9b656dc1..b3a7c33ea 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,7 +13,6 @@ use Pterodactyl\Observers\UserObserver; use Pterodactyl\Observers\ServerObserver; use Pterodactyl\Observers\SubuserObserver; use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; -use DaneEveritt\LoginNotifications\NotificationServiceProvider; use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider; class AppServiceProvider extends ServiceProvider @@ -42,10 +41,6 @@ class AppServiceProvider extends ServiceProvider $this->app->register(DebugbarServiceProvider::class); $this->app->register(IdeHelperServiceProvider::class); } - - if (config('pterodactyl.auth.notifications')) { - $this->app->register(NotificationServiceProvider::class); - } } /** diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index bc393a883..e34520962 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -26,6 +26,7 @@ use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\ScheduleRepository; +use Pterodactyl\Repositories\Eloquent\SettingsRepository; use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\PermissionRepository; @@ -47,6 +48,7 @@ use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; +use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; @@ -86,10 +88,13 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class); + $this->app->bind(SettingsRepositoryInterface::class, SettingsRepository::class); $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class); $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); + $this->app->alias(SettingsRepositoryInterface::class, 'settings'); + // Daemon Repositories if ($this->app->make('config')->get('pterodactyl.daemon.use_new_daemon')) { $this->app->bind(ConfigurationRepositoryInterface::class, \Pterodactyl\Repositories\Wings\ConfigurationRepository::class); diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php new file mode 100644 index 000000000..dc9e9cdc6 --- /dev/null +++ b/app/Providers/SettingsServiceProvider.php @@ -0,0 +1,101 @@ +get('pterodactyl.load_environment_only', false)) { + return; + } + + // Only set the email driver settings from the database if we + // are configured using SMTP as the driver. + if ($config->get('mail.driver') === 'smtp') { + $this->keys = array_merge($this->keys, $this->emailKeys); + } + + $values = $settings->all()->mapWithKeys(function ($setting) { + return [$setting->key => $setting->value]; + })->toArray(); + + foreach ($this->keys as $key) { + $value = array_get($values, 'settings::' . $key, $config->get(str_replace(':', '.', $key))); + if (in_array($key, self::$encrypted)) { + try { + $value = $encrypter->decrypt($value); + } catch (DecryptException $exception) { + } + } + + $config->set(str_replace(':', '.', $key), $value); + } + } + + /** + * @return array + */ + public static function getEncryptedKeys(): array + { + return self::$encrypted; + } +} diff --git a/app/Repositories/Eloquent/SettingsRepository.php b/app/Repositories/Eloquent/SettingsRepository.php new file mode 100644 index 000000000..b6937bf31 --- /dev/null +++ b/app/Repositories/Eloquent/SettingsRepository.php @@ -0,0 +1,96 @@ +clearCache($key); + $this->withoutFresh()->updateOrCreate(['key' => $key], ['value' => $value]); + } + + /** + * Retrieve a persistent setting from the database. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get(string $key, $default = null) + { + // If item has already been requested return it from the cache. If + // we already know it is missing, immediately return the default + // value. + if (array_key_exists($key, $this->cache)) { + return $this->cache[$key]; + } elseif (array_key_exists($key, $this->databaseMiss)) { + return $default; + } + + $instance = $this->getBuilder()->where('key', $key)->first(); + + if (is_null($instance)) { + $this->databaseMiss[$key] = true; + + return $default; + } + + $this->cache[$key] = $instance->value; + + return $this->cache[$key]; + } + + /** + * Remove a key from the database cache. + * + * @param string $key + * @return mixed + */ + public function forget(string $key) + { + $this->clearCache($key); + $this->deleteWhere(['key' => $key]); + } + + /** + * Remove a key from the cache. + * + * @param string $key + */ + protected function clearCache(string $key) + { + unset($this->cache[$key], $this->databaseMiss[$key]); + } +} diff --git a/app/Traits/Helpers/AvailableLanguages.php b/app/Traits/Helpers/AvailableLanguages.php new file mode 100644 index 000000000..f44771f6d --- /dev/null +++ b/app/Traits/Helpers/AvailableLanguages.php @@ -0,0 +1,56 @@ +getFilesystemInstance()->directories(resource_path('lang')))->mapWithKeys(function ($path) use ($localize) { + $code = basename($path); + $value = $localize ? $this->getIsoInstance()->nativeByCode1($code) : $this->getIsoInstance()->languageByCode1($code); + + return [$code => title_case($value)]; + })->toArray(); + } + + /** + * Return an instance of the filesystem for getting a folder listing. + * + * @return \Illuminate\Filesystem\Filesystem + */ + private function getFilesystemInstance(): Filesystem + { + return $this->filesystem = $this->filesystem ?: app()->make(Filesystem::class); + } + + /** + * Return an instance of the ISO639 class for generating names. + * + * @return \Matriphe\ISO639\ISO639 + */ + private function getIsoInstance(): ISO639 + { + return $this->iso639 = $this->iso639 ?: app()->make(ISO639::class); + } +} diff --git a/composer.json b/composer.json index fabe01f0f..6b1271f84 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "laravel/framework": "5.4.27", "laravel/tinker": "1.0.1", "lord/laroute": "~2.4.5", + "matriphe/iso-639": "^1.2", "mtdowling/cron-expression": "^1.2", "nesbot/carbon": "^1.22", "nicolaslopezj/searchable": "^1.9", diff --git a/composer.lock b/composer.lock index 9895b8330..139e3c1c3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "a393763d136e25a93fd5b636229496cf", + "content-hash": "bd42f43877e96cca4d4af755c590eb25", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -687,57 +687,6 @@ ], "time": "2014-09-09T13:34:57+00:00" }, - { - "name": "edvinaskrucas/settings", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/edvinaskrucas/settings.git", - "reference": "23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/edvinaskrucas/settings/zipball/23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022", - "reference": "23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022", - "shasum": "" - }, - "require": { - "illuminate/console": "^5.2", - "illuminate/database": "^5.2", - "illuminate/filesystem": "^5.2", - "illuminate/support": "^5.2", - "php": "^5.5|^7.0" - }, - "require-dev": { - "mockery/mockery": "0.9.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Krucas\\Settings\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Edvinas Kručas", - "email": "edv.krucas@gmail.com" - } - ], - "description": "Persistent settings package for Laravel framework.", - "keywords": [ - "Settings", - "laravel", - "persistent settings" - ], - "time": "2016-01-19T13:50:39+00:00" - }, { "name": "erusev/parsedown", "version": "1.6.3", @@ -1674,6 +1623,50 @@ ], "time": "2017-09-04T02:25:29+00:00" }, + { + "name": "matriphe/iso-639", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/matriphe/php-iso-639.git", + "reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matriphe/php-iso-639/zipball/0245d844daeefdd22a54b47103ffdb0e03c323e1", + "reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matriphe\\ISO639\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muhammad Zamroni", + "email": "halo@matriphe.com" + } + ], + "description": "PHP library to convert ISO-639-1 code to language name.", + "keywords": [ + "639", + "iso", + "iso-639", + "lang", + "language", + "laravel" + ], + "time": "2017-07-19T15:11:19+00:00" + }, { "name": "monolog/monolog", "version": "1.23.0", diff --git a/config/app.php b/config/app.php index 2f9da6704..d56b80634 100644 --- a/config/app.php +++ b/config/app.php @@ -1,5 +1,6 @@ env('APP_ENV', 'production'), @@ -14,7 +15,7 @@ return [ | framework needs to place the application's name in a notification or | any other location as required by the application or its packages. */ - 'name' => 'Pterodactyl', + 'name' => env('APP_NAME', 'Pterodactyl'), /* |-------------------------------------------------------------------------- @@ -158,6 +159,7 @@ return [ /* * Application Service Providers... */ + Pterodactyl\Providers\SettingsServiceProvider::class, Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, @@ -173,7 +175,6 @@ return [ */ igaster\laravelTheme\themeServiceProvider::class, Prologue\Alerts\AlertsServiceProvider::class, - Krucas\Settings\Providers\SettingsServiceProvider::class, Fideloper\Proxy\TrustedProxyServiceProvider::class, Laracasts\Utilities\JavaScript\JavaScriptServiceProvider::class, Lord\Laroute\LarouteServiceProvider::class, @@ -228,7 +229,6 @@ return [ 'Response' => Illuminate\Support\Facades\Response::class, 'Route' => Illuminate\Support\Facades\Route::class, 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Settings' => Krucas\Settings\Facades\Settings::class, 'Session' => Illuminate\Support\Facades\Session::class, 'Storage' => Illuminate\Support\Facades\Storage::class, 'Theme' => igaster\laravelTheme\Facades\Theme::class, diff --git a/config/pterodactyl.php b/config/pterodactyl.php index ad371bce9..523080ae3 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -1,6 +1,18 @@ (bool) env('APP_ENVIRONMENT_ONLY', false), + /* |-------------------------------------------------------------------------- | Service Author @@ -22,7 +34,7 @@ return [ | Should login success and failure events trigger an email to the user? */ 'auth' => [ - 'notifications' => env('LOGIN_NOTIFICATIONS', false), + '2fa_required' => env('APP_2FA_REQUIRED', 0), '2fa' => [ 'bytes' => 32, 'window' => env('APP_2FA_WINDOW', 4), diff --git a/config/recaptcha.php b/config/recaptcha.php index 1bac5a877..22d739481 100644 --- a/config/recaptcha.php +++ b/config/recaptcha.php @@ -14,12 +14,14 @@ return [ /* * Use a custom secret key, we use our public one by default */ - 'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LekAxoUAAAAAPW-PxNWaCLH76WkClMLSa2jImwD'), + 'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5'), + '_shipped_secret_key' => '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5', /* * Use a custom website key, we use our public one by default */ - 'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LekAxoUAAAAADjWZJ4ufcDRZBBiH9vfHawqRbup'), + 'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn'), + '_shipped_website_key' => '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn', /* * Domain verification is enabled by default and compares the domain used when solving the captcha diff --git a/config/settings.php b/config/settings.php deleted file mode 100644 index c6a75fb01..000000000 --- a/config/settings.php +++ /dev/null @@ -1,113 +0,0 @@ - env('SETTINGS_DRIVER', 'database'), - - /* - |-------------------------------------------------------------------------- - | Enable / Disable caching - |-------------------------------------------------------------------------- - | - | If it is enabled all values gets cached after accessing it. - | - */ - 'cache' => true, - - /* - |-------------------------------------------------------------------------- - | Enable / Disable value encryption - |-------------------------------------------------------------------------- - | - | If it is enabled all values gets encrypted and decrypted. - | - */ - 'encryption' => env('SETTINGS_ENCRYPTION', false), - - /* - |-------------------------------------------------------------------------- - | Enable / Disable events - |-------------------------------------------------------------------------- - | - | If it is enabled various settings related events will be fired. - | - */ - 'events' => true, - - /* - |-------------------------------------------------------------------------- - | Repositories Configuration - |-------------------------------------------------------------------------- - | - | Here you may configure the driver information for each repository that - | is used by your application. A default configuration has been added - | for each back-end shipped with this package. You are free to add more. - | - */ - - 'repositories' => [ - 'database' => [ - 'driver' => 'database', - 'connection' => env('DB_CONNECTION', 'mysql'), - 'table' => 'settings', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Key generator class - |-------------------------------------------------------------------------- - | - | Key generator is used to generate keys based on setting key and context. - | - */ - 'key_generator' => \Krucas\Settings\KeyGenerators\KeyGenerator::class, - - /* - |-------------------------------------------------------------------------- - | Context serializer class - |-------------------------------------------------------------------------- - | - | Context serializer serializes context. - | It is used with "Krucas\Settings\KeyGenerators\KeyGenerator" class. - | - */ - 'context_serializer' => \Krucas\Settings\ContextSerializers\ContextSerializer::class, - - /* - |-------------------------------------------------------------------------- - | Value serializer class - |-------------------------------------------------------------------------- - | - | Value serializer serializes / unserializes given value. - | - */ - 'value_serializer' => \Krucas\Settings\ValueSerializers\ValueSerializer::class, - - /* - |-------------------------------------------------------------------------- - | Override application config values - |-------------------------------------------------------------------------- - | - | If defined, settings package will override these config values from persistent - | settings repository. - | - | Sample: - | "app.fallback_locale", - | "app.locale" => "settings.locale", - | - */ - - 'override' => [ - ], -]; diff --git a/database/migrations/2017_12_12_220426_MigrateSettingsTableToNewFormat.php b/database/migrations/2017_12_12_220426_MigrateSettingsTableToNewFormat.php new file mode 100644 index 000000000..1bdaf6477 --- /dev/null +++ b/database/migrations/2017_12_12_220426_MigrateSettingsTableToNewFormat.php @@ -0,0 +1,30 @@ +truncate(); + Schema::table('settings', function (Blueprint $table) { + $table->increments('id')->first(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('id'); + }); + } +} diff --git a/phpunit.xml b/phpunit.xml index ceb832d08..89b3c0b22 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -32,5 +32,6 @@ + diff --git a/public/themes/pterodactyl/js/frontend/server.socket.js b/public/themes/pterodactyl/js/frontend/server.socket.js index ef6986168..f43409fd4 100644 --- a/public/themes/pterodactyl/js/frontend/server.socket.js +++ b/public/themes/pterodactyl/js/frontend/server.socket.js @@ -66,6 +66,7 @@ var Server = (function () { delay: 0, }); } + setStatusIcon(999); }); Socket.io.on('connect_error', function (err) { @@ -77,6 +78,7 @@ var Server = (function () { delay: 0, }); } + setStatusIcon(999); }); // Connected to Socket Successfully @@ -111,6 +113,7 @@ var Server = (function () { $('#server_status_icon').html(' Stopping'); break; default: + $('#server_status_icon').html(' Connection Error'); break; } } diff --git a/resources/themes/pterodactyl/admin/settings.blade.php b/resources/themes/pterodactyl/admin/settings.blade.php deleted file mode 100644 index 3e0407e67..000000000 --- a/resources/themes/pterodactyl/admin/settings.blade.php +++ /dev/null @@ -1,69 +0,0 @@ -{{-- Pterodactyl - Panel --}} -{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} - -{{-- This software is licensed under the terms of the MIT license. --}} -{{-- https://opensource.org/licenses/MIT --}} -@extends('layouts.admin') - -@section('title') - Settings -@endsection - -@section('content-header') -

Panel SettingsConfigure Pterodactyl to your liking.

- -@endsection - -@section('content') -
-
-
-
-

Panel Settings

-
-
-
-
-
- -
- -

This is the name that is used throughout the panel and in emails sent to clients.

-
-
-
- -
-
- - - -
-

For improved security you can require all administrators to have 2-Factor authentication enabled, or even require it for all users on the Panel.

-
-
-
-
-
-
In order to modify your SMTP settings for sending mail you will need to run php artisan p:environment:mail in this project's root folder.
-
-
-
- -
-
-
-
-@endsection diff --git a/resources/themes/pterodactyl/admin/settings/advanced.blade.php b/resources/themes/pterodactyl/admin/settings/advanced.blade.php new file mode 100644 index 000000000..5a7b4d724 --- /dev/null +++ b/resources/themes/pterodactyl/admin/settings/advanced.blade.php @@ -0,0 +1,117 @@ +@extends('layouts.admin') +@include('partials/admin.settings.nav', ['activeTab' => 'advanced']) + +@section('title') + Advanced Settings +@endsection + +@section('content-header') +

Advanced SettingsConfigure advanced settings for Pterodactyl.

+ +@endsection + +@section('content') + @yield('settings::nav') +
+
+
+
+
+

reCAPTCHA

+
+
+
+
+ +
+ +

If enabled, login forms and password reset forms will do a silent captcha check and display a visible captcha if needed.

+
+
+
+ +
+ +

Used for communication between your site and Google. Be sure to keep it a secret.

+
+
+
+ +
+ +
+
+
+ @if($showRecaptchaWarning) +
+
+
+ You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is recommended to generate new invisible reCAPTCHA keys that tied specifically to your website. +
+
+
+ @endif +
+
+
+
+

HTTP Connections

+
+
+
+
+ +
+ +

The amount of time in seconds to wait for a connection to be opened before throwing an error.

+
+
+
+ +
+ +

The amount of time in seconds to wait for a request to be completed before throwing an error.

+
+
+
+
+
+
+
+

Console

+
+
+
+
+ +
+ +

The number of messages to be pushed to the console per frequency tick.

+
+
+
+ +
+ +

The amount of time in milliseconds between each console message sending tick.

+
+
+
+
+
+
+ +
+
+
+
+@endsection diff --git a/resources/themes/pterodactyl/admin/settings/index.blade.php b/resources/themes/pterodactyl/admin/settings/index.blade.php new file mode 100644 index 000000000..62ef09631 --- /dev/null +++ b/resources/themes/pterodactyl/admin/settings/index.blade.php @@ -0,0 +1,75 @@ +@extends('layouts.admin') +@include('partials/admin.settings.nav', ['activeTab' => 'basic']) + +@section('title') + Settings +@endsection + +@section('content-header') +

Panel SettingsConfigure Pterodactyl to your liking.

+ +@endsection + +@section('content') + @yield('settings::nav') +
+
+
+
+

Panel Settings

+
+
+
+
+
+ +
+ +

This is the name that is used throughout the panel and in emails sent to clients.

+
+
+
+ +
+
+ @php + $level = old('pterodactyl:auth:2fa_required', config('pterodactyl.auth.2fa_required')); + @endphp + + + +
+

If enabled, any account falling into the selected grouping will be required to have 2-Factor authentication enabled to use the Panel.

+
+
+
+ +
+ +

The default language to use when rendering UI components.

+
+
+
+
+ +
+
+
+
+@endsection diff --git a/resources/themes/pterodactyl/admin/settings/mail.blade.php b/resources/themes/pterodactyl/admin/settings/mail.blade.php new file mode 100644 index 000000000..40403993f --- /dev/null +++ b/resources/themes/pterodactyl/admin/settings/mail.blade.php @@ -0,0 +1,108 @@ +@extends('layouts.admin') +@include('partials/admin.settings.nav', ['activeTab' => 'mail']) + +@section('title') + Mail Settings +@endsection + +@section('content-header') +

Mail SettingsConfigure how Pterodactyl should handle sending emails.

+ +@endsection + +@section('content') + @yield('settings::nav') +
+
+
+
+

Email Settings

+
+ @if($disabled) +
+
+
+
+ This interface is limited to instances using SMTP as the mail driver. Please either use php artisan p:environment:mail command to update your email settings, or set MAIL_DRIVER=smtp in your environment file. +
+
+
+
+ @else +
+
+
+
+ +
+ +

Enter the SMTP server address that mail should be sent through.

+
+
+
+ +
+ +

Enter the SMTP server port that mail should be sent through.

+
+
+
+ +
+ @php + $encryption = old('mail:encryption', config('mail.encryption')); + @endphp + +

Select the type of encryption to use when sending mail.

+
+
+
+ +
+ +

The username to use when connecting to the SMTP server.

+
+
+
+ +
+ +

The password to use in conjunction with the SMTP username. Leave blank to continue using the existing password. To set the password to an empty value enter !e into the field.

+
+
+
+
+
+
+ +
+ +

Enter an email address that all outgoing emails will originate from.

+
+
+
+ +
+ +

The name that emails should appear to come from.

+
+
+
+
+ +
+ @endif +
+
+
+@endsection diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index 0c2108ffa..262e59cd5 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -8,7 +8,7 @@ - {{ Settings::get('company', 'Pterodactyl') }} - @yield('title') + {{ config('app.name', 'Pterodactyl') }} - @yield('title') @@ -44,7 +44,7 @@