From b07052548cc385dbed93010ce2f6cc667a455e5c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Nov 2017 21:31:54 -0600 Subject: [PATCH 01/14] Fix inability to delete a node, closes #741 --- CHANGELOG.md | 1 + .../themes/pterodactyl/admin/nodes/view/allocation.blade.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a3a8b1..614cf42a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.1]` — Fixes missing check in environment setup that would leave the Hashids salt empty. * `[beta.1]` — Fixes bug preventing loading of allocations when trying to create a new server. * `[beta.1]` — Fixes bug causing inability to create new servers on the Panel. +* `[beta.1]` — Fixes bug causing inability to delete an allocation due to misconfigured JS. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php index 730ed577..0c7262c3 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php @@ -177,7 +177,7 @@ }, function () { $.ajax({ method: 'DELETE', - url: Router.route('admin.nodes.view.allocation.removeSingle', { id: Pterodactyl.node.id, allocation: allocation }), + url: Router.route('admin.nodes.view.allocation.removeSingle', { node: Pterodactyl.node.id, allocation: allocation }), headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, }).done(function (data) { element.parent().parent().addClass('warning').delay(100).fadeOut(); From 4dfc7a0053979ea87e8488fe2537c88419a24ba8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Nov 2017 21:41:39 -0600 Subject: [PATCH 02/14] Cleanup pagination CSS and UI --- app/Repositories/Eloquent/NodeRepository.php | 2 +- public/themes/pterodactyl/css/pterodactyl.css | 4 +++ .../admin/nodes/view/allocation.blade.php | 8 +++-- .../vendor/pagination/bootstrap-4.blade.php | 36 ------------------- .../vendor/pagination/default.blade.php | 2 +- .../pagination/simple-bootstrap-4.blade.php | 17 --------- .../pagination/simple-default.blade.php | 17 --------- 7 files changed, 11 insertions(+), 75 deletions(-) delete mode 100644 resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php delete mode 100644 resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php delete mode 100644 resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 9a706726..2cd26c15 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -105,7 +105,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa $instance->setRelation( 'allocations', $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc') - ->with('server')->paginate(50) + ->with('server')->paginate(2) ); return $instance; diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 0cc55b21..a0b9da1e 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -420,3 +420,7 @@ label.control-label > span.field-optional:before { content: "optional"; color: #bbbbbb; } + +.pagination > li > a, .pagination > li > span { + padding: 3px 10px !important; +} diff --git a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php index 0c7262c3..50a6a84e 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php @@ -72,9 +72,11 @@ @endforeach - + @if($node->allocations->hasPages()) + + @endif
diff --git a/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php b/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php deleted file mode 100644 index 9d80428c..00000000 --- a/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php +++ /dev/null @@ -1,36 +0,0 @@ -@if ($paginator->count() > 1) -
    - - @if ($paginator->onFirstPage()) -
  • «
  • - @else -
  • - @endif - - - @foreach ($elements as $element) - - @if (is_string($element)) -
  • {{ $element }}
  • - @endif - - - @if (is_array($element)) - @foreach ($element as $page => $url) - @if ($page == $paginator->currentPage()) -
  • {{ $page }}
  • - @else -
  • {{ $page }}
  • - @endif - @endforeach - @endif - @endforeach - - - @if ($paginator->hasMorePages()) -
  • - @else -
  • »
  • - @endif -
