Simplify handling of permissions for websocket, only send permissions the user actually has

This commit is contained in:
Dane Everitt 2020-04-06 21:03:00 -07:00
parent a924eb56cc
commit b1e7e0b8b0
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
2 changed files with 38 additions and 27 deletions

View file

@ -2,15 +2,14 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers; namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Cake\Chronos\Chronos; use Carbon\CarbonImmutable;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Permission; use Pterodactyl\Models\Permission;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Cache\Repository;
use Pterodactyl\Services\Nodes\NodeJWTService;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
@ -22,16 +21,23 @@ class WebsocketController extends ClientApiController
*/ */
private $cache; private $cache;
/**
* @var \Pterodactyl\Services\Nodes\NodeJWTService
*/
private $jwtService;
/** /**
* WebsocketController constructor. * WebsocketController constructor.
* *
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
* @param \Illuminate\Contracts\Cache\Repository $cache * @param \Illuminate\Contracts\Cache\Repository $cache
*/ */
public function __construct(Repository $cache) public function __construct(NodeJWTService $jwtService, Repository $cache)
{ {
parent::__construct(); parent::__construct();
$this->cache = $cache; $this->cache = $cache;
$this->jwtService = $jwtService;
} }
/** /**
@ -46,30 +52,35 @@ class WebsocketController extends ClientApiController
*/ */
public function __invoke(ClientApiRequest $request, Server $server) public function __invoke(ClientApiRequest $request, Server $server)
{ {
if ($request->user()->cannot(Permission::ACTION_WEBSOCKET, $server)) { $user = $request->user();
throw new HttpException( if ($user->cannot(Permission::ACTION_WEBSOCKET, $server)) {
Response::HTTP_FORBIDDEN, 'You do not have permission to connect to this server\'s websocket.' throw new HttpException(Response::HTTP_FORBIDDEN, 'You do not have permission to connect to this server\'s websocket.');
);
} }
$now = Chronos::now(); if ($user->root_admin || $user->id === $server->owner_id) {
$permissions = ['*'];
$signer = new Sha256; if ($user->root_admin) {
$permissions[] = 'admin.errors';
$permissions[] = 'admin.install';
}
} else {
/** @var \Pterodactyl\Models\Subuser|null $subuserPermissions */
$subuserPermissions = $server->subusers->first(function (Subuser $subuser) use ($user) {
return $subuser->user_id === $user->id;
});
$token = (new Builder)->issuedBy(config('app.url')) $permissions = $subuserPermissions ? $subuserPermissions->permissions : [];
->permittedFor($server->node->getConnectionAddress()) }
->identifiedBy(hash('sha256', $request->user()->id . $server->uuid), true)
->issuedAt($now->getTimestamp()) $token = $this->jwtService
->canOnlyBeUsedAfter($now->getTimestamp()) ->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->expiresAt($now->addMinutes(15)->getTimestamp()) ->setClaims([
->withClaim('user_id', $request->user()->id) 'user_id' => $request->user()->id,
->withClaim('server_uuid', $server->uuid) 'server_uuid' => $server->uuid,
->withClaim('permissions', array_merge([ 'permissions' => $permissions ?? [],
'connect', ])
'send-command', ->handle($server->node, $user->id . $server->uuid);
'send-power',
], $request->user()->root_admin ? ['receive-errors', 'receive-install'] : []))
->getToken($signer, new Key($server->node->daemonSecret));
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress()); $socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());

View file

@ -20,7 +20,7 @@ export class Websocket extends EventEmitter {
// refreshed at a pretty continuous interval. The socket server will respond // refreshed at a pretty continuous interval. The socket server will respond
// with "token expiring" and "token expired" events when approaching 3 minutes // with "token expiring" and "token expired" events when approaching 3 minutes
// and 0 minutes to expiry. // and 0 minutes to expiry.
private token: string = ''; private token = '';
// Connects to the websocket instance and sets the token for the initial request. // Connects to the websocket instance and sets the token for the initial request.
connect (url: string): this { connect (url: string): this {
@ -28,7 +28,7 @@ export class Websocket extends EventEmitter {
this.socket = new Sockette(`${this.url}`, { this.socket = new Sockette(`${this.url}`, {
onmessage: e => { onmessage: e => {
try { try {
let { event, args } = JSON.parse(e.data); const { event, args } = JSON.parse(e.data);
args ? this.emit(event, ...args) : this.emit(event); args ? this.emit(event, ...args) : this.emit(event);
} catch (ex) { } catch (ex) {
console.warn('Failed to parse incoming websocket message.', ex); console.warn('Failed to parse incoming websocket message.', ex);