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

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

View file

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

View file

@ -1,11 +1,4 @@
<?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;

View file

@ -1,11 +1,4 @@
<?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;
@ -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);

View file

@ -1,30 +1,8 @@
<?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;
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');

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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'])
);
}

View file

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

View file

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

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
/**
* 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;
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(),
]);
}
}