From 74ea1aa0aafef7290203f12b71e65da7d9b25f87 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 23 Aug 2017 21:34:11 -0500 Subject: [PATCH] Push subuser creation service --- .../Daemon/ServerRepositoryInterface.php | 9 + .../PermissionRepositoryInterface.php | 29 ++ .../SubuserRepositoryInterface.php} | 17 +- .../Helper/CdnVersionFetchingException.php | 29 ++ .../Subuser/ServerSubuserExistsException.php | 31 +++ .../Subuser/UserIsServerOwnerException.php | 31 +++ app/Http/Controllers/Admin/BaseController.php | 23 +- app/Models/Permission.php | 14 +- app/Models/Subuser.php | 26 +- app/Repositories/Daemon/ServerRepository.php | 14 + .../Eloquent/PermissionRepository.php | 39 +++ .../Eloquent/SubuserRepository.php | 39 +++ .../Helpers/SoftwareVersionService.php | 149 ++++++++++ app/Services/Old/APILogService.php | 65 ----- app/Services/Old/VersionService.php | 133 --------- .../Subusers/SubuserCreationService.php | 188 +++++++++++++ config/cache.php | 2 +- config/pterodactyl.php | 2 +- database/factories/ModelFactory.php | 9 + resources/lang/en/admin/exceptions.php | 4 + .../themes/pterodactyl/admin/index.blade.php | 10 +- .../Helpers/SoftwareVersionServiceTest.php | 183 ++++++++++++ .../Subusers/SubuserCreationServiceTest.php | 260 ++++++++++++++++++ 23 files changed, 1077 insertions(+), 229 deletions(-) create mode 100644 app/Contracts/Repository/PermissionRepositoryInterface.php rename app/{Facades/Version.php => Contracts/Repository/SubuserRepositoryInterface.php} (79%) create mode 100644 app/Exceptions/Service/Helper/CdnVersionFetchingException.php create mode 100644 app/Exceptions/Service/Subuser/ServerSubuserExistsException.php create mode 100644 app/Exceptions/Service/Subuser/UserIsServerOwnerException.php create mode 100644 app/Repositories/Eloquent/PermissionRepository.php create mode 100644 app/Repositories/Eloquent/SubuserRepository.php create mode 100644 app/Services/Helpers/SoftwareVersionService.php delete mode 100644 app/Services/Old/APILogService.php delete mode 100644 app/Services/Old/VersionService.php create mode 100644 app/Services/Subusers/SubuserCreationService.php create mode 100644 tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php create mode 100644 tests/Unit/Services/Subusers/SubuserCreationServiceTest.php diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index de9fe0c3..c6d9ff08 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -36,6 +36,15 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface */ public function create($id, $overrides = [], $start = false); + /** + * Set an access token and associated permissions for a server. + * + * @param string $key + * @param array $permissions + * @return \Psr\Http\Message\ResponseInterface + */ + public function setSubuserKey($key, array $permissions); + /** * Update server details on the daemon. * diff --git a/app/Contracts/Repository/PermissionRepositoryInterface.php b/app/Contracts/Repository/PermissionRepositoryInterface.php new file mode 100644 index 00000000..f84c16f7 --- /dev/null +++ b/app/Contracts/Repository/PermissionRepositoryInterface.php @@ -0,0 +1,29 @@ +. + * + * 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 PermissionRepositoryInterface extends RepositoryInterface +{ +} diff --git a/app/Facades/Version.php b/app/Contracts/Repository/SubuserRepositoryInterface.php similarity index 79% rename from app/Facades/Version.php rename to app/Contracts/Repository/SubuserRepositoryInterface.php index e9475d2a..766fc1b3 100644 --- a/app/Facades/Version.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -1,5 +1,5 @@ . * @@ -22,19 +22,8 @@ * SOFTWARE. */ -namespace Pterodactyl\Facades; +namespace Pterodactyl\Contracts\Repository; -use Illuminate\Support\Facades\Facade; - -class Version extends Facade +interface SubuserRepositoryInterface extends RepositoryInterface { - /** - * Returns the facade accessor class. - * - * @return strig - */ - protected static function getFacadeAccessor() - { - return '\Pterodactyl\Services\VersionService'; - } } diff --git a/app/Exceptions/Service/Helper/CdnVersionFetchingException.php b/app/Exceptions/Service/Helper/CdnVersionFetchingException.php new file mode 100644 index 00000000..d96fe25d --- /dev/null +++ b/app/Exceptions/Service/Helper/CdnVersionFetchingException.php @@ -0,0 +1,29 @@ +. + * + * 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\Exceptions\Service\Helper; + +class CdnVersionFetchingException extends \Exception +{ +} diff --git a/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php b/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php new file mode 100644 index 00000000..0c14f803 --- /dev/null +++ b/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Subuser; + +use Pterodactyl\Exceptions\DisplayException; + +class ServerSubuserExistsException extends DisplayException +{ +} diff --git a/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php b/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php new file mode 100644 index 00000000..ad551b19 --- /dev/null +++ b/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Subuser; + +use Pterodactyl\Exceptions\DisplayException; + +class UserIsServerOwnerException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Admin/BaseController.php b/app/Http/Controllers/Admin/BaseController.php index 14665c32..2858cdaf 100644 --- a/app/Http/Controllers/Admin/BaseController.php +++ b/app/Http/Controllers/Admin/BaseController.php @@ -28,6 +28,7 @@ use Krucas\Settings\Settings; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\BaseFormRequest; +use Pterodactyl\Services\Helpers\SoftwareVersionService; class BaseController extends Controller { @@ -41,10 +42,26 @@ class BaseController extends Controller */ protected $settings; - public function __construct(AlertsMessageBag $alert, Settings $settings) - { + /** + * @var \Pterodactyl\Services\Helpers\SoftwareVersionService + */ + protected $version; + + /** + * BaseController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Krucas\Settings\Settings $settings + * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version + */ + public function __construct( + AlertsMessageBag $alert, + Settings $settings, + SoftwareVersionService $version + ) { $this->alert = $alert; $this->settings = $settings; + $this->version = $version; } /** @@ -54,7 +71,7 @@ class BaseController extends Controller */ public function getIndex() { - return view('admin.index'); + return view('admin.index', ['version' => $this->version]); } /** diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 086586cd..3587e7fc 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -25,9 +25,13 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Eloquence; -class Permission extends Model +class Permission extends Model implements CleansAttributes { + use Eloquence; + /** * Should timestamps be used on this model. * @@ -118,12 +122,12 @@ class Permission extends Model /** * Return a collection of permissions available. * - * @param array $single - * @return \Illuminate\Support\Collection|array + * @param bool $array + * @return array|\Illuminate\Support\Collection */ - public static function listPermissions($single = false) + public static function getPermissions($array = false) { - if ($single) { + if ($array) { return collect(self::$permissions)->mapWithKeys(function ($item) { return $item; })->all(); diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index a2eff9c9..276f97b9 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -26,10 +26,14 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; -class Subuser extends Model +class Subuser extends Model implements CleansAttributes, ValidableContract { - use Notifiable; + use Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -62,6 +66,24 @@ class Subuser extends Model 'server_id' => 'integer', ]; + /** + * @var array + */ + protected static $applicationRules = [ + 'user_id' => 'required', + 'server_id' => 'required', + 'daemonSecret' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'numeric|exists:users,id', + 'server_id' => 'numeric|exists:servers,id', + 'daemonSecret' => 'string', + ]; + /** * Gets the server associated with a subuser. * diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index c1cbee1c..8de958b5 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -84,6 +84,20 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa ]); } + /** + * {@inheritdoc} + */ + public function setSubuserKey($key, array $permissions) + { + return $this->getHttpClient()->request('PATCH', '/server', [ + 'json' => [ + 'keys' => [ + $key => $permissions, + ], + ], + ]); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/PermissionRepository.php b/app/Repositories/Eloquent/PermissionRepository.php new file mode 100644 index 00000000..7fb7b56f --- /dev/null +++ b/app/Repositories/Eloquent/PermissionRepository.php @@ -0,0 +1,39 @@ +. + * + * 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 Pterodactyl\Models\Permission; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; + +class PermissionRepository extends EloquentRepository implements PermissionRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Permission::class; + } +} diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php new file mode 100644 index 00000000..cda8864e --- /dev/null +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -0,0 +1,39 @@ +. + * + * 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 Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Models\Subuser; + +class SubuserRepository extends EloquentRepository implements SubuserRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Subuser::class; + } +} diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php new file mode 100644 index 00000000..87f84a40 --- /dev/null +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -0,0 +1,149 @@ +. + * + * 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\Services\Helpers; + +use Exception; +use GuzzleHttp\Client; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException; + +class SoftwareVersionService +{ + const VERSION_CACHE_KEY = 'pterodactyl:versions'; + + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + protected $cache; + + /** + * @var \GuzzleHttp\Client + */ + protected $client; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * SoftwareVersionService constructor. + * + * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \GuzzleHttp\Client $client + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct( + CacheRepository $cache, + Client $client, + ConfigRepository $config + ) { + $this->cache = $cache; + $this->client = $client; + $this->config = $config; + + $this->cacheVersionData(); + } + + /** + * Get the latest version of the panel from the CDN servers. + * + * @return string + */ + public function getPanel() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'panel', 'error'); + } + + /** + * Get the latest version of the daemon from the CDN servers. + * + * @return string + */ + public function getDaemon() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'daemon', 'error'); + } + + /** + * Get the URL to the discord server. + * + * @return string + */ + public function getDiscord() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'discord', 'https://pterodactyl.io/discord'); + } + + /** + * Determine if the current version of the panel is the latest. + * + * @return bool + */ + public function isLatestPanel() + { + if ($this->config->get('app.version') === 'canary') { + return true; + } + + return version_compare($this->config->get('app.version'), $this->getPanel()) >= 0; + } + + /** + * Determine if a passed daemon version string is the latest. + * + * @param string $version + * @return bool + */ + public function isLatestDaemon($version) + { + if ($version === '0.0.0-canary') { + return true; + } + + return version_compare($version, $this->getDaemon()) >= 0; + } + + /** + * Keeps the versioning cache up-to-date with the latest results from the CDN. + */ + protected function cacheVersionData() + { + $this->cache->remember(self::VERSION_CACHE_KEY, $this->config->get('pterodactyl.cdn.cache_time'), function () { + try { + $response = $this->client->request('GET', $this->config->get('pterodactyl.cdn.url')); + + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody()); + } + + throw new CdnVersionFetchingException; + } catch (Exception $exception) { + return (object) []; + } + }); + } +} diff --git a/app/Services/Old/APILogService.php b/app/Services/Old/APILogService.php deleted file mode 100644 index d4467041..00000000 --- a/app/Services/Old/APILogService.php +++ /dev/null @@ -1,65 +0,0 @@ -. - * - * 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\Services; - -use Log; -use Illuminate\Http\Request; -use Pterodactyl\Models\APILog; - -class APILogService -{ - /** - * Log an API Request. - * - * @param \Illuminate\Http\Request $request - * @param null|string $error - * @param bool $authorized - */ - public static function log(Request $request, $error = null, $authorized = false) - { - if ($request->bearerToken() && ! empty($request->bearerToken())) { - list($public, $hashed) = explode('.', $request->bearerToken()); - } else { - $public = null; - } - - try { - $log = APILog::create([ - 'authorized' => $authorized, - 'error' => $error, - 'key' => $public, - 'method' => $request->method(), - 'route' => $request->fullUrl(), - 'content' => (empty($request->getContent())) ? null : $request->getContent(), - 'user_agent' => $request->header('User-Agent'), - 'request_ip' => $request->ip(), - ]); - $log->save(); - } catch (\Exception $ex) { - // Simply log it and move on. - Log::error($ex); - } - } -} diff --git a/app/Services/Old/VersionService.php b/app/Services/Old/VersionService.php deleted file mode 100644 index 7134b31f..00000000 --- a/app/Services/Old/VersionService.php +++ /dev/null @@ -1,133 +0,0 @@ -. - * - * 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\Services; - -use Cache; -use GuzzleHttp\Client; - -class VersionService -{ - /** - * The cached CDN response. - * - * @var object - */ - protected static $versions; - - /** - * Version constructor. - */ - public function __construct() - { - self::$versions = Cache::remember('versions', config('pterodactyl.cdn.cache'), function () { - $client = new Client(); - - try { - $response = $client->request('GET', config('pterodactyl.cdn.url')); - - if ($response->getStatusCode() === 200) { - return json_decode($response->getBody()); - } else { - throw new \Exception('Invalid response code.'); - } - } catch (\Exception $ex) { - // Failed request, just return errored version. - return (object) [ - 'panel' => 'error', - 'daemon' => 'error', - 'discord' => 'https://pterodactyl.io/discord', - ]; - } - }); - } - - /** - * Return current panel version from CDN. - * - * @return string - */ - public static function getPanel() - { - return self::$versions->panel; - } - - /** - * Return current daemon version from CDN. - * - * @return string - */ - public static function getDaemon() - { - return self::$versions->daemon; - } - - /** - * Return Discord link from CDN. - * - * @return string - */ - public static function getDiscord() - { - return self::$versions->discord; - } - - /** - * Return current panel version. - * - * @return null|string - */ - public function getCurrentPanel() - { - return config('app.version'); - } - - /** - * Determine if panel is latest version. - * - * @return bool - */ - public static function isLatestPanel() - { - if (config('app.version') === 'canary') { - return true; - } - - return version_compare(config('app.version'), self::$versions->panel) >= 0; - } - - /** - * Determine if daemon is latest version. - * - * @return bool - */ - public static function isLatestDaemon($daemon) - { - if ($daemon === '0.0.0-canary') { - return true; - } - - return version_compare($daemon, self::$versions->daemon) >= 0; - } -} diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php new file mode 100644 index 00000000..39c38f18 --- /dev/null +++ b/app/Services/Subusers/SubuserCreationService.php @@ -0,0 +1,188 @@ +. + * + * 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\Services\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; +use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Models\Permission; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Users\CreationService; + +class SubuserCreationService +{ + const CORE_DAEMON_PERMISSIONS = [ + 's:get', + 's:console', + ]; + + const DAEMON_SECRET_BYTES = 18; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $subuserRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Users\CreationService + */ + protected $userCreationService; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + public function __construct( + ConnectionInterface $connection, + CreationService $userCreationService, + DaemonServerRepositoryInterface $daemonRepository, + PermissionRepositoryInterface $permissionRepository, + ServerRepositoryInterface $serverRepository, + SubuserRepositoryInterface $subuserRepository, + UserRepositoryInterface $userRepository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->permissionRepository = $permissionRepository; + $this->subuserRepository = $subuserRepository; + $this->serverRepository = $serverRepository; + $this->userRepository = $userRepository; + $this->userCreationService = $userCreationService; + $this->writer = $writer; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * @param string $email + * @param array $permissions + * @return \Pterodactyl\Models\Subuser + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException + * @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException + */ + public function handle($server, $email, array $permissions) + { + if (! $server instanceof Server) { + $server = $this->serverRepository->find($server); + } + + $user = $this->userRepository->findWhere([['email', '=', $email]]); + if (is_null($user)) { + $user = $this->userCreationService->handle([ + 'email' => $email, + 'username' => substr(strtok($email, '@'), 0, 8), + 'name_first' => 'Server', + 'name_last' => 'Subuser', + 'root_admin' => false, + ]); + } else { + if ($server->owner_id === $user->id) { + throw new UserIsServerOwnerException(trans('admin/exceptions.subusers.user_is_owner')); + } + + $subuserCount = $this->subuserRepository->findCountWhere([['user_id', '=', $user->id], ['server_id', '=', $server->id]]); + if ($subuserCount !== 0) { + throw new ServerSubuserExistsException(trans('admin/exceptions.subusers.subuser_exists')); + } + } + + $this->connection->beginTransaction(); + $subuser = $this->subuserRepository->create([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => bin2hex(random_bytes(self::DAEMON_SECRET_BYTES)), + ]); + + $permissionMappings = Permission::getPermissions(true); + $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; + + foreach ($permissions as $permission) { + if (array_key_exists($permission, $permissionMappings)) { + if (! is_null($permissionMappings[$permission])) { + array_push($daemonPermissions, $permissionMappings[$permission]); + } + + $this->permissionRepository->create([ + 'subuser_id' => $subuser->id, + 'permission' => $permission, + ]); + } + } + + try { + $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid) + ->setSubuserKey($subuser->daemonSecret, $daemonPermissions); + $this->connection->commit(); + + return $subuser; + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/config/cache.php b/config/cache.php index fb619fb5..6109216c 100644 --- a/config/cache.php +++ b/config/cache.php @@ -80,5 +80,5 @@ return [ | */ - 'prefix' => 'laravel', + 'prefix' => 'pterodactyl', ]; diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 32b239bd..a21283d1 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -119,7 +119,7 @@ return [ | if panel is up to date. */ 'cdn' => [ - 'cache' => 60, + 'cache_time' => 60, 'url' => 'https://cdn.pterodactyl.io/releases/latest.json', ], diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index a6fbc069..398915ac 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -145,3 +145,12 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker\Generator $fake 'locked' => 0, ]; }); + +$factory->define(Pterodactyl\Models\Subuser::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'user_id' => $faker->randomNumber(), + 'server_id' => $faker->randomNumber(), + 'daemonSecret' => $faker->unique()->uuid, + ]; +}); diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 4ff0c4b4..21bd812b 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -54,4 +54,8 @@ return [ 'zip_extraction' => 'An exception was encountered while attempting to extract the archive provided onto the server.', 'invalid_archive_exception' => 'The pack archive provided appears to be missing a required archive.tar.gz or import.json file in the base directory.', ], + 'subusers' => [ + 'user_is_owner' => 'You cannot add the server owner as a subuser for this server.', + 'subuser_exists' => 'A user with that email address is already assigned as a subuser for this server.', + ], ]; diff --git a/resources/themes/pterodactyl/admin/index.blade.php b/resources/themes/pterodactyl/admin/index.blade.php index 76f0ed3f..e2884ba0 100644 --- a/resources/themes/pterodactyl/admin/index.blade.php +++ b/resources/themes/pterodactyl/admin/index.blade.php @@ -35,7 +35,7 @@
System Information
- @if (Version::isLatestPanel()) - You are running Pterodactyl Panel version {{ Version::getCurrentPanel() }}. Your panel is up-to-date! + @if ($version->isLatestPanel()) + You are running Pterodactyl Panel version {{ config('app.version') }}. Your panel is up-to-date! @else - Your panel is not up-to-date! The latest version is {{ Version::getPanel() }} and you are currently running version {{ Version::getCurrentPanel() }}. + Your panel is not up-to-date! The latest version is {{ $version->getPanel() }} and you are currently running version {{ config('app.version') }}. @endif
@@ -56,7 +56,7 @@
diff --git a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php new file mode 100644 index 00000000..a3039694 --- /dev/null +++ b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php @@ -0,0 +1,183 @@ +. + * + * 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 Tests\Unit\Services\Helpers; + +use Closure; +use GuzzleHttp\Client; +use Mockery as m; +use Pterodactyl\Services\Helpers\SoftwareVersionService; +use Tests\TestCase; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; + +class SoftwareVersionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + protected $cache; + + /** + * @var \GuzzleHttp\Client + */ + protected $client; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var object + */ + protected static $response = [ + 'panel' => '0.2.0', + 'daemon' => '0.1.0', + 'discord' => 'https://pterodactyl.io/discord', + ]; + + /** + * @var \Pterodactyl\Services\Helpers\SoftwareVersionService + */ + protected $service; + + /** + * Setup tests + */ + public function setUp() + { + parent::setUp(); + + self::$response = (object) self::$response; + + $this->cache = m::mock(CacheRepository::class); + $this->client = m::mock(Client::class); + $this->config = m::mock(ConfigRepository::class); + + $this->config->shouldReceive('get')->with('pterodactyl.cdn.cache_time')->once()->andReturn(60); + + $this->cache->shouldReceive('remember')->with(SoftwareVersionService::VERSION_CACHE_KEY, 60, Closure::class)->once()->andReturnNull(); + + $this->service = m::mock(SoftwareVersionService::class, [$this->cache, $this->client, $this->config])->makePartial(); + } + + /** + * Test that the panel version is returned. + */ + public function testPanelVersionIsReturned() + { + $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); + $this->assertEquals(self::$response->panel, $this->service->getPanel()); + } + + /** + * Test that the panel version is returned as error. + */ + public function testPanelVersionIsReturnedAsErrorIfNoKeyIsFound() + { + $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); + $this->assertEquals('error', $this->service->getPanel()); + } + + /** + * Test that the daemon version is returned. + */ + public function testDaemonVersionIsReturned() + { + $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); + $this->assertEquals(self::$response->daemon, $this->service->getDaemon()); + } + + /** + * Test that the daemon version is returned as an error. + */ + public function testDaemonVersionIsReturnedAsErrorIfNoKeyIsFound() + { + $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); + $this->assertEquals('error', $this->service->getDaemon()); + } + + /** + * Test that the discord URL is returned. + */ + public function testDiscordUrlIsReturned() + { + $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); + $this->assertEquals(self::$response->discord, $this->service->getDiscord()); + } + + /** + * Test that the correct boolean value is returned by the helper for each version passed. + * + * @dataProvider panelVersionProvider + */ + public function testCorrectBooleanValueIsReturnedWhenCheckingPanelVersion($version, $response) + { + $this->config->shouldReceive('get')->with('app.version')->andReturn($version); + $this->service->shouldReceive('getPanel')->withNoArgs()->andReturn(self::$response->panel); + + $this->assertEquals($response, $this->service->isLatestPanel()); + } + + /** + * Test that the correct boolean value is returned. + * + * @dataProvider daemonVersionProvider + */ + public function testCorrectBooleanValueIsReturnedWhenCheckingDaemonVersion($version, $response) + { + $this->service->shouldReceive('getDaemon')->withNoArgs()->andReturn(self::$response->daemon); + + $this->assertEquals($response, $this->service->isLatestDaemon($version)); + } + + /** + * Provide data for testing boolean response on panel version. + * + * @return array + */ + public function panelVersionProvider() + { + return [ + [self::$response['panel'], true], + ['0.0.1', false], + ['canary', true], + ]; + } + + /** + * Provide data for testing booklean response for daemon version. + * + * @return array + */ + public function daemonVersionProvider() + { + return [ + [self::$response['daemon'], true], + ['0.0.1', false], + ['0.0.0-canary', true], + ]; + } +} diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php new file mode 100644 index 00000000..a7492cb3 --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -0,0 +1,260 @@ +. + * + * 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 Tests\Unit\Services\Subusers; + +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; +use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Subusers\SubuserCreationService; +use Pterodactyl\Services\Users\CreationService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserCreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Models\Permission + */ + protected $permission; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $subuserRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserCreationService + */ + protected $service; + + /** + * @var \Pterodactyl\Services\Users\CreationService + */ + protected $userCreationService; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Subusers', 'bin2hex')->expects($this->any())->willReturn('bin2hex'); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->permission = m::mock('overload:Pterodactyl\Models\Permission'); + $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->userCreationService = m::mock(CreationService::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserCreationService( + $this->connection, + $this->userCreationService, + $this->daemonRepository, + $this->permissionRepository, + $this->serverRepository, + $this->subuserRepository, + $this->userRepository, + $this->writer + ); + } + + /** + * Test that a user without an existing account can be added as a subuser. + */ + public function testAccountIsCreatedForNewUser() + { + $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $server = factory(Server::class)->make(); + $user = factory(User::class)->make(); + $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturnNull(); + $this->userCreationService->shouldReceive('handle')->with([ + 'email' => $user->email, + 'username' => substr(strtok($user->email, '@'), 0, 8), + 'name_first' => 'Server', + 'name_last' => 'Subuser', + 'root_admin' => false, + ])->once()->andReturn($user); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->subuserRepository->shouldReceive('create')->with([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => 'bin2hex', + ])->once()->andReturn($subuser); + + $this->permission->shouldReceive('getPermissions')->with(true)->once() + ->andReturn($permissions); + + foreach(array_keys($permissions) as $permission) { + $this->permissionRepository->shouldReceive('create') + ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) + ->once()->andReturnNull(); + } + + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($server, $user->email, array_keys($permissions)); + + $this->assertInstanceOf(Subuser::class, $response); + $this->assertSame($subuser, $response); + } + + /** + * Test that an existing user can be added as a subuser. + */ + public function testExistingUserCanBeAddedAsASubuser() + { + $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $server = factory(Server::class)->make(); + $user = factory(User::class)->make(); + $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->subuserRepository->shouldReceive('findCountWhere')->with([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ])->once()->andReturn(0); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->subuserRepository->shouldReceive('create')->with([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => 'bin2hex', + ])->once()->andReturn($subuser); + + $this->permission->shouldReceive('getPermissions')->with(true)->once() + ->andReturn($permissions); + + foreach(array_keys($permissions) as $permission) { + $this->permissionRepository->shouldReceive('create') + ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) + ->once()->andReturnNull(); + } + + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($server, $user->email, array_keys($permissions)); + + $this->assertInstanceOf(Subuser::class, $response); + $this->assertSame($subuser, $response); + } + + /** + * Test that an exception gets thrown if the subuser is actually the server owner + */ + public function testExceptionIsThrownIfUserIsServerOwner() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(['owner_id' => $user->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + + try { + $this->service->handle($server, $user->email, []); + } catch (DisplayException $exception) { + $this->assertInstanceOf(UserIsServerOwnerException::class, $exception); + $this->assertEquals(trans('admin/exceptions.subusers.user_is_owner'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if the user is already added as a subuser. + */ + public function testExceptionIsThrownIfUserIsAlreadyASubuser() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->subuserRepository->shouldReceive('findCountWhere')->with([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ])->once()->andReturn(1); + + try { + $this->service->handle($server, $user->email, []); + } catch (DisplayException $exception) { + $this->assertInstanceOf(ServerSubuserExistsException::class, $exception); + $this->assertEquals(trans('admin/exceptions.subusers.subuser_exists'), $exception->getMessage()); + } + + } +}