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); } }