-@endif diff --git a/resources/themes/pterodactyl/vendor/pagination/default.blade.php b/resources/themes/pterodactyl/vendor/pagination/default.blade.php index 26e56994..1ecfac98 100644 --- a/resources/themes/pterodactyl/vendor/pagination/default.blade.php +++ b/resources/themes/pterodactyl/vendor/pagination/default.blade.php @@ -1,5 +1,5 @@ @if ($paginator->lastPage() > 1) -
    +
      @if ($paginator->onFirstPage()) {{--
    • «
    • --}} diff --git a/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php b/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php deleted file mode 100644 index 4b14efeb..00000000 --- a/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@if ($paginator->count() > 1) -
        - - @if ($paginator->onFirstPage()) -
      • «
      • - @else -
      • - @endif - - - @if ($paginator->hasMorePages()) -
      • - @else -
      • »
      • - @endif -
      -@endif diff --git a/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php b/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php deleted file mode 100644 index a45097ee..00000000 --- a/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@if ($paginator->count() > 1) -
        - - @if ($paginator->onFirstPage()) -
      • «
      • - @else -
      • - @endif - - - @if ($paginator->hasMorePages()) -
      • - @else -
      • »
      • - @endif -
      -@endif From 1740b8dfb597b48732720c62a5d65aa6e886dee2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Nov 2017 21:42:24 -0600 Subject: [PATCH 03/14] Revert change to node allocation selection query --- app/Repositories/Eloquent/NodeRepository.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 2cd26c15..fbd361e9 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -104,8 +104,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa $instance->setRelation( 'allocations', - $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc') - ->with('server')->paginate(2) + $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50) ); return $instance; From 81869bd5f2f0c57aaba5f7f9db20762209e66ea7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Nov 2017 21:47:43 -0600 Subject: [PATCH 04/14] Fix allocation alias setting --- CHANGELOG.md | 1 + app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php | 2 +- app/Models/Allocation.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614cf42a..74534404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.1]` — Fixes bug preventing loading of allocations when trying to create a new server. * `[beta.1]` — Fixes bug causing inability to create new servers on the Panel. * `[beta.1]` — Fixes bug causing inability to delete an allocation due to misconfigured JS. +* `[beta.1]` — Fixes bug causing inability to set the IP alias for an allocation to an empty value. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php index 6d607211..2552114a 100644 --- a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php +++ b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php @@ -19,7 +19,7 @@ class AllocationAliasFormRequest extends AdminFormRequest public function rules() { return [ - 'alias' => 'required|nullable|string', + 'alias' => 'present|nullable|string', 'allocation_id' => 'required|numeric|exists:allocations,id', ]; } diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index bb77647d..2fce57e8 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -60,7 +60,7 @@ class Allocation extends Model implements CleansAttributes, ValidableContract 'node_id' => 'exists:nodes,id', 'ip' => 'ip', 'port' => 'numeric|between:1024,65553', - 'alias' => 'string', + 'ip_alias' => 'nullable|string', 'server_id' => 'nullable|exists:servers,id', ]; From 1800d1c095d0b942f114e37cb21c0b5b6e82dc44 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 11 Nov 2017 13:56:38 -0600 Subject: [PATCH 05/14] Fix bug preventing variables with quotes from rendering in the ACP. --- .../admin/servers/view/startup.blade.php | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index df78cadb..c102167b 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -122,24 +122,9 @@ {!! Theme::js('vendor/lodash/lodash.js') !!} From 26eeffd7649c33303825d4ba341d21bdcf4d7abf Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 11 Nov 2017 15:07:01 -0600 Subject: [PATCH 06/14] Fix bug preventing changing of the server startup on first save attempt. --- CHANGELOG.md | 4 +++ .../Repository/ServerRepositoryInterface.php | 11 ++++--- .../Controllers/Admin/ServersController.php | 19 ----------- .../Eloquent/ServerRepository.php | 24 ++++++++------ .../Servers/StartupModificationService.php | 27 ++++++++-------- .../admin/servers/view/details.blade.php | 32 +------------------ .../admin/servers/view/startup.blade.php | 18 ++++++++++- routes/admin.php | 1 - .../StartupModificationServiceTest.php | 16 +++++++--- 9 files changed, 69 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74534404..804d01dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.1]` — Fixes bug causing inability to create new servers on the Panel. * `[beta.1]` — Fixes bug causing inability to delete an allocation due to misconfigured JS. * `[beta.1]` — Fixes bug causing inability to set the IP alias for an allocation to an empty value. +* `[beta.1]` — Fixes bug that caused startup changes to not propigate to the server correctly on the first save. + +### Changed +* Moved Docker image setting to be on the startup management page for a server rather than the details page. This value changes based on the Nest and Egg that are selected. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index e074d705..2fb8349e 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -95,14 +95,15 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter public function getWithDatabases($id); /** - * Return data about the daemon service in a consumable format. + * Get data for use when updating a server on the Daemon. Returns an array of + * the egg and pack UUID which are used for build and rebuild. Only loads relations + * if they are missing, or refresh is set to true. * - * @param int $id + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getDaemonServiceData($id); + public function getDaemonServiceData(Server $server, bool $refresh = false): array; /** * Return an array of server IDs that a given user can access based on owner and subuser permissions. diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 203e7058..0bf3610f 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -410,25 +410,6 @@ class ServersController extends Controller return redirect()->route('admin.servers.view.details', $server->id); } - /** - * Set the new docker container for a server. - * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function setContainer(Request $request, Server $server) - { - $this->detailsModificationService->setDockerImage($server, $request->input('docker_image')); - $this->alert->success(trans('admin/server.alerts.docker_image_updated'))->flash(); - - return redirect()->route('admin.servers.view.details', $server->id); - } - /** * Toggles the install status for a server. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index d66b6813..b8f5cc6f 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -187,21 +187,27 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } /** - * {@inheritdoc} + * Get data for use when updating a server on the Daemon. Returns an array of + * the egg and pack UUID which are used for build and rebuild. Only loads relations + * if they are missing, or refresh is set to true. + * + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return array */ - public function getDaemonServiceData($id) + public function getDaemonServiceData(Server $server, bool $refresh = false): array { - Assert::integerish($id, 'First argument passed to getDaemonServiceData must be integer, received %s.'); + if (! $server->relationLoaded('egg') || $refresh) { + $server->load('egg'); + } - $instance = $this->getBuilder()->with('egg.nest', 'pack')->find($id, $this->getColumns()); - if (! $instance) { - throw new RecordNotFoundException(); + if (! $server->relationLoaded('pack') || $refresh) { + $server->load('pack'); } return [ - 'type' => $instance->egg->nest->folder, - 'option' => $instance->egg->tag, - 'pack' => (! is_null($instance->pack_id)) ? $instance->pack->uuid : null, + 'egg' => $server->getRelation('egg')->uuid, + 'pack' => is_null($server->getRelation('pack')) ? null : $server->getRelation('pack')->uuid, ]; } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index ae91fb3b..969d4310 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -99,14 +99,17 @@ class StartupModificationService }); } - $daemonData = ['build' => [ - 'env|overwrite' => $this->environmentService->handle($server), - ]]; - + $daemonData = []; if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { $this->updateAdministrativeSettings($data, $server, $daemonData); } + $daemonData = array_merge_recursive($daemonData, [ + 'build' => [ + 'env|overwrite' => $this->environmentService->handle($server), + ], + ]); + try { $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); } catch (RequestException $exception) { @@ -136,17 +139,15 @@ class StartupModificationService 'egg_id' => array_get($data, 'egg_id', $server->egg_id), 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, 'skip_scripts' => isset($data['skip_scripts']), + 'image' => array_get($data, 'docker_image', $server->image), ]); - if ( - $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || - $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || - $server->pack_id != array_get($data, 'pack_id', $server->pack_id) - ) { - $daemonData['service'] = array_merge( - $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), + $daemonData = array_merge($daemonData, [ + 'build' => ['image' => $server->image], + 'service' => array_merge( + $this->repository->getDaemonServiceData($server, true), ['skip_scripts' => isset($data['skip_scripts'])] - ); - } + ), + ]); } } diff --git a/resources/themes/pterodactyl/admin/servers/view/details.blade.php b/resources/themes/pterodactyl/admin/servers/view/details.blade.php index 275f99eb..60bcded6 100644 --- a/resources/themes/pterodactyl/admin/servers/view/details.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/details.blade.php @@ -39,7 +39,7 @@
-
+

Base Information

@@ -63,15 +63,6 @@

A brief description of this server.

-
- - -

This token should not be shared with anyone as it has full control over this server.

-
-
- -

Resetting this token will cause any requests using the old token to fail.

-
-
-
-
-

Container Setup

-
-
-
-
- - -

The docker image to use for this server. The default image for this service and option combination is {{ $server->egg->docker_image }}.

-
-
- -
-
-
@endsection diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index c102167b..15bee661 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -109,6 +109,18 @@
+
+
+

Docker Container Configuration

+
+
+
+ + +

The Docker image to use for this server. The default image for the selected egg is .

+
+
+
@@ -143,7 +155,11 @@ var parentChain = _.get(Pterodactyl.nests, $('#pNestId').val(), null); var objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null); - $('#pDefaultContainer').val(_.get(objectChain, 'docker_image', 'not defined!')); + $('#setDefaultImage').html(_.get(objectChain, 'docker_image', 'undefined')); + $('#pDockerImage').val(_.get(objectChain, 'docker_image', 'undefined')); + if (objectChain.id === parseInt('{{ $server->egg_id }}')) { + $('#pDockerImage').val('{{ $server->image }}'); + } if (!_.get(objectChain, 'startup', false)) { $('#pDefaultStartupCommand').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!')); diff --git a/routes/admin.php b/routes/admin.php index 1dfdf972..edb6fd9f 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -105,7 +105,6 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/view/{server}/delete', 'ServersController@delete'); Route::patch('/view/{server}/details', 'ServersController@setDetails'); - Route::patch('/view/{server}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::patch('/view/{server}/database', 'ServersController@resetDatabasePassword'); Route::delete('/view/{server}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index 5d8076ae..ca1dc33c 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -107,6 +107,7 @@ class StartupModificationServiceTest extends TestCase { $model = factory(Server::class)->make([ 'egg_id' => 123, + 'image' => 'docker:image', ]); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); @@ -121,22 +122,29 @@ class StartupModificationServiceTest extends TestCase 'variable_id' => 1, ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); - $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); - $this->repository->shouldReceive('update')->with($model->id, m::subset([ 'installed' => 0, 'egg_id' => 456, 'pack_id' => 789, + 'image' => 'docker:image', ]))->once()->andReturn($model); - $this->repository->shouldReceive('withColumns->getDaemonServiceData')->with($model->id)->once()->andReturn([]); + $this->repository->shouldReceive('getDaemonServiceData')->with($model, true)->once()->andReturn([ + 'egg' => 'abcd1234', + 'pack' => 'xyz987', + ]); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); $this->daemonServerRepository->shouldReceive('update')->with([ 'build' => [ 'env|overwrite' => ['env'], + 'image' => $model->image, ], 'service' => [ + 'egg' => 'abcd1234', + 'pack' => 'xyz987', 'skip_scripts' => false, ], ])->once()->andReturnSelf(); @@ -145,7 +153,7 @@ class StartupModificationServiceTest extends TestCase $service = $this->getService(); $service->setUserLevel(User::USER_LEVEL_ADMIN); - $service->handle($model, ['egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); + $service->handle($model, ['docker_image' => 'docker:image', 'egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); $this->assertTrue(true); } From 6043114f38d65b1aad554e5bf0fdccd6b5de6049 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 11 Nov 2017 15:58:42 -0600 Subject: [PATCH 07/14] Text cleanup for settings --- .../pterodactyl/admin/settings.blade.php | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/resources/themes/pterodactyl/admin/settings.blade.php b/resources/themes/pterodactyl/admin/settings.blade.php index 0de2e63f..3e0407e6 100644 --- a/resources/themes/pterodactyl/admin/settings.blade.php +++ b/resources/themes/pterodactyl/admin/settings.blade.php @@ -34,24 +34,6 @@

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

