diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 9c3816db3..72995d004 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -25,22 +25,48 @@ namespace Pterodactyl\Http\Controllers\Base; -use Log; -use Alert; use Illuminate\Http\Request; use Pterodactyl\Models\APIKey; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\APIPermission; -use Pterodactyl\Repositories\APIRepository; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\ApiKeyService; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Http\Requests\ApiKeyRequest; class APIController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Models\APIKey + */ + protected $model; + + /** + * @var \Pterodactyl\Services\ApiKeyService + */ + protected $service; + + /** + * APIController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\ApiKeyService $service + */ + public function __construct(AlertsMessageBag $alert, ApiKeyService $service, APIKey $model) + { + $this->alert = $alert; + $this->model = $model; + $this->service = $service; + } + /** * Display base API index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -53,15 +79,14 @@ class APIController extends Controller /** * Display API key creation page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('base.api.new', [ 'permissions' => [ - 'user' => collect(APIPermission::permissions())->pull('_user'), - 'admin' => collect(APIPermission::permissions())->except('_user')->toArray(), + 'user' => collect(APIPermission::PERMISSIONS)->pull('_user'), + 'admin' => collect(APIPermission::PERMISSIONS)->except('_user')->toArray(), ], ]); } @@ -69,52 +94,46 @@ class APIController extends Controller /** * Handle saving new API key. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ApiKeyRequest $request) { - try { - $repo = new APIRepository($request->user()); - $secret = $repo->create($request->intersect([ - 'memo', 'allowed_ips', - 'admin_permissions', 'permissions', - ])); - Alert::success('An API Key-Pair has successfully been generated. The API secret for this public key is shown below and will not be shown again.

' . $secret . '')->flash(); - - return redirect()->route('account.api'); - } catch (DisplayValidationException $ex) { - return redirect()->route('account.api.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to add this API key.')->flash(); + $adminPermissions = []; + if ($request->user()->isRootAdmin()) { + $adminPermissions = $request->input('admin_permissions') ?? []; } - return redirect()->route('account.api.new')->withInput(); + $secret = $this->service->create([ + 'user_id' => $request->user()->id, + 'allowed_ips' => $request->input('allowed_ips'), + 'memo' => $request->input('memo'), + ], $request->input('permissions') ?? [], $adminPermissions); + + $this->alert->success('An API Key-Pair has successfully been generated. The API secret for this public key is shown below and will not be shown again.

' . $secret . '')->flash(); + + return redirect()->route('account.api'); } /** - * Handle revoking API key. - * * @param \Illuminate\Http\Request $request * @param string $key - * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response + * @return \Illuminate\Http\Response + * + * @throws \Exception */ public function revoke(Request $request, $key) { - try { - $repo = new APIRepository($request->user()); - $repo->revoke($key); + $key = $this->model->newQuery() + ->where('user_id', $request->user()->id) + ->where('public', $key) + ->firstOrFail(); - return response('', 204); - } catch (\Exception $ex) { - Log::error($ex); + $this->service->revoke($key); - return response()->json([ - 'error' => 'An error occured while attempting to remove this key.', - ], 503); - } + return response('', 204); } } diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index e3e0be37f..a92a89c68 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -24,7 +24,6 @@ namespace Pterodactyl\Http\Requests\Admin; -use Pterodactyl\Models\User; use Illuminate\Foundation\Http\FormRequest; abstract class AdminFormRequest extends FormRequest @@ -37,7 +36,7 @@ abstract class AdminFormRequest extends FormRequest */ public function authorize() { - if (! $this->user() instanceof User) { + if (is_null($this->user())) { return false; } diff --git a/app/Http/Requests/ApiKeyRequest.php b/app/Http/Requests/ApiKeyRequest.php new file mode 100644 index 000000000..52b8f90ea --- /dev/null +++ b/app/Http/Requests/ApiKeyRequest.php @@ -0,0 +1,89 @@ +. + * + * 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\Requests; + +use IPTools\Network; + +class ApiKeyRequest extends BaseFormRequest +{ + /** + * Rules applied to data passed in this request. + * + * @return array + */ + public function rules() + { + $this->parseAllowedIntoArray(); + + return [ + 'memo' => 'required|nullable|string|max:500', + 'permissions' => 'sometimes|present|array', + 'admin_permissions' => 'sometimes|present|array', + 'allowed_ips' => 'present', + 'allowed_ips.*' => 'sometimes|string', + ]; + } + + /** + * Parse the string of allowed IPs into an array. + */ + protected function parseAllowedIntoArray() + { + $loop = []; + if (! empty($this->input('allowed_ips'))) { + foreach (explode(PHP_EOL, $this->input('allowed_ips')) as $ip) { + $loop[] = trim($ip); + } + } + + $this->merge(['allowed_ips' => $loop], $this->except('allowed_ips')); + } + + /** + * Run additional validation rules on the request to ensure all of the data is good. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (empty($this->input('permissions')) && empty($this->input('admin_permissions'))) { + $validator->errors()->add('permissions', 'At least one permission must be selected.'); + } + }); + + $validator->after(function ($validator) { + foreach ($this->input('allowed_ips') as $ip) { + $ip = trim($ip); + + try { + Network::parse($ip); + } catch (\Exception $ex) { + $validator->errors()->add('allowed_ips', 'Could not parse IP ' . $ip . ' because it is in an invalid format.'); + } + } + }); + } +} diff --git a/app/Http/Requests/BaseFormRequest.php b/app/Http/Requests/BaseFormRequest.php new file mode 100644 index 000000000..7d5274bb3 --- /dev/null +++ b/app/Http/Requests/BaseFormRequest.php @@ -0,0 +1,53 @@ +. + * + * 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\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class BaseFormRequest extends FormRequest +{ + /** + * Determine if a user is authorized to access this endpoint. + * + * @return bool + */ + public function authorize() + { + return ! is_null($this->user()); + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index 6ed73b7c2..b62b6eb2f 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -24,16 +24,14 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIKey extends Model +class APIKey extends Model implements ValidableContract { - /** - * Public key defined length used in verification methods. - * - * @var int - */ - const PUBLIC_KEY_LEN = 16; + use Eloquence, Validable; /** * The table associated with the model. @@ -65,6 +63,32 @@ class APIKey extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; + /** + * Rules defining what fields must be passed when making a model. + * + * @var array + */ + protected static $applicationRules = [ + 'memo' => 'required', + 'user_id' => 'required', + 'secret' => 'required', + 'public' => 'required', + ]; + + /** + * Rules to protect aganist invalid data entry to DB. + * + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'exists:users,id', + 'public' => 'string|size:16', + 'secret' => 'string', + 'memo' => 'nullable|string|max:500', + 'allowed_ips' => 'nullable|json', + 'expires_at' => 'nullable|datetime', + ]; + /** * Gets the permissions associated with a key. * diff --git a/app/Models/APIPermission.php b/app/Models/APIPermission.php index bfe2fc908..9361d31b2 100644 --- a/app/Models/APIPermission.php +++ b/app/Models/APIPermission.php @@ -24,46 +24,19 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIPermission extends Model +class APIPermission extends Model implements ValidableContract { - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'api_permissions'; - - /** - * Fields that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id']; - - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'key_id' => 'integer', - ]; - - /** - * Disable timestamps for this table. - * - * @var bool - */ - public $timestamps = false; + use Eloquence, Validable; /** * List of permissions available for the API. - * - * @var array */ - protected static $permissions = [ + const PERMISSIONS = [ // Items within this block are available to non-adminitrative users. '_user' => [ 'server' => [ @@ -119,13 +92,49 @@ class APIPermission extends Model ], ]; + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'api_permissions'; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id']; + + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'key_id' => 'integer', + ]; + + protected static $dataIntegrityRules = [ + 'key_id' => 'required|numeric', + 'permission' => 'required|string|max:200', + ]; + + /** + * Disable timestamps for this table. + * + * @var bool + */ + public $timestamps = false; + /** * Return permissions for API. * * @return array + * @deprecated */ public static function permissions() { - return self::$permissions; + return []; } } diff --git a/app/Services/ApiKeyService.php b/app/Services/ApiKeyService.php new file mode 100644 index 000000000..91e703ea1 --- /dev/null +++ b/app/Services/ApiKeyService.php @@ -0,0 +1,153 @@ +. + * + * 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 Pterodactyl\Models\APIKey; +use Illuminate\Database\Connection; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Exceptions\Model\DataValidationException; + +class ApiKeyService +{ + const PUB_CRYPTO_BYTES = 8; + const PRIV_CRYPTO_BYTES = 32; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\APIKey + */ + protected $model; + + /** + * @var \Pterodactyl\Services\ApiPermissionService + */ + protected $permissionService; + + /** + * ApiKeyService constructor. + * + * @param \Pterodactyl\Models\APIKey $model + * @param \Illuminate\Database\Connection $database + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Services\ApiPermissionService $permissionService + */ + public function __construct( + APIKey $model, + Connection $database, + Encrypter $encrypter, + ApiPermissionService $permissionService + ) { + $this->database = $database; + $this->encrypter = $encrypter; + $this->model = $model; + $this->permissionService = $permissionService; + } + + /** + * Create a new API Key on the system with the given permissions. + * + * @param array $data + * @param array $permissions + * @param array $administrative + * @return string + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create(array $data, array $permissions, array $administrative = []) + { + $publicKey = bin2hex(random_bytes(self::PUB_CRYPTO_BYTES)); + $secretKey = bin2hex(random_bytes(self::PRIV_CRYPTO_BYTES)); + + // Start a Transaction + $this->database->beginTransaction(); + + $instance = $this->model->newInstance($data); + $instance->public = $publicKey; + $instance->secret = $this->encrypter->encrypt($secretKey); + + if (! $instance->save()) { + $this->database->rollBack(); + throw new DataValidationException($instance->getValidator()); + } + + $key = $instance->fresh(); + $nodes = $this->permissionService->getPermissions(); + + foreach ($permissions as $permission) { + @list($block, $search) = explode('-', $permission); + + if ( + (empty($block) || empty($search)) || + ! array_key_exists($block, $nodes['_user']) || + ! in_array($search, $nodes['_user'][$block]) + ) { + continue; + } + + $this->permissionService->create($key->id, sprintf('user.%s', $permission)); + } + + foreach ($administrative as $permission) { + @list($block, $search) = explode('-', $permission); + + if ( + (empty($block) || empty($search)) || + ! array_key_exists($block, $nodes) || + ! in_array($search, $nodes[$block]) + ) { + continue; + } + + $this->permissionService->create($key->id, $permission); + } + + $this->database->commit(); + + return $secretKey; + } + + /** + * Delete the API key and associated permissions from the database. + * + * @param int|\Pterodactyl\Models\APIKey $key + * @return bool|null + * + * @throws \Exception + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function revoke($key) + { + if (! $key instanceof APIKey) { + $key = $this->model->findOrFail($key); + } + + return $key->delete(); + } +} diff --git a/app/Services/ApiPermissionService.php b/app/Services/ApiPermissionService.php new file mode 100644 index 000000000..20a722bf3 --- /dev/null +++ b/app/Services/ApiPermissionService.php @@ -0,0 +1,79 @@ +. + * + * 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 Pterodactyl\Models\APIPermission; +use Pterodactyl\Exceptions\Model\DataValidationException; + +class ApiPermissionService +{ + /** + * @var \Pterodactyl\Models\APIPermission + */ + protected $model; + + /** + * ApiPermissionService constructor. + * + * @param \Pterodactyl\Models\APIPermission $model + */ + public function __construct(APIPermission $model) + { + $this->model = $model; + } + + /** + * Store a permission key in the database. + * + * @param string $key + * @param string $permission + * @return bool + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create($key, $permission) + { + $instance = $this->model->newInstance([ + 'key_id' => $key, + 'permission' => $permission, + ]); + + if (! $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + + return true; + } + + /** + * Return all of the permissions available for an API Key. + * + * @return array + */ + public function getPermissions() + { + return APIPermission::PERMISSIONS; + } +} diff --git a/app/Services/Helpers/ApiAllowedIpsValidatorService.php b/app/Services/Helpers/ApiAllowedIpsValidatorService.php new file mode 100644 index 000000000..2b420f9c6 --- /dev/null +++ b/app/Services/Helpers/ApiAllowedIpsValidatorService.php @@ -0,0 +1,23 @@ +. + * + * 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. + */ diff --git a/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php new file mode 100644 index 000000000..17dbe8228 --- /dev/null +++ b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php @@ -0,0 +1,36 @@ +dropForeign(['key_id']); + + $table->foreign('key_id')->references('id')->on('api_keys')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_permissions', function (Blueprint $table) { + $table->dropForeign(['key_id']); + + $table->foreign('key_id')->references('id')->on('api_keys'); + }); + } +}