From 906a699ee2070d526a8ba5e82b2ac7ed137603cd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 23 Sep 2017 20:45:25 -0500 Subject: [PATCH] Begin implementation of new daemon authentication scheme --- .../DaemonKeyRepositoryInterface.php | 46 ++++++ .../Repository/SubuserRepositoryInterface.php | 22 +++ app/Exceptions/Handler.php | 15 +- .../API/Remote/ValidateKeyController.php | 90 +++++++++++ app/Http/Kernel.php | 8 +- .../Middleware/Daemon/DaemonAuthenticate.php | 82 ++++++++++ app/Http/Middleware/Server/ScheduleAccess.php | 4 +- app/Http/Middleware/Server/SubuserAccess.php | 1 + app/Http/Middleware/ServerAuthenticate.php | 2 + app/Models/DaemonKey.php | 103 +++++++++++++ app/Models/Server.php | 145 ++++-------------- app/Models/Subuser.php | 9 -- app/Providers/AppServiceProvider.php | 3 + app/Providers/RepositoryServiceProvider.php | 3 + app/Providers/RouteServiceProvider.php | 6 +- .../Eloquent/DaemonKeyRepository.php | 66 ++++++++ .../Eloquent/SubuserRepository.php | 40 +++++ .../Servers/ServerAccessHelperService.php | 52 +++++-- app/Transformers/Daemon/ApiKeyTransformer.php | 82 ++++++++++ ...017_09_23_170933_CreateDaemonKeysTable.php | 35 +++++ ...628_RemoveDaemonSecretFromServersTable.php | 51 ++++++ ...22_RemoveDaemonSecretFromSubusersTable.php | 52 +++++++ routes/api-remote.php | 24 +++ 23 files changed, 796 insertions(+), 145 deletions(-) create mode 100644 app/Contracts/Repository/DaemonKeyRepositoryInterface.php create mode 100644 app/Http/Controllers/API/Remote/ValidateKeyController.php create mode 100644 app/Http/Middleware/Daemon/DaemonAuthenticate.php create mode 100644 app/Models/DaemonKey.php create mode 100644 app/Repositories/Eloquent/DaemonKeyRepository.php create mode 100644 app/Transformers/Daemon/ApiKeyTransformer.php create mode 100644 database/migrations/2017_09_23_170933_CreateDaemonKeysTable.php create mode 100644 database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php create mode 100644 database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php create mode 100644 routes/api-remote.php diff --git a/app/Contracts/Repository/DaemonKeyRepositoryInterface.php b/app/Contracts/Repository/DaemonKeyRepositoryInterface.php new file mode 100644 index 000000000..fff8f72f5 --- /dev/null +++ b/app/Contracts/Repository/DaemonKeyRepositoryInterface.php @@ -0,0 +1,46 @@ +. + * + * 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\Contracts\Repository; + +interface DaemonKeyRepositoryInterface extends RepositoryInterface +{ + /** + * Gets the daemon keys associated with a specific server. + * + * @param int $server + * @return \Illuminate\Support\Collection + */ + public function getServerKeys($server); + + /** + * Return a daemon key with the associated server relation attached. + * + * @param string $key + * @return \Pterodactyl\Models\DaemonKey + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getKeyWithServer($key); +} diff --git a/app/Contracts/Repository/SubuserRepositoryInterface.php b/app/Contracts/Repository/SubuserRepositoryInterface.php index 6d6889fe9..9ea9f7b0b 100644 --- a/app/Contracts/Repository/SubuserRepositoryInterface.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -46,6 +46,17 @@ interface SubuserRepositoryInterface extends RepositoryInterface */ public function getWithPermissions($id); + /** + * Return a subuser and associated permissions given a user_id and server_id. + * + * @param int $user + * @param int $server + * @return \Pterodactyl\Models\Subuser + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithPermissionsUsingUserAndServer($user, $server); + /** * Find a subuser and return with server and permissions relationships. * @@ -55,4 +66,15 @@ interface SubuserRepositoryInterface extends RepositoryInterface * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getWithServerAndPermissions($id); + + /** + * Return a subuser and their associated connection key for a server. + * + * @param int $user + * @param int $server + * @return \Pterodactyl\Models\Subuser + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithKey($user, $server); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 31fb975f0..0dafbc96e 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -66,11 +66,16 @@ class Handler extends ExceptionHandler $displayError = 'An unhandled exception was encountered with this request.'; } - $response = response()->json([ - 'error' => $displayError, - 'http_code' => (! $this->isHttpException($exception)) ?: $exception->getStatusCode(), - 'trace' => (! config('app.debug')) ? null : class_basename($exception) . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine(), - ], ($this->isHttpException($exception)) ? $exception->getStatusCode() : 500, [], JSON_UNESCAPED_SLASHES); + $response = response()->json( + [ + 'error' => $displayError, + 'http_code' => (! $this->isHttpException($exception)) ?: $exception->getStatusCode(), + 'trace' => (! config('app.debug')) ? null : $exception->getTrace(), + ], + $this->isHttpException($exception) ? $exception->getStatusCode() : 500, + $this->isHttpException($exception) ? $exception->getHeaders() : [], + JSON_UNESCAPED_SLASHES + ); parent::report($exception); } elseif ($exception instanceof DisplayException) { diff --git a/app/Http/Controllers/API/Remote/ValidateKeyController.php b/app/Http/Controllers/API/Remote/ValidateKeyController.php new file mode 100644 index 000000000..c10310c27 --- /dev/null +++ b/app/Http/Controllers/API/Remote/ValidateKeyController.php @@ -0,0 +1,90 @@ +. + * + * 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\API\Remote; + +use Spatie\Fractal\Fractal; +use Pterodactyl\Http\Controllers\Controller; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Foundation\Testing\HttpException; +use League\Fractal\Serializer\JsonApiSerializer; +use Pterodactyl\Transformers\Daemon\ApiKeyTransformer; +use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; + +class ValidateKeyController extends Controller +{ + /** + * @var \Illuminate\Contracts\Foundation\Application + */ + protected $app; + + /** + * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface + */ + protected $daemonKeyRepository; + + /** + * @var \Spatie\Fractal\Fractal + */ + protected $fractal; + + /** + * ValidateKeyController constructor. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $daemonKeyRepository + * @param \Spatie\Fractal\Fractal $fractal + */ + public function __construct( + Application $app, + DaemonKeyRepositoryInterface $daemonKeyRepository, + Fractal $fractal + ) { + $this->app = $app; + $this->daemonKeyRepository = $daemonKeyRepository; + $this->fractal = $fractal; + } + + /** + * Return the server(s) and permissions associated with an API key. + * + * @param string $token + * @return array + * + * @throws \Illuminate\Foundation\Testing\HttpException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index($token) + { + if (! starts_with($token, 'i_')) { + throw new HttpException(501); + } + + $key = $this->daemonKeyRepository->getKeyWithServer($token); + + return $this->fractal->item($key, $this->app->make(ApiKeyTransformer::class), 'server') + ->serializeWith(JsonApiSerializer::class) + ->toArray(); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1e98b9cfd..d5940b2e2 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,7 +2,9 @@ namespace Pterodactyl\Http; +use Pterodactyl\Http\Middleware\DaemonAuthenticate; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Routing\Middleware\SubstituteBindings; class Kernel extends HttpKernel { @@ -43,6 +45,10 @@ class Kernel extends HttpKernel 'throttle:60,1', 'bindings', ], + 'daemon' => [ + \Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate::class, + SubstituteBindings::class, + ], ]; /** @@ -57,7 +63,7 @@ class Kernel extends HttpKernel 'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class, 'subuser' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, - 'daemon' => \Pterodactyl\Http\Middleware\DaemonAuthenticate::class, + 'daemon-old' => DaemonAuthenticate::class, 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, diff --git a/app/Http/Middleware/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Daemon/DaemonAuthenticate.php new file mode 100644 index 000000000..2804fa923 --- /dev/null +++ b/app/Http/Middleware/Daemon/DaemonAuthenticate.php @@ -0,0 +1,82 @@ +. + * + * 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\Middleware\Daemon; + +use Closure; +use Illuminate\Http\Request; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; + +class DaemonAuthenticate +{ + /** + * @var array + */ + protected $except = ['daemon.configuration']; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * DaemonAuthenticate constructor. + * + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + */ + public function __construct(NodeRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Check if a request from the daemon can be properly attributed back to a single node instance. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + public function handle(Request $request, Closure $next) + { + $token = $request->bearerToken(); + + if (is_null($token)) { + throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); + } + + try { + $node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]); + } catch (RecordNotFoundException $exception) { + throw new HttpException(403); + } + + $request->attributes->set('node.model', $node); + + return $next($request); + } +} diff --git a/app/Http/Middleware/Server/ScheduleAccess.php b/app/Http/Middleware/Server/ScheduleAccess.php index 68b7aff9b..880630a89 100644 --- a/app/Http/Middleware/Server/ScheduleAccess.php +++ b/app/Http/Middleware/Server/ScheduleAccess.php @@ -70,8 +70,8 @@ class ScheduleAccess * @param \Closure $next * @return mixed * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function handle($request, Closure $next) { diff --git a/app/Http/Middleware/Server/SubuserAccess.php b/app/Http/Middleware/Server/SubuserAccess.php index 97e08af5a..c1124167c 100644 --- a/app/Http/Middleware/Server/SubuserAccess.php +++ b/app/Http/Middleware/Server/SubuserAccess.php @@ -63,6 +63,7 @@ class SubuserAccess * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function handle($request, Closure $next) { diff --git a/app/Http/Middleware/ServerAuthenticate.php b/app/Http/Middleware/ServerAuthenticate.php index b5d3fd1c2..4d83070fb 100644 --- a/app/Http/Middleware/ServerAuthenticate.php +++ b/app/Http/Middleware/ServerAuthenticate.php @@ -82,6 +82,8 @@ class ServerAuthenticate * * @throws \Illuminate\Auth\AuthenticationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function handle(Request $request, Closure $next) { diff --git a/app/Models/DaemonKey.php b/app/Models/DaemonKey.php new file mode 100644 index 000000000..625df0c9c --- /dev/null +++ b/app/Models/DaemonKey.php @@ -0,0 +1,103 @@ +. + * + * 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\Models; + +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; +use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; + +class DaemonKey extends Model implements CleansAttributes, ValidableContract +{ + use Eloquence, Validable; + + /** + * @var string + */ + protected $table = 'daemon_keys'; + + /** + * @var array + */ + protected $casts = [ + 'user_id' => 'integer', + 'server_id' => 'integer', + ]; + + /** + * @var array + */ + protected $dates = [ + self::CREATED_AT, + self::UPDATED_AT, + 'expires_at', + ]; + + /** + * @var array + */ + protected $fillable = ['user_id', 'server_id', 'secret', 'expires_at']; + + /** + * @var array + */ + protected static $applicationRules = [ + 'user_id' => 'required', + 'server_id' => 'required', + 'secret' => 'required', + 'expires_at' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'numeric|exists:users,id', + 'server_id' => 'numeric|exists:servers,id', + 'secret' => 'string|min:20', + 'expires_at' => 'date', + ]; + + /** + * Return the server relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function server() + { + return $this->belongsTo(Server::class); + } + + /** + * Return the user relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 2dd37c5f9..66c5cfe3c 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -24,21 +24,18 @@ namespace Pterodactyl\Models; -use Auth; -use Cache; -use Carbon; use Schema; -use Javascript; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Znck\Eloquent\Traits\BelongsToThrough; use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; class Server extends Model implements CleansAttributes, ValidableContract { - use Eloquence, Notifiable, Validable; + use BelongsToThrough, Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -52,7 +49,7 @@ class Server extends Model implements CleansAttributes, ValidableContract * * @var array */ - protected $hidden = ['daemonSecret', 'sftp_password']; + protected $hidden = ['sftp_password']; /** * The attributes that should be mutated to dates. @@ -152,109 +149,6 @@ class Server extends Model implements CleansAttributes, ValidableContract 'node.name' => 2, ]; - /** - * Returns a single server specified by UUID. - * DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS. - * YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS. - * - * @param string $uuid - * @param array $with - * @param array $withCount - * @return \Pterodactyl\Models\Server - * @throws \Exception - * @todo Remove $with and $withCount due to cache issues, they aren't used anyways. - */ - public static function byUuid($uuid, array $with = [], array $withCount = []) - { - if (! Auth::check()) { - throw new \Exception('You must call Server:byUuid as an authenticated user.'); - } - - // Results are cached because we call this functions a few times on page load. - $result = Cache::tags(['Model:Server', 'Model:Server:byUuid:' . $uuid])->remember('Model:Server:byUuid:' . $uuid . Auth::user()->uuid, Carbon::now()->addMinutes(15), function () use ($uuid) { - $query = self::with('service', 'node')->where(function ($q) use ($uuid) { - $q->where('uuidShort', $uuid)->orWhere('uuid', $uuid); - }); - - if (! Auth::user()->isRootAdmin()) { - $query->whereIn('id', Auth::user()->serverAccessArray()); - } - - return $query->first(); - }); - - if (! is_null($result)) { - $result->daemonSecret = Auth::user()->daemonToken($result); - } - - return $result; - } - - /** - * Returns non-administrative headers for accessing a server on the daemon. - * - * @param Pterodactyl\Models\User|null $user - * @return array - */ - public function guzzleHeaders(User $user = null) - { - // If no specific user is passed, see if we can find an active - // auth session to pull data from. - if (is_null($user) && Auth::check()) { - $user = Auth::user(); - } - - return [ - 'X-Access-Server' => $this->uuid, - 'X-Access-Token' => ($user) ? $user->daemonToken($this) : $this->daemonSecret, - ]; - } - - /** - * Return an instance of the Guzzle client for this specific server using defined access token. - * - * @param Pterodactyl\Models\User|null $user - * @return \GuzzleHttp\Client - */ - public function guzzleClient(User $user = null) - { - return $this->node->guzzleClient($this->guzzleHeaders($user)); - } - - /** - * Returns javascript object to be embedded on server view pages with relevant information. - * - * @param array|null $additional - * @param array|null $overwrite - * @return \Laracasts\Utilities\JavaScript\JavaScriptFacade - */ - public function js($additional = null, $overwrite = null) - { - $response = [ - 'server' => collect($this->makeVisible('daemonSecret'))->only([ - 'uuid', - 'uuidShort', - 'daemonSecret', - 'username', - ]), - 'node' => collect($this->node)->only([ - 'fqdn', - 'scheme', - 'daemonListen', - ]), - ]; - - if (is_array($additional)) { - $response = array_merge($response, $additional); - } - - if (is_array($overwrite)) { - $response = $overwrite; - } - - return Javascript::put($response); - } - /** * Return the columns available for this table. * @@ -358,12 +252,11 @@ class Server extends Model implements CleansAttributes, ValidableContract /** * Gets information for the tasks associated with this server. * - * @TODO adjust server column in tasks to be server_id * @return \Illuminate\Database\Eloquent\Relations\HasMany */ - public function tasks() + public function schedule() { - return $this->hasMany(Task::class); + return $this->hasMany(Schedule::class); } /** @@ -377,12 +270,34 @@ class Server extends Model implements CleansAttributes, ValidableContract } /** - * Gets the location of the server. + * Returns the location that a server belongs to. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return \Znck\Eloquent\Relations\BelongsToThrough + * + * @throws \Exception */ public function location() { - return $this->node->location(); + return $this->belongsToThrough(Location::class, Node::class); + } + + /** + * Return the key belonging to the server owner. + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function ownerKey() + { + return $this->hasOne(DaemonKey::class, 'user_id', 'owner_id'); + } + + /** + * Returns all of the daemon keys belonging to this server. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function keys() + { + return $this->hasMany(DaemonKey::class); } } diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index 5326da3f4..bdd532465 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -42,13 +42,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract */ protected $table = 'subusers'; - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = ['daemonSecret']; - /** * Fields that are not mass assignable. * @@ -72,7 +65,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract protected static $applicationRules = [ 'user_id' => 'required', 'server_id' => 'required', - 'daemonSecret' => 'required', ]; /** @@ -81,7 +73,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract protected static $dataIntegrityRules = [ 'user_id' => 'numeric|exists:users,id', 'server_id' => 'numeric|exists:servers,id', - 'daemonSecret' => 'string', ]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1fc8b7423..b02d3206e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -28,6 +28,7 @@ use View; use Cache; use Pterodactyl\Models; use Pterodactyl\Observers; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -37,6 +38,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { + Schema::defaultStringLength(191); + Models\User::observe(Observers\UserObserver::class); Models\Server::observe(Observers\ServerObserver::class); Models\Subuser::observe(Observers\SubuserObserver::class); diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 4167b95da..a5209a14a 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -40,6 +40,7 @@ use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\ScheduleRepository; +use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\PermissionRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository; @@ -61,6 +62,7 @@ use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; +use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; @@ -87,6 +89,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); + $this->app->bind(DaemonKeyRepositoryInterface::class, DaemonKeyRepository::class); $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 64b747d0d..e17f96b45 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -54,7 +54,11 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); - Route::middleware(['web', 'daemon'])->prefix('/daemon') + Route::middleware(['daemon'])->prefix('/api/remote') + ->namespace($this->namespace . '\API\Remote') + ->group(base_path('routes/api-remote.php')); + + Route::middleware(['web', 'daemon-old'])->prefix('/daemon') ->namespace($this->namespace . '\Daemon') ->group(base_path('routes/daemon.php')); } diff --git a/app/Repositories/Eloquent/DaemonKeyRepository.php b/app/Repositories/Eloquent/DaemonKeyRepository.php new file mode 100644 index 000000000..238615f72 --- /dev/null +++ b/app/Repositories/Eloquent/DaemonKeyRepository.php @@ -0,0 +1,66 @@ +. + * + * 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\Repositories\Eloquent; + +use Webmozart\Assert\Assert; +use Pterodactyl\Models\DaemonKey; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; + +class DaemonKeyRepository extends EloquentRepository implements DaemonKeyRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return DaemonKey::class; + } + + /** + * {@inheritdoc} + */ + public function getServerKeys($server) + { + Assert::integerish($server, 'First argument passed to getServerKeys must be integer, received %s.'); + + return $this->getBuilder()->where('server_id', $server)->get($this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getKeyWithServer($key) + { + Assert::stringNotEmpty($key, 'First argument passed to getServerByKey must be string, received %s.'); + + $instance = $this->getBuilder()->with('server')->where('secret', '=', $key)->first(); + if (is_null($instance)) { + throw new RecordNotFoundException; + } + + return $instance; + } +} diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index 32d1a172a..7ed87c936 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -69,6 +69,26 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI return $instance; } + /** + * {@inheritdoc} + */ + public function getWithPermissionsUsingUserAndServer($user, $server) + { + Assert::integerish($user, 'First argument passed to getWithPermissionsUsingUserAndServer must be integer, received %s.'); + Assert::integerish($server, 'Second argument passed to getWithPermissionsUsingUserAndServer must be integer, received %s.'); + + $instance = $this->getBuilder()->with('permissions')->where([ + ['user_id', '=', $user], + ['server_id', '=', $server], + ])->first(); + + if (is_null($instance)) { + throw new RecordNotFoundException; + } + + return $instance; + } + /** * {@inheritdoc} */ @@ -83,4 +103,24 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI return $instance; } + + /** + * {@inheritdoc} + */ + public function getWithKey($user, $server) + { + Assert::integerish($user, 'First argument passed to getWithKey must be integer, received %s.'); + Assert::integerish($server, 'Second argument passed to getWithKey must be integer, received %s.'); + + $instance = $this->getBuilder()->with('key')->where([ + ['user_id', '=', $user], + ['server_id', '=', $server], + ])->first(); + + if (is_null($instance)) { + throw new RecordNotFoundException; + } + + return $instance; + } } diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php index 4618d323b..986d34c76 100644 --- a/app/Services/Servers/ServerAccessHelperService.php +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -29,24 +29,54 @@ use Pterodactyl\Models\Server; use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; class ServerAccessHelperService { + /** + * @var \Illuminate\Cache\Repository + */ + protected $cache; + + /** + * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface + */ + protected $daemonKeyRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * ServerAccessHelperService constructor. + * + * @param \Illuminate\Cache\Repository $cache + * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $daemonKeyRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository + */ public function __construct( CacheRepository $cache, + DaemonKeyRepositoryInterface $daemonKeyRepository, ServerRepositoryInterface $repository, - SubuserRepositoryInterface $subuserRepository, UserRepositoryInterface $userRepository ) { $this->cache = $cache; + $this->daemonKeyRepository = $daemonKeyRepository; $this->repository = $repository; - $this->subuserRepository = $subuserRepository; $this->userRepository = $userRepository; } /** + * Return the daemon secret to use when making a connection. + * * @param int|\Pterodactyl\Models\Server $server * @param int|\Pterodactyl\Models\User $user * @return string @@ -64,19 +94,17 @@ class ServerAccessHelperService $user = $this->userRepository->find($user); } - if ($user->root_admin || $server->owner_id === $user->id) { - return $server->daemonSecret; + $keys = $server->relationLoaded('keys') ? $server->keys : $this->daemonKeyRepository->getServerKeys($server->id); + + $key = array_get($keys->where('user_id', $user->id)->first(null, []), 'secret'); + if ($user->root_admin) { + $key = array_get($keys->where('user_id', $server->owner_id)->first(null, []), 'secret'); } - if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) { + if (is_null($key)) { throw new UserNotLinkedToServerException; } - $subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ]); - - return $subuser->daemonSecret; + return $key; } } diff --git a/app/Transformers/Daemon/ApiKeyTransformer.php b/app/Transformers/Daemon/ApiKeyTransformer.php new file mode 100644 index 000000000..e17b18f82 --- /dev/null +++ b/app/Transformers/Daemon/ApiKeyTransformer.php @@ -0,0 +1,82 @@ +. + * + * 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\Transformers\Daemon; + +use Pterodactyl\Models\DaemonKey; +use Pterodactyl\Models\Permission; +use League\Fractal\TransformerAbstract; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; + +class ApiKeyTransformer extends TransformerAbstract +{ + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * ApiKeyTransformer constructor. + * + * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository + */ + public function __construct(SubuserRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Return a listing of servers that a daemon key can access. + * + * @param \Pterodactyl\Models\DaemonKey $key + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function transform(DaemonKey $key) + { + if ($key->user_id === $key->server->owner_id) { + return [ + 'id' => $key->server->uuid, + 'permissions' => ['s:*'], + ]; + } + + $subuser = $this->repository->getWithPermissionsUsingUserAndServer($key->user_id, $key->server_id); + + $permissions = $subuser->permissions->pluck('permission')->toArray(); + $mappings = Permission::getPermissions(true); + $daemonPermissions = []; + + foreach ($permissions as $permission) { + if (! is_null($mappings[$permission])) { + $daemonPermissions[] = $mappings[$permission]; + } + } + + return [ + $key->server->uuid => $daemonPermissions, + ]; + } +} diff --git a/database/migrations/2017_09_23_170933_CreateDaemonKeysTable.php b/database/migrations/2017_09_23_170933_CreateDaemonKeysTable.php new file mode 100644 index 000000000..cfbfc88b0 --- /dev/null +++ b/database/migrations/2017_09_23_170933_CreateDaemonKeysTable.php @@ -0,0 +1,35 @@ +increments('id'); + $table->unsignedInteger('server_id'); + $table->unsignedInteger('user_id'); + $table->string('secret')->unique(); + $table->timestamp('expires_at'); + $table->timestamps(); + + $table->index(['server_id', 'user_id']); + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('daemon_keys'); + } +} diff --git a/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php b/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php new file mode 100644 index 000000000..4eb52db03 --- /dev/null +++ b/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php @@ -0,0 +1,51 @@ +select('id', 'owner_id')->get(); + $servers->each(function ($server) use (&$inserts) { + $inserts[] = [ + 'user_id' => $server->owner_id, + 'server_id' => $server->id, + 'secret' => 'i_' . str_random(40), + 'expires_at' => Carbon::now()->addHours(24), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + }); + + DB::transaction(function () use ($inserts) { + DB::table('daemon_keys')->insert($inserts); + }); + + Schema::table('servers', function (Blueprint $table) { + $table->dropUnique(['daemonSecret']); + $table->dropColumn('daemonSecret'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->char('daemonSecret', 36)->after('startup')->unique(); + }); + + DB::table('daemon_keys')->truncate(); + } +} diff --git a/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php new file mode 100644 index 000000000..d2f6aaf7c --- /dev/null +++ b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php @@ -0,0 +1,52 @@ +get(); + $subusers->each(function ($subuser) use (&$inserts) { + $inserts[] = [ + 'user_id' => $subuser->user_id, + 'server_id' => $subuser->server_id, + 'secret' => 'i_' . str_random(40), + 'expires_at' => Carbon::now()->addHours(24), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + }); + + DB::transaction(function () use ($inserts) { + DB::table('daemon_keys')->insert($inserts); + }); + + Schema::table('subusers', function (Blueprint $table) { + $table->dropUnique(['daemonSecret']); + $table->dropColumn('daemonSecret'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('subusers', function (Blueprint $table) { + $table->char('daemonSecret', 36)->after('server_id')->unique(); + }); + + $subusers = DB::table('subusers')->get(); + $subusers->each(function ($subuser) { + DB::table('daemon_keys')->delete($subuser->id); + }); + } +} diff --git a/routes/api-remote.php b/routes/api-remote.php new file mode 100644 index 000000000..d649a556a --- /dev/null +++ b/routes/api-remote.php @@ -0,0 +1,24 @@ +. + * + * 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. + */ +Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('post.api.remote.authenticate');