Implement changes to administrative user revocation, closes #733

This commit is contained in:
Dane Everitt 2017-12-03 14:00:47 -06:00
parent 20beb2f280
commit 975597b4d0
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
19 changed files with 458 additions and 125 deletions

View file

@ -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]` — 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]` — 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 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) ## v0.7.0-beta.2 (Derelict Dermodactylus)
### Fixed ### Fixed

View file

@ -78,8 +78,10 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface
/** /**
* Revoke an access key on the daemon before the time is expired. * Revoke an access key on the daemon before the time is expired.
* *
* @param string $key * @param string|array $key
* @return \Psr\Http\Message\ResponseInterface * @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\RequestException
*/ */
public function revokeAccessKey($key); public function revokeAccessKey($key);
} }

View file

@ -24,7 +24,9 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\User;
use Pterodactyl\Models\DaemonKey; use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
interface DaemonKeyRepositoryInterface extends RepositoryInterface interface DaemonKeyRepositoryInterface extends RepositoryInterface
{ {
@ -59,4 +61,22 @@ interface DaemonKeyRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function getKeyWithServer($key); 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);
} }

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Http\Connection; namespace Pterodactyl\Exceptions\Http\Connection;

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
@ -160,10 +153,30 @@ class UserController extends Controller
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/ */
public function update(UserFormRequest $request, User $user) 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(); $this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash();
return redirect()->route('admin.users.view', $user->id); return redirect()->route('admin.users.view', $user->id);

View file

@ -1,30 +1,8 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>
* Some Modifications (c) 2015 Dylan Seidt <dylan.seidt@gmail.com>.
*
* 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; namespace Pterodactyl\Http\Controllers\Base;
use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
@ -48,10 +26,8 @@ class AccountController extends Controller
* @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService * @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/ */
public function __construct( public function __construct(AlertsMessageBag $alert, UserUpdateService $updateService)
AlertsMessageBag $alert, {
UserUpdateService $updateService
) {
$this->alert = $alert; $this->alert = $alert;
$this->updateService = $updateService; $this->updateService = $updateService;
} }
@ -74,6 +50,7 @@ class AccountController extends Controller
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/ */
public function update(AccountDataFormRequest $request) public function update(AccountDataFormRequest $request)
{ {
@ -86,7 +63,8 @@ class AccountController extends Controller
$data = $request->only(['name_first', 'name_last', 'username']); $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(); $this->alert->success(trans('base.account.details_updated'))->flash();
return redirect()->route('account'); return redirect()->route('account');

View file

@ -21,6 +21,8 @@ class AdminAuthenticate
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -46,6 +46,8 @@ class DaemonAuthenticate
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -47,9 +47,8 @@ class AuthenticateAsSubuser
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* *
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -19,7 +19,11 @@ class UserFormRequest extends AdminFormRequest
public function rules() public function rules()
{ {
if ($this->method() === 'PATCH') { 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(); return User::getCreateRules();
@ -30,7 +34,7 @@ class UserFormRequest extends AdminFormRequest
if ($this->method === 'PATCH') { if ($this->method === 'PATCH') {
return array_merge( return array_merge(
$this->intersect('password'), $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'])
); );
} }

View file

@ -107,7 +107,13 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa
*/ */
public function revokeAccessKey($key) 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); return $this->getHttpClient()->request('DELETE', 'keys/' . $key);
} }

View file

@ -24,8 +24,10 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\DaemonKey; use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
@ -83,4 +85,28 @@ class DaemonKeyRepository extends EloquentRepository implements DaemonKeyReposit
return $instance; 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();
}
} }

View file

@ -0,0 +1,91 @@
<?php
namespace Pterodactyl\Services\DaemonKeys;
use Pterodactyl\Models\User;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepository;
class RevokeMultipleDaemonKeysService
{
/**
* @var array
*/
protected $exceptions = [];
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
private $repository;
/**
* RevokeMultipleDaemonKeysService constructor.
*
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository
*/
public function __construct(
DaemonKeyRepositoryInterface $repository,
DaemonServerRepository $daemonRepository
) {
$this->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;
}
}

View file

@ -1,59 +1,79 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Users; namespace Pterodactyl\Services\Users;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
class UserUpdateService class UserUpdateService
{ {
use HasUserLevels;
/** /**
* @var \Illuminate\Contracts\Hashing\Hasher * @var \Illuminate\Contracts\Hashing\Hasher
*/ */
protected $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/ */
protected $repository; private $repository;
/**
* @var \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService
*/
private $revocationService;
/** /**
* UpdateService constructor. * UpdateService constructor.
* *
* @param \Illuminate\Contracts\Hashing\Hasher $hasher * @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService $revocationService
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
Hasher $hasher, Hasher $hasher,
RevokeMultipleDaemonKeysService $revocationService,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->hasher = $hasher; $this->hasher = $hasher;
$this->repository = $repository; $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 \Pterodactyl\Models\User $user
* @param array $data * @param array $data
* @return mixed * @return \Illuminate\Support\Collection
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @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']); $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(),
]);
} }
} }

View file

@ -59,4 +59,7 @@ return [
'locations' => [ 'locations' => [
'has_nodes' => 'Cannot delete a location that has active nodes attached to it.', 'has_nodes' => 'Cannot delete a location that has active nodes attached to it.',
], ],
'users' => [
'node_revocation_failed' => 'Failed to revoke keys on <a href=":link">Node #:node</a>. :error',
],
]; ];

View file

@ -66,10 +66,11 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="alert alert-success" style="display:none;margin-bottom:10px;" id="gen_pass"></div> <div class="alert alert-success" style="display:none;margin-bottom:10px;" id="gen_pass"></div>
<div class="form-group"> <div class="form-group no-margin-bottom">
<label for="password" class="control-label">Password</label> <label for="password" class="control-label">Password <span class="field-optional"></span></label>
<div> <div>
<input readonly type="password" id="password" name="password" class="form-control form-autocomplete-stop"> <input readonly type="password" id="password" name="password" class="form-control form-autocomplete-stop">
<p class="text-muted small">Leave blank to keep this user's password the same. User will not receive any notification if password is changed.</p>
</div> </div>
</div> </div>
</div> </div>
@ -90,6 +91,11 @@
</select> </select>
<p class="text-muted"><small>Setting this to 'Yes' gives a user full administrative access.</small></p> <p class="text-muted"><small>Setting this to 'Yes' gives a user full administrative access.</small></p>
</div> </div>
<div class="checkbox checkbox-primary">
<input type="checkbox" id="pIgnoreConnectionError" value="1" name="ignore_connection_error">
<label for="pIgnoreConnectionError"> Ignore exceptions raised while revoking keys.</label>
<p class="text-muted small">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.</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,43 +1,24 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Http\Controllers\Base; namespace Tests\Unit\Http\Controllers\Base;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Tests\Assertions\ControllerAssertionsTrait;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Tests\Unit\Http\Controllers\ControllerTestCase;
use Pterodactyl\Http\Controllers\Base\AccountController; use Pterodactyl\Http\Controllers\Base\AccountController;
use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; 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; protected $alert;
/** /**
* @var \Pterodactyl\Http\Controllers\Base\AccountController * @var \Pterodactyl\Services\Users\UserUpdateService|\Mockery\Mock
*/
protected $controller;
/**
* @var \Pterodactyl\Http\Requests\Base\AccountDataFormRequest
*/
protected $request;
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/ */
protected $updateService; protected $updateService;
@ -49,10 +30,7 @@ class AccountControllerTest extends TestCase
parent::setUp(); parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class); $this->alert = m::mock(AlertsMessageBag::class);
$this->request = m::mock(AccountDataFormRequest::class);
$this->updateService = m::mock(UserUpdateService::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() public function testIndexController()
{ {
$response = $this->controller->index(); $response = $this->getController()->index();
$this->assertIsViewResponse($response); $this->assertIsViewResponse($response);
$this->assertViewNameEquals('base.account', $response); $this->assertViewNameEquals('base.account', $response);
@ -71,14 +49,17 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForPassword() public function testUpdateControllerForPassword()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$this->request->shouldReceive('input')->with('do_action')->andReturn('password'); $this->request->shouldReceive('input')->with('do_action')->andReturn('password');
$this->request->shouldReceive('input')->with('new_password')->once()->andReturn('test-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('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['password' => 'test-password'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['password' => 'test-password'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->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->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $response); $this->assertRedirectRouteEquals('account', $response);
} }
@ -88,14 +69,17 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForEmail() public function testUpdateControllerForEmail()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$this->request->shouldReceive('input')->with('do_action')->andReturn('email'); $this->request->shouldReceive('input')->with('do_action')->andReturn('email');
$this->request->shouldReceive('input')->with('new_email')->once()->andReturn('test@example.com'); $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('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['email' => 'test@example.com'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['email' => 'test@example.com'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->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->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $response); $this->assertRedirectRouteEquals('account', $response);
} }
@ -105,17 +89,30 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForIdentity() public function testUpdateControllerForIdentity()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$this->request->shouldReceive('input')->with('do_action')->andReturn('identity'); $this->request->shouldReceive('input')->with('do_action')->andReturn('identity');
$this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([ $this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([
'test_data' => 'value', 'test_data' => 'value',
]); ]);
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['test_data' => 'value'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['test_data' => 'value'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->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->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $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);
}
} }

View file

@ -0,0 +1,115 @@
<?php
namespace Tests\Unit\Services\DaemonKeys;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\DaemonKey;
use Tests\Traits\MocksRequestException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface;
class RevokeMultipleDaemonKeysServiceTest extends TestCase
{
use MocksRequestException;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->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);
}
}

View file

@ -1,36 +1,32 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Users; namespace Tests\Unit\Services\Users;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
class UserUpdateServiceTest extends TestCase 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. * Setup tests.
@ -41,8 +37,7 @@ class UserUpdateServiceTest extends TestCase
$this->hasher = m::mock(Hasher::class); $this->hasher = m::mock(Hasher::class);
$this->repository = m::mock(UserRepositoryInterface::class); $this->repository = m::mock(UserRepositoryInterface::class);
$this->revocationService = m::mock(RevokeMultipleDaemonKeysService::class);
$this->service = new UserUpdateService($this->hasher, $this->repository);
} }
/** /**
@ -50,9 +45,14 @@ class UserUpdateServiceTest extends TestCase
*/ */
public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed() 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() public function testUpdateUserAndHashPasswordIfProvided()
{ {
$user = factory(User::class)->make();
$this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass'); $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);
} }
} }