- {{--
- -
- -

This is the default language that all clients will use unless they manually change it.

-
-
--}}
@@ -66,13 +48,13 @@ Everybody
-

Require your administrators or users to have 2FA enabled. Users include Admins. Everybody includes Sub Users.

+

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 pterodactyl:mail in this project's root folder.
+
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.
From f94e4c15b09df2340c79c6e1bf55a8c11a0f0e45 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Sun, 12 Nov 2017 18:16:54 -0500 Subject: [PATCH 08/14] Replace magic numbers with constants (#754) * Replace magic numbers with constants --- .../Commands/Maintenance/CleanServiceBackupFilesCommand.php | 4 +++- app/Repositories/Eloquent/NodeRepository.php | 5 ++++- app/Services/Schedules/Tasks/TaskCreationService.php | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php index 1cbe6090..f3982921 100644 --- a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php +++ b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php @@ -15,6 +15,8 @@ use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; class CleanServiceBackupFilesCommand extends Command { + const BACKUP_THRESHOLD_MINUTES = 5; + /** * @var \Carbon\Carbon */ @@ -58,7 +60,7 @@ class CleanServiceBackupFilesCommand extends Command collect($files)->each(function ($file) { $lastModified = $this->carbon->timestamp($this->disk->lastModified($file)); - if ($lastModified->diffInMinutes($this->carbon->now()) > 5) { + if ($lastModified->diffInMinutes($this->carbon->now()) > self::BACKUP_THRESHOLD_MINUTES) { $this->disk->delete($file); $this->info(trans('command/messages.maintenance.deleting_service_backup', ['file' => $file])); } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index fbd361e9..c1dd68a8 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -18,6 +18,9 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa { use Searchable; + const THRESHOLD_PERCENTAGE_LOW = 75; + const THRESHOLD_PERCENTAGE_MEDIUM = 90; + /** * {@inheritdoc} */ @@ -56,7 +59,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa 'value' => number_format($value), 'max' => number_format($maxUsage), 'percent' => $percent, - 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), + 'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'), ], ]; }) diff --git a/app/Services/Schedules/Tasks/TaskCreationService.php b/app/Services/Schedules/Tasks/TaskCreationService.php index 0a015299..9ea1c178 100644 --- a/app/Services/Schedules/Tasks/TaskCreationService.php +++ b/app/Services/Schedules/Tasks/TaskCreationService.php @@ -16,6 +16,8 @@ use Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException; class TaskCreationService { + const MAX_INTERVAL_TIME_SECONDS = 900; + /** * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface */ @@ -50,7 +52,7 @@ class TaskCreationService $schedule = ($schedule instanceof Schedule) ? $schedule->id : $schedule; $delay = $data['time_interval'] === 'm' ? $data['time_value'] * 60 : $data['time_value']; - if ($delay > 900) { + if ($delay > self::MAX_INTERVAL_TIME_SECONDS) { throw new TaskIntervalTooLongException(trans('exceptions.tasks.chain_interval_too_long')); } From 559aa51f0110996702ba42cd6814ca896db730ce Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Wed, 15 Nov 2017 21:49:07 -0500 Subject: [PATCH 09/14] Add links to beta (#756) --- resources/themes/pterodactyl/partials/_internal/beta.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/themes/pterodactyl/partials/_internal/beta.blade.php b/resources/themes/pterodactyl/partials/_internal/beta.blade.php index d3ef3c46..88570c8d 100644 --- a/resources/themes/pterodactyl/partials/_internal/beta.blade.php +++ b/resources/themes/pterodactyl/partials/_internal/beta.blade.php @@ -2,7 +2,7 @@
- You are running a beta version of Pterodactyl Panel. Not all features are complete and bugs should be expected. Please report any bugs on Discord or via our Github issue tracker. + You are running a beta version of Pterodactyl Panel. Not all features are complete and bugs should be expected. Please report any bugs on Discord or via our GitHub Issue Tracker.
From c2408a28d815ad3b4ef789032fe5de643b14dc2a Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Fri, 17 Nov 2017 18:08:10 -0500 Subject: [PATCH 10/14] Remove unused imports --- app/Policies/APIKeyPolicy.php | 1 - app/Providers/EventServiceProvider.php | 1 - app/Providers/RouteServiceProvider.php | 1 - app/Transformers/Admin/SubuserTransformer.php | 1 - app/Transformers/User/SubuserTransformer.php | 1 - 5 files changed, 5 deletions(-) diff --git a/app/Policies/APIKeyPolicy.php b/app/Policies/APIKeyPolicy.php index 7ca4e0a9..69ce45c0 100644 --- a/app/Policies/APIKeyPolicy.php +++ b/app/Policies/APIKeyPolicy.php @@ -13,7 +13,6 @@ use Cache; use Carbon; use Pterodactyl\Models\User; use Pterodactyl\Models\APIKey as Key; -use Pterodactyl\Models\APIPermission as Permission; class APIKeyPolicy { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 1f48d33d..c7c928f1 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Providers; -use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 96bfb2ec..57ae43fa 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Providers; -use Pterodactyl\Models\User; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; diff --git a/app/Transformers/Admin/SubuserTransformer.php b/app/Transformers/Admin/SubuserTransformer.php index 0bc0ed01..93ed25d5 100644 --- a/app/Transformers/Admin/SubuserTransformer.php +++ b/app/Transformers/Admin/SubuserTransformer.php @@ -11,7 +11,6 @@ namespace Pterodactyl\Transformers\Admin; use Illuminate\Http\Request; use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; use League\Fractal\TransformerAbstract; class SubuserTransformer extends TransformerAbstract diff --git a/app/Transformers/User/SubuserTransformer.php b/app/Transformers/User/SubuserTransformer.php index 48d9b5ce..faac5965 100644 --- a/app/Transformers/User/SubuserTransformer.php +++ b/app/Transformers/User/SubuserTransformer.php @@ -10,7 +10,6 @@ namespace Pterodactyl\Transformers\User; use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; use League\Fractal\TransformerAbstract; class SubuserTransformer extends TransformerAbstract From c7f01d66d5b61020274490e9b2353a6b37312283 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Fri, 17 Nov 2017 20:01:42 -0500 Subject: [PATCH 11/14] Fix namespace --- app/Repositories/Eloquent/EloquentRepository.php | 2 +- app/Repositories/Repository.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index ea333d2d..d94cd5ca 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -10,7 +10,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Webmozart\Assert\Assert; -use Pterodactyl\Repository\Repository; +use Pterodactyl\Repositories\Repository; use Illuminate\Database\Query\Expression; use Pterodactyl\Contracts\Repository\RepositoryInterface; use Pterodactyl\Exceptions\Model\DataValidationException; diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index f74b519f..f9164d28 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -7,7 +7,7 @@ * https://opensource.org/licenses/MIT */ -namespace Pterodactyl\Repository; +namespace Pterodactyl\Repositories; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\RepositoryInterface; From 5d4f8ca9ab1ce378bcb20b37c95aebb4906dfe6a Mon Sep 17 00:00:00 2001 From: TheProKiller756 Date: Sat, 18 Nov 2017 15:26:28 +0100 Subject: [PATCH 12/14] Fix maximum size translation to get it working Fixed that :size was translated and it doesn't work --- resources/lang/es/server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/es/server.php b/resources/lang/es/server.php index 8bbb31b4..b643d421 100644 --- a/resources/lang/es/server.php +++ b/resources/lang/es/server.php @@ -259,7 +259,7 @@ return [ 'header' => 'El Administrador De Archivos', 'header_sub' => 'Administrar todos tus archivos directamente desde la web.', 'loading' => 'La carga inicial de la estructura del archivo, esto puede tardar unos segundos.', - 'path' => 'Cuando la configuración de rutas de archivo en su servidor de plugins o configuración que debe utilizar :path de acceso como base de la ruta. El tamaño máximo para la web basado en la carga de archivos a este nodo es :tamaño de la.', + 'path' => 'Cuando la configuración de rutas de archivo en su servidor de plugins o configuración que debe utilizar :path de acceso como base de la ruta. El tamaño máximo para la web basado en la carga de archivos a este nodo es :size de la.', 'seconds_ago' => 'hace segundos', 'file_name' => 'Nombre De Archivo', 'size' => 'Tamaño', From 2782985ce2eae9e93d1728c13d9150a566f8e7e2 Mon Sep 17 00:00:00 2001 From: TheProKiller756 Date: Sat, 18 Nov 2017 15:28:53 +0100 Subject: [PATCH 13/14] Update auth.php --- resources/lang/es/auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/es/auth.php b/resources/lang/es/auth.php index 4c33b8f1..e8269476 100644 --- a/resources/lang/es/auth.php +++ b/resources/lang/es/auth.php @@ -6,7 +6,7 @@ return [ 'authentication_required' => 'La autenticación es necesaria para continuar.', 'remember_me' => 'Recuérdame', 'sign_in' => 'Iniciar Sesión', - 'forgot_password' => 'Olvidé mi contraseña!', + 'forgot_password' => '¡Olvidé mi contraseña!', 'request_reset_text' => '¿Olvidaste tu contraseña? No es el fin del mundo, sólo proporcione su correo electrónico a continuación.', 'reset_password_text' => 'Restablece la contraseña de su cuenta.', 'reset_password' => 'Restablece contraseña de cuenta.', From c7c2c1a45eea92274cfaf4f4a441e2255ba030d1 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 18 Nov 2017 13:35:33 -0500 Subject: [PATCH 14/14] Implement changes to 2FA system (#761) --- CHANGELOG.md | 1 + app/Http/Controllers/Auth/LoginController.php | 2 +- .../Controllers/Base/SecurityController.php | 13 +- app/Models/User.php | 6 + app/Services/Users/ToggleTwoFactorService.php | 56 ++++-- app/Services/Users/TwoFactorSetupService.php | 45 +++-- composer.json | 4 +- composer.lock | 189 ++++++++++-------- config/app.php | 2 - config/pterodactyl.php | 5 + ...1922_Add2FaLastAuthorizationTimeColumn.php | 60 ++++++ .../pterodactyl/js/frontend/2fa-modal.js | 1 - .../pterodactyl/base/security.blade.php | 4 +- .../Base/SecurityControllerTest.php | 107 ++++------ tests/Unit/Jobs/Schedule/RunTaskJobTest.php | 2 +- .../DaemonKeyProviderServiceTest.php | 2 +- .../Users/ToggleTwoFactorServiceTest.php | 97 +++++---- .../Users/TwoFactorSetupServiceTest.php | 62 +++--- 18 files changed, 360 insertions(+), 298 deletions(-) create mode 100644 database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 804d01dd..6b450865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Changed * Moved Docker image setting to be on the startup management page for a server rather than the details page. This value changes based on the Nest and Egg that are selected. +* Two-Factor authentication tokens are now 32 bytes in length, and are stored encrypted at rest in the database. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 12f3df53..9fab7b53 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -202,7 +202,7 @@ class LoginController extends Controller return $this->sendFailedLoginResponse($request); } - if (! $G2FA->verifyKey($user->totp_secret, $request->input('2fa_token'), 2)) { + if (! $G2FA->verifyKey(Crypt::decrypt($user->totp_secret), $request->input('2fa_token'), 2)) { event(new \Illuminate\Auth\Events\Failed($user, $credentials)); return $this->sendFailedLoginResponse($request); diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index d22c0ddb..62f07738 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -27,7 +27,6 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; -use Illuminate\Contracts\Session\Session; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Services\Users\ToggleTwoFactorService; @@ -52,11 +51,6 @@ class SecurityController extends Controller */ protected $repository; - /** - * @var \Illuminate\Contracts\Session\Session - */ - protected $session; - /** * @var \Pterodactyl\Services\Users\ToggleTwoFactorService */ @@ -72,7 +66,6 @@ class SecurityController extends Controller * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Illuminate\Contracts\Config\Repository $config - * @param \Illuminate\Contracts\Session\Session $session * @param \Pterodactyl\Contracts\Repository\SessionRepositoryInterface $repository * @param \Pterodactyl\Services\Users\ToggleTwoFactorService $toggleTwoFactorService * @param \Pterodactyl\Services\Users\TwoFactorSetupService $twoFactorSetupService @@ -80,7 +73,6 @@ class SecurityController extends Controller public function __construct( AlertsMessageBag $alert, ConfigRepository $config, - Session $session, SessionRepositoryInterface $repository, ToggleTwoFactorService $toggleTwoFactorService, TwoFactorSetupService $twoFactorSetupService @@ -88,7 +80,6 @@ class SecurityController extends Controller $this->alert = $alert; $this->config = $config; $this->repository = $repository; - $this->session = $session; $this->toggleTwoFactorService = $toggleTwoFactorService; $this->twoFactorSetupService = $twoFactorSetupService; } @@ -122,7 +113,9 @@ class SecurityController extends Controller */ public function generateTotp(Request $request) { - return response()->json($this->twoFactorSetupService->handle($request->user())); + return response()->json([ + 'qrImage' => $this->twoFactorSetupService->handle($request->user()), + ]); } /** diff --git a/app/Models/User.php b/app/Models/User.php index 7b09165a..39e4a0a0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -63,6 +63,7 @@ class User extends Model implements 'language', 'use_totp', 'totp_secret', + 'totp_authenticated_at', 'gravatar', 'root_admin', ]; @@ -78,6 +79,11 @@ class User extends Model implements 'gravatar' => 'boolean', ]; + /** + * @var array + */ + protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'totp_authenticated_at']; + /** * The attributes excluded from the model's JSON form. * diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 56ec6953..e03a7638 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -1,66 +1,82 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Users; +use Carbon\Carbon; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; class ToggleTwoFactorService { /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Config\Repository */ - protected $google2FA; + private $config; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $repository; + private $repository; /** * ToggleTwoFactorService constructor. * - * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( + Encrypter $encrypter, Google2FA $google2FA, + Repository $config, UserRepositoryInterface $repository ) { + $this->config = $config; + $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; } /** - * @param int|\Pterodactyl\Models\User $user - * @param string $token - * @param null|bool $toggleState + * Toggle 2FA on an account only if the token provided is valid. + * + * @param \Pterodactyl\Models\User $user + * @param string $token + * @param bool|null $toggleState * @return bool * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ - public function handle($user, $token, $toggleState = null) + public function handle(User $user, string $token, bool $toggleState = null): bool { - if (! $user instanceof User) { - $user = $this->repository->find($user); - } + $window = $this->config->get('pterodactyl.auth.2fa.window'); + $secret = $this->encrypter->decrypt($user->totp_secret); - if (! $this->google2FA->verifyKey($user->totp_secret, $token, 2)) { + $isValidToken = $this->google2FA->verifyKey($secret, $token, $window); + + if (! $isValidToken) { throw new TwoFactorAuthenticationTokenInvalid; } $this->repository->withoutFresh()->update($user->id, [ + 'totp_authenticated_at' => Carbon::now(), 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), ]); diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index 608a3643..a8554ccf 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -10,7 +10,8 @@ namespace Pterodactyl\Services\Users; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; @@ -19,58 +20,62 @@ class TwoFactorSetupService /** * @var \Illuminate\Contracts\Config\Repository */ - protected $config; + private $config; /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Encryption\Encrypter */ - protected $google2FA; + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $repository; + private $repository; /** * TwoFactorSetupService constructor. * * @param \Illuminate\Contracts\Config\Repository $config - * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( ConfigRepository $config, + Encrypter $encrypter, Google2FA $google2FA, UserRepositoryInterface $repository ) { $this->config = $config; + $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; } /** - * Generate a 2FA token and store it in the database. + * Generate a 2FA token and store it in the database before returning the + * QR code image. * - * @param int|\Pterodactyl\Models\User $user - * @return array + * @param \Pterodactyl\Models\User $user + * @return string * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($user) + public function handle(User $user): string { - if (! $user instanceof User) { - $user = $this->repository->find($user); - } - - $secret = $this->google2FA->generateSecretKey(); + $secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes')); $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); - $this->repository->withoutFresh()->update($user->id, ['totp_secret' => $secret]); + $this->repository->withoutFresh()->update($user->id, [ + 'totp_secret' => $this->encrypter->encrypt($secret), + ]); - return [ - 'qrImage' => $image, - 'secret' => $secret, - ]; + return $image; } } diff --git a/composer.json b/composer.json index 58903551..fabe01f0 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "mtdowling/cron-expression": "^1.2", "nesbot/carbon": "^1.22", "nicolaslopezj/searchable": "^1.9", - "pragmarx/google2fa": "^1.0", + "pragmarx/google2fa": "^2.0", "predis/predis": "^1.1", "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", @@ -46,7 +46,7 @@ "require-dev": { "barryvdh/laravel-debugbar": "^2.4", "barryvdh/laravel-ide-helper": "^2.4", - "friendsofphp/php-cs-fixer": "^2.4", + "friendsofphp/php-cs-fixer": "^2.8.0", "fzaninotto/faker": "^1.6", "mockery/mockery": "^0.9", "php-mock/php-mock-phpunit": "^1.1", diff --git a/composer.lock b/composer.lock index 2979fad3..9895b833 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": "3758867d4fb2d20e4b4e45b7c410f79b", + "content-hash": "a393763d136e25a93fd5b636229496cf", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -61,16 +61,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.36.37", + "version": "3.38.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9" + "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a6d7fd9f32c63d018a6603a36174b4cb971fccd9", - "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9f704274f4748d2039a16d45b3388ed8dde74e89", + "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89", "shasum": "" }, "require": { @@ -137,61 +137,7 @@ "s3", "sdk" ], - "time": "2017-11-03T16:39:35+00:00" - }, - { - "name": "christian-riesen/base32", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/ChristianRiesen/base32.git", - "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa", - "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "0.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Base32\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Riesen", - "email": "chris.riesen@gmail.com", - "homepage": "http://christianriesen.com", - "role": "Developer" - } - ], - "description": "Base32 encoder/decoder according to RFC 4648", - "homepage": "https://github.com/ChristianRiesen/base32", - "keywords": [ - "base32", - "decode", - "encode", - "rfc4648" - ], - "time": "2016-05-05T11:49:03+00:00" + "time": "2017-11-09T19:15:59+00:00" }, { "name": "daneeveritt/login-notifications", @@ -2055,6 +2001,68 @@ ], "time": "2017-11-04T11:48:34+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4", + "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "vimeo/psalm": "^0.3|^1" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "time": "2017-09-22T14:55:37+00:00" + }, { "name": "paragonie/random_compat", "version": "v2.0.11", @@ -2105,26 +2113,28 @@ }, { "name": "pragmarx/google2fa", - "version": "v1.0.1", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "b346dc138339b745c5831405d00cff7c1351aa0d" + "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/b346dc138339b745c5831405d00cff7c1351aa0d", - "reference": "b346dc138339b745c5831405d00cff7c1351aa0d", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/bc2d654305e4d09254125f8cd390a7fbc4742d46", + "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46", "shasum": "" }, "require": { - "christian-riesen/base32": "~1.3", + "paragonie/constant_time_encoding": "~1.0|~2.0", "paragonie/random_compat": "~1.4|~2.0", "php": ">=5.4", "symfony/polyfill-php56": "~1.2" }, "require-dev": { - "phpspec/phpspec": "~2.1" + "bacon/bacon-qr-code": "~1.0", + "phpspec/phpspec": "~2.1", + "phpunit/phpunit": "~4" }, "suggest": { "bacon/bacon-qr-code": "Required to generate inline QR Codes." @@ -2132,11 +2142,8 @@ "type": "library", "extra": { "component": "package", - "frameworks": [ - "Laravel" - ], "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2157,12 +2164,13 @@ ], "description": "A One Time Password Authentication package, compatible with Google Authenticator.", "keywords": [ + "2fa", "Authentication", "Two Factor Authentication", "google2fa", "laravel" ], - "time": "2016-07-18T20:25:04+00:00" + "time": "2017-09-12T06:55:05+00:00" }, { "name": "predis/predis", @@ -3796,16 +3804,16 @@ }, { "name": "watson/validating", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/dwightwatson/validating.git", - "reference": "ade13078bf2e820e244603446114a28eda51b08c" + "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/ade13078bf2e820e244603446114a28eda51b08c", - "reference": "ade13078bf2e820e244603446114a28eda51b08c", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/22edd06d45893f5d4f79c9e901bd7fbce174a79f", + "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f", "shasum": "" }, "require": { @@ -3842,7 +3850,7 @@ "laravel", "validation" ], - "time": "2017-10-08T22:42:01+00:00" + "time": "2017-11-06T21:35:49+00:00" }, { "name": "webmozart/assert", @@ -4291,16 +4299,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.8.0", + "version": "v2.8.1", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6" + "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/89e7b083f27241e03dd776cb8d6781c77e341db6", - "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/04f71e56e03ba2627e345e8c949c80dcef0e683e", + "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e", "shasum": "" }, "require": { @@ -4367,7 +4375,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-11-03T02:21:46+00:00" + "time": "2017-11-09T13:31:39+00:00" }, { "name": "fzaninotto/faker", @@ -4421,23 +4429,23 @@ }, { "name": "gecko-packages/gecko-php-unit", - "version": "v2.2", + "version": "v3.0", "source": { "type": "git", "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1" + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/6a866551dffc2154c1b091bae3a7877d39c25ca3", + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3", "shasum": "" }, "require": { - "php": "^5.3.6 || ^7.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-dom": "When testing with xml.", @@ -4445,6 +4453,11 @@ "phpunit/phpunit": "This is an extension for it so make sure you have it some way." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { "psr-4": { "GeckoPackages\\PHPUnit\\": "src/PHPUnit" @@ -4461,7 +4474,7 @@ "filesystem", "phpunit" ], - "time": "2017-08-23T07:39:54+00:00" + "time": "2017-08-23T07:46:41+00:00" }, { "name": "hamcrest/hamcrest-php", diff --git a/config/app.php b/config/app.php index c193e42e..2f9da670 100644 --- a/config/app.php +++ b/config/app.php @@ -171,7 +171,6 @@ return [ /* * Additional Dependencies */ - PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider::class, igaster\laravelTheme\themeServiceProvider::class, Prologue\Alerts\AlertsServiceProvider::class, Krucas\Settings\Providers\SettingsServiceProvider::class, @@ -213,7 +212,6 @@ return [ 'File' => Illuminate\Support\Facades\File::class, 'Fractal' => Spatie\Fractal\FractalFacade::class, 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Google2FA' => PragmaRX\Google2FA\Vendor\Laravel\Facade::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Input' => Illuminate\Support\Facades\Input::class, 'Inspiring' => Illuminate\Foundation\Inspiring::class, diff --git a/config/pterodactyl.php b/config/pterodactyl.php index bd157df2..ad371bce 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -23,6 +23,11 @@ return [ */ 'auth' => [ 'notifications' => env('LOGIN_NOTIFICATIONS', false), + '2fa' => [ + 'bytes' => 32, + 'window' => env('APP_2FA_WINDOW', 4), + 'verify_newer' => true, + ], ], /* diff --git a/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php new file mode 100644 index 00000000..53cb6526 --- /dev/null +++ b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php @@ -0,0 +1,60 @@ +text('totp_secret')->nullable()->change(); + $table->timestampTz('totp_authenticated_at')->after('totp_secret')->nullable(); + }); + + DB::transaction(function () { + DB::table('users')->get()->each(function ($user) { + if (is_null($user->totp_secret)) { + return; + } + + DB::table('users')->where('id', $user->id)->update([ + 'totp_secret' => Crypt::encrypt($user->totp_secret), + 'updated_at' => Carbon::now()->toIso8601String(), + ]); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + DB::transaction(function () { + DB::table('users')->get()->each(function ($user) { + if (is_null($user->totp_secret)) { + return; + } + + DB::table('users')->where('id', $user->id)->update([ + 'totp_secret' => Crypt::decrypt($user->totp_secret), + 'updated_at' => Carbon::now()->toIso8601String(), + ]); + }); + }); + + DB::statement('ALTER TABLE users MODIFY totp_secret CHAR(16) DEFAULT NULL'); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('totp_authenticated_at'); + }); + } +} diff --git a/public/themes/pterodactyl/js/frontend/2fa-modal.js b/public/themes/pterodactyl/js/frontend/2fa-modal.js index 022ece2f..d542b377 100644 --- a/public/themes/pterodactyl/js/frontend/2fa-modal.js +++ b/public/themes/pterodactyl/js/frontend/2fa-modal.js @@ -42,7 +42,6 @@ var TwoFactorModal = (function () { $('#qr_image_insert').attr('src', image.src).slideDown(); }); }); - $('#2fa_secret_insert').html(data.secret); $('#open2fa').modal('show'); }).fail(function (jqXHR) { alert('An error occured while attempting to load the 2FA setup modal. Please try again.'); diff --git a/resources/themes/pterodactyl/base/security.blade.php b/resources/themes/pterodactyl/base/security.blade.php index a3a6cc51..7c4693dd 100644 --- a/resources/themes/pterodactyl/base/security.blade.php +++ b/resources/themes/pterodactyl/base/security.blade.php @@ -106,8 +106,8 @@
-
-
Loading QR Code...
+
+ Loading QR Code...
@lang('base.security.2fa_checkpoint_help')
diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php index 727f2ab5..3c821729 100644 --- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php @@ -1,69 +1,41 @@ . - * - * 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 Prologue\Alerts\AlertsMessageBag; -use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Config\Repository; -use Tests\Assertions\ControllerAssertionsTrait; +use Tests\Unit\Http\Controllers\ControllerTestCase; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Services\Users\ToggleTwoFactorService; use Pterodactyl\Http\Controllers\Base\SecurityController; use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -class SecurityControllerTest extends TestCase +class SecurityControllerTest extends ControllerTestCase { - use ControllerAssertionsTrait; - /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ protected $alert; /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ protected $config; /** - * @var \Pterodactyl\Http\Controllers\Base\SecurityController - */ - protected $controller; - - /** - * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface|\Mockery\Mock */ protected $repository; /** - * @var \Illuminate\Http\Request - */ - protected $request; - - /** - * @var \Illuminate\Contracts\Session\Session - */ - protected $session; - - /** - * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + * @var \Pterodactyl\Services\Users\ToggleTwoFactorService|\Mockery\Mock */ protected $toggleTwoFactorService; /** - * @var \Pterodactyl\Services\Users\TwoFactorSetupService + * @var \Pterodactyl\Services\Users\TwoFactorSetupService|\Mockery\Mock */ protected $twoFactorSetupService; @@ -77,19 +49,8 @@ class SecurityControllerTest extends TestCase $this->alert = m::mock(AlertsMessageBag::class); $this->config = m::mock(Repository::class); $this->repository = m::mock(SessionRepositoryInterface::class); - $this->request = m::mock(Request::class); - $this->session = m::mock(Session::class); $this->toggleTwoFactorService = m::mock(ToggleTwoFactorService::class); $this->twoFactorSetupService = m::mock(TwoFactorSetupService::class); - - $this->controller = new SecurityController( - $this->alert, - $this->config, - $this->session, - $this->repository, - $this->toggleTwoFactorService, - $this->twoFactorSetupService - ); } /** @@ -97,13 +58,12 @@ class SecurityControllerTest extends TestCase */ public function testIndexControllerWithDatabaseDriver() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('database'); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('getUserSessions')->with($model->id)->once()->andReturn(['sessions']); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.security', $response); $this->assertViewHasKey('sessions', $response); @@ -117,7 +77,7 @@ class SecurityControllerTest extends TestCase { $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('redis'); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.security', $response); $this->assertViewHasKey('sessions', $response); @@ -129,14 +89,13 @@ class SecurityControllerTest extends TestCase */ public function testGenerateTotpController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); - $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn(['string']); + $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn('qrCodeImage'); - $response = $this->controller->generateTotp($this->request); + $response = $this->getController()->generateTotp($this->request); $this->assertIsJsonResponse($response); - $this->assertResponseJsonEquals(['string'], $response); + $this->assertResponseJsonEquals(['qrImage' => 'qrCodeImage'], $response); } /** @@ -144,13 +103,12 @@ class SecurityControllerTest extends TestCase */ public function testDisableTotpControllerSuccess() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andReturnNull(); - $response = $this->controller->disableTotp($this->request); + $response = $this->getController()->disableTotp($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } @@ -160,16 +118,14 @@ class SecurityControllerTest extends TestCase */ public function testDisableTotpControllerWhenExceptionIsThrown() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); - $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once() - ->andThrow(new TwoFactorAuthenticationTokenInvalid); - $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf() - ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andThrow(new TwoFactorAuthenticationTokenInvalid); + $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf(); + $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); - $response = $this->controller->disableTotp($this->request); + $response = $this->getController()->disableTotp($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } @@ -179,13 +135,28 @@ class SecurityControllerTest extends TestCase */ public function testRevokeController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('deleteUserSession')->with($model->id, 123)->once()->andReturnNull(); - $response = $this->controller->revoke($this->request, 123); + $response = $this->getController()->revoke($this->request, 123); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } + + /** + * Return an instance of the controller for testing with mocked dependencies. + * + * @return \Pterodactyl\Http\Controllers\Base\SecurityController + */ + private function getController(): SecurityController + { + return new SecurityController( + $this->alert, + $this->config, + $this->repository, + $this->toggleTwoFactorService, + $this->twoFactorSetupService + ); + } } diff --git a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php index 176eb4d8..c72ab33b 100644 --- a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php +++ b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php @@ -64,7 +64,7 @@ class RunTaskJobTest extends TestCase { parent::setUp(); Bus::fake(); - Carbon::setTestNow(); + Carbon::setTestNow(Carbon::now()); $this->commandRepository = m::mock(CommandRepositoryInterface::class); $this->config = m::mock(Repository::class); diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php index 7c240b08..87d5f506 100644 --- a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php +++ b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php @@ -44,7 +44,7 @@ class DaemonKeyProviderServiceTest extends TestCase public function setUp() { parent::setUp(); - Carbon::setTestNow(); + Carbon::setTestNow(Carbon::now()); $this->keyCreationService = m::mock(DaemonKeyCreationService::class); $this->keyUpdateService = m::mock(DaemonKeyUpdateService::class); diff --git a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php index ae45ec8f..c8d1cc85 100644 --- a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php +++ b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php @@ -1,37 +1,42 @@ . - * - * 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 Carbon\Carbon; use Tests\TestCase; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Users\ToggleTwoFactorService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class ToggleTwoFactorServiceTest extends TestCase { - /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA - */ - protected $google2FA; + const TEST_WINDOW_INT = 4; + const USER_TOTP_SECRET = 'encryptedValue'; + const DECRYPTED_USER_SECRET = 'decryptedValue'; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $repository; + private $config; /** - * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock */ - protected $service; + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock + */ + private $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock + */ + private $repository; /** * Setup tests. @@ -39,11 +44,15 @@ class ToggleTwoFactorServiceTest extends TestCase public function setUp() { parent::setUp(); + Carbon::setTestNow(Carbon::now()); + $this->config = m::mock(Repository::class); + $this->encrypter = m::mock(Encrypter::class); $this->google2FA = m::mock(Google2FA::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new ToggleTwoFactorService($this->google2FA, $this->repository); + $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.window')->once()->andReturn(self::TEST_WINDOW_INT); + $this->encrypter->shouldReceive('decrypt')->with(self::USER_TOTP_SECRET)->once()->andReturn(self::DECRYPTED_USER_SECRET); } /** @@ -51,13 +60,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorIsEnabledForUser() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => true])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => true, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token')); + $this->assertTrue($this->getService()->handle($model, 'test-token')); } /** @@ -65,13 +76,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorIsDisabled() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => true]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => true]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => false, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token')); + $this->assertTrue($this->getService()->handle($model, 'test-token')); } /** @@ -79,13 +92,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorRemainsDisabledForUser() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => false, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token', false)); + $this->assertTrue($this->getService()->handle($model, 'test-token', false)); } /** @@ -95,23 +110,19 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testExceptionIsThrownIfTokenIsInvalid() { - $model = factory(User::class)->make(); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET]); $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(false); - $this->service->handle($model, 'test-token'); + $this->getService()->handle($model, 'test-token'); } /** - * Test that an integer can be passed in place of a user model. + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Users\ToggleTwoFactorService */ - public function testIntegerCanBePassedInPlaceOfUserModel() + private function getService(): ToggleTwoFactorService { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); - - $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); - $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); - - $this->assertTrue($this->service->handle($model->id, 'test-token')); + return new ToggleTwoFactorService($this->encrypter, $this->google2FA, $this->config, $this->repository); } } diff --git a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php index e58d99f2..d6f5f8b9 100644 --- a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php +++ b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php @@ -1,43 +1,37 @@ . - * - * 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 PragmaRX\Google2FA\Google2FA; use Illuminate\Contracts\Config\Repository; -use PragmaRX\Google2FA\Contracts\Google2FA; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class TwoFactorSetupServiceTest extends TestCase { /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $config; + private $config; /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock */ - protected $google2FA; + private $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock */ - protected $repository; + private $google2FA; /** - * @var \Pterodactyl\Services\Users\TwoFactorSetupService + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ - protected $service; + private $repository; /** * Setup tests. @@ -47,10 +41,9 @@ class TwoFactorSetupServiceTest extends TestCase parent::setUp(); $this->config = m::mock(Repository::class); + $this->encrypter = m::mock(Encrypter::class); $this->google2FA = m::mock(Google2FA::class); $this->repository = m::mock(UserRepositoryInterface::class); - - $this->service = new TwoFactorSetupService($this->config, $this->google2FA, $this->repository); } /** @@ -60,34 +53,25 @@ class TwoFactorSetupServiceTest extends TestCase { $model = factory(User::class)->make(); - $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturn('secretKey'); + $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes')->once()->andReturn(32); + $this->google2FA->shouldReceive('generateSecretKey')->with(32)->once()->andReturn('secretKey'); $this->config->shouldReceive('get')->with('app.name')->once()->andReturn('CompanyName'); - $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey') - ->once()->andReturn('http://url.com'); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['totp_secret' => 'secretKey'])->once()->andReturnNull(); + $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey')->once()->andReturn('http://url.com'); + $this->encrypter->shouldReceive('encrypt')->with('secretKey')->once()->andReturn('encryptedSecret'); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, ['totp_secret' => 'encryptedSecret'])->once()->andReturnNull(); - $response = $this->service->handle($model); + $response = $this->getService()->handle($model); $this->assertNotEmpty($response); - $this->assertArrayHasKey('qrImage', $response); - $this->assertArrayHasKey('secret', $response); - $this->assertEquals('http://url.com', $response['qrImage']); - $this->assertEquals('secretKey', $response['secret']); + $this->assertSame('http://url.com', $response); } /** - * Test that an integer can be passed in place of the user model. + * Return an instance of the service to test with mocked dependencies. + * + * @return \Pterodactyl\Services\Users\TwoFactorSetupService */ - public function testIntegerCanBePassedInPlaceOfUserModel() + private function getService(): TwoFactorSetupService { - $model = factory(User::class)->make(); - - $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); - $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturnNull(); - $this->config->shouldReceive('get')->with('app.name')->once()->andReturnNull(); - $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); - - $this->assertTrue(is_array($this->service->handle($model->id))); + return new TwoFactorSetupService($this->config, $this->encrypter, $this->google2FA, $this->repository); } }