diff --git a/CHANGELOG.md b/CHANGELOG.md index 613a63dcd..eb0e8d00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. should be completely seamless for most installations as the Panel is able to convert between the two. Custom solutions using these eggs should be updated to account for the new format. +This release also changes API key behavior — "client" keys belonging to admin users can now be used to access +the `/api/application` endpoints in their entirety. Existing "application" keys generated in the admin area should +be considered deprecated, but will continue to work. Application keys _will not_ work with the client API. + ### Fixed * Schedules are no longer run when a server is suspended or marked as installing. * The remote field when creating a database is no longer limited to an IP address and `%` wildcard — all expected MySQL remote host values are allowed. @@ -22,6 +26,8 @@ using these eggs should be updated to account for the new format. * Additional permissions (`CREATE TEMPORARY TABLES`, `CREATE VIEW`, `SHOW VIEW`, `EVENT`, and `TRIGGER`) are granted to users when creating new databases for servers. * development: removed Laravel Debugbar in favor of Clockwork for debugging. * The 2FA input field when logging in is now correctly identified as `one-time-password` to help browser autofill capabilities. +* Changed API authentication mechanisms to make use of Laravel Sanctum to significantly clean up our internal handling of sessions. +* API keys generated by the system now set a prefix to identify them as Pterodactyl API keys, and if they are client or application keys. This prefix looks like `ptlc_` for client keys, and `ptla_` for application keys. Existing API keys are unaffected by this change. ### Added * Added support for PHP 8.1 in addition to PHP 8.0 and 7.4. @@ -33,9 +39,11 @@ using these eggs should be updated to account for the new format. * Adds command to return the configuration for a specific node in both YAML and JSON format (`php artisan p:node:configuration`). * Adds command to return a list of all nodes available on the Panel in both table and JSON format (`php artisan p:node:list`). * Adds server network (inbound/outbound) usage graphs to the console screen. +* Adds support for configuring CORS on the API by setting the `APP_CORS_ALLOWED_ORIGINS=example.com,dashboard.example.com` environment variable. By default all instances are configured with this set to `*` which allows any origin. ### Removed * Removes Google Analytics from the front end code. +* Removes multiple middleware that were previously used for configuring API access and controlling model fetching. This has all been replaced with Laravel Sanctum and standard Laravel API tooling. This should make codebase discovery significantly more simple. ## v1.7.0 ### Fixed diff --git a/app/Http/Controllers/Api/Client/ApiKeyController.php b/app/Http/Controllers/Api/Client/ApiKeyController.php index b978cc8d4..427c353a5 100644 --- a/app/Http/Controllers/Api/Client/ApiKeyController.php +++ b/app/Http/Controllers/Api/Client/ApiKeyController.php @@ -63,7 +63,6 @@ class ApiKeyController extends ClientApiController * @return array * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function store(StoreApiKeyRequest $request) { @@ -71,17 +70,14 @@ class ApiKeyController extends ClientApiController throw new DisplayException('You have reached the account limit for number of API keys.'); } - $key = $this->keyCreationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([ - 'user_id' => $request->user()->id, - 'memo' => $request->input('description'), - 'allowed_ips' => $request->input('allowed_ips') ?? [], - ]); + $token = $request->user()->createToken( + $request->input('description'), + $request->input('allowed_ips') + ); - return $this->fractal->item($key) + return $this->fractal->item($token->accessToken) ->transformWith($this->getTransformer(ApiKeyTransformer::class)) - ->addMeta([ - 'secret_token' => $this->encrypter->decrypt($key->token), - ]) + ->addMeta(['secret_token' => $token->plainTextToken]) ->toArray(); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f2c113b8c..bbd5d2ce2 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -27,6 +27,7 @@ use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; +use Pterodactyl\Http\Middleware\Api\Client\RequireClientApiKey; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings; use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; @@ -74,9 +75,10 @@ class Kernel extends HttpKernel SubstituteBindings::class, AuthenticateApplicationUser::class, ], - // TODO: don't allow an application key to use the client API, but do allow a client - // api key to access the application API. - 'client-api' => [SubstituteClientBindings::class], + 'client-api' => [ + SubstituteClientBindings::class, + RequireClientApiKey::class, + ], 'daemon' => [ SubstituteBindings::class, DaemonAuthenticate::class, diff --git a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index bf9a64606..983f4d369 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -16,7 +16,9 @@ class AuthenticateApplicationUser */ public function handle(Request $request, Closure $next) { - if (is_null($request->user()) || !$request->user()->root_admin) { + /** @var \Pterodactyl\Models\User|null $user */ + $user = $request->user(); + if (!$user || !$user->root_admin) { throw new AccessDeniedHttpException('This account does not have permission to access the API.'); } diff --git a/app/Http/Middleware/Api/Client/RequireClientApiKey.php b/app/Http/Middleware/Api/Client/RequireClientApiKey.php new file mode 100644 index 000000000..7203203c7 --- /dev/null +++ b/app/Http/Middleware/Api/Client/RequireClientApiKey.php @@ -0,0 +1,27 @@ +user()->currentAccessToken(); + + if ($token instanceof ApiKey && $token->key_type === ApiKey::TYPE_APPLICATION) { + throw new AccessDeniedHttpException('You are attempting to use an application API key on an endpoint that requires a client API key.'); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index ec7643bd5..f3960b306 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Requests\Api\Application; use Webmozart\Assert\Assert; +use Pterodactyl\Models\ApiKey; use Laravel\Sanctum\TransientToken; use Illuminate\Validation\Validator; use Illuminate\Database\Eloquent\Model; @@ -45,6 +46,10 @@ abstract class ApplicationApiRequest extends FormRequest return true; } + if ($token->key_type === ApiKey::TYPE_ACCOUNT) { + return true; + } + return AdminAcl::check($token, $this->resource, $this->permission); } diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php index cd5a0ddca..814175a56 100644 --- a/app/Models/ApiKey.php +++ b/app/Models/ApiKey.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Models; use Illuminate\Support\Str; +use Webmozart\Assert\Assert; use Pterodactyl\Services\Acl\Api\AdminAcl; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -194,21 +195,33 @@ class ApiKey extends Model */ public static function findToken($token) { - $id = Str::substr($token, 0, self::IDENTIFIER_LENGTH); + $identifier = substr($token, 0, self::IDENTIFIER_LENGTH); - $model = static::where('identifier', $id)->first(); - if (!is_null($model) && decrypt($model->token) === Str::substr($token, strlen($id))) { + $model = static::where('identifier', $identifier)->first(); + if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) { return $model; } return null; } + /** + * Returns the standard prefix for API keys in the system. + */ + public static function getPrefixForType(int $type): string + { + Assert::oneOf($type, [self::TYPE_ACCOUNT, self::TYPE_APPLICATION]); + + return $type === self::TYPE_ACCOUNT ? 'ptlc_' : 'ptla_'; + } + /** * Generates a new identifier for an API key. */ - public static function generateTokenIdentifier(): string + public static function generateTokenIdentifier(int $type): string { - return 'ptdl_' . Str::random(self::IDENTIFIER_LENGTH - 5); + $prefix = self::getPrefixForType($type); + + return $prefix . Str::random(self::IDENTIFIER_LENGTH - strlen($prefix)); } } diff --git a/app/Models/Traits/HasAccessTokens.php b/app/Models/Traits/HasAccessTokens.php index 2aa21cb9e..ed042ccfa 100644 --- a/app/Models/Traits/HasAccessTokens.php +++ b/app/Models/Traits/HasAccessTokens.php @@ -6,6 +6,7 @@ use Illuminate\Support\Str; use Laravel\Sanctum\Sanctum; use Pterodactyl\Models\ApiKey; use Laravel\Sanctum\HasApiTokens; +use Illuminate\Database\Eloquent\Relations\HasMany; use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken; /** @@ -13,25 +14,28 @@ use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken; */ trait HasAccessTokens { - use HasApiTokens; + use HasApiTokens { + tokens as private _tokens; + createToken as private _createToken; + } - public function tokens() + public function tokens(): HasMany { return $this->hasMany(Sanctum::$personalAccessTokenModel); } - public function createToken(string $name, array $abilities = ['*']) + public function createToken(?string $memo, ?array $ips): NewAccessToken { /** @var \Pterodactyl\Models\ApiKey $token */ - $token = $this->tokens()->create([ + $token = $this->tokens()->forceCreate([ 'user_id' => $this->id, 'key_type' => ApiKey::TYPE_ACCOUNT, - 'identifier' => ApiKey::generateTokenIdentifier(), + 'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT), 'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)), - 'memo' => $name, - 'allowed_ips' => [], + 'memo' => $memo ?? '', + 'allowed_ips' => $ips ?? [], ]); - return new NewAccessToken($token, $token->identifier . $plain); + return new NewAccessToken($token, $plain); } } diff --git a/app/Models/User.php b/app/Models/User.php index 6e676a416..fff28b9b1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,12 +3,12 @@ namespace Pterodactyl\Models; use Pterodactyl\Rules\Username; -use Laravel\Sanctum\HasApiTokens; use Illuminate\Support\Collection; use Illuminate\Validation\Rules\In; use Illuminate\Auth\Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Models\Traits\HasAccessTokens; use Illuminate\Auth\Passwords\CanResetPassword; use Pterodactyl\Traits\Helpers\AvailableLanguages; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -84,7 +84,7 @@ class User extends Model implements use Authorizable; use AvailableLanguages; use CanResetPassword; - use HasApiTokens; + use HasAccessTokens; use Notifiable; public const USER_LEVEL_USER = 0; diff --git a/app/Services/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 20a11add0..29f079c81 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -56,7 +56,7 @@ class KeyCreationService { $data = array_merge($data, [ 'key_type' => $this->keyType, - 'identifier' => str_random(ApiKey::IDENTIFIER_LENGTH), + 'identifier' => ApiKey::generateTokenIdentifier($this->keyType), 'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)), ]); diff --git a/database/Factories/ApiKeyFactory.php b/database/Factories/ApiKeyFactory.php index 77795e607..40c78ce26 100644 --- a/database/Factories/ApiKeyFactory.php +++ b/database/Factories/ApiKeyFactory.php @@ -25,7 +25,7 @@ class ApiKeyFactory extends Factory return [ 'key_type' => ApiKey::TYPE_APPLICATION, - 'identifier' => ApiKey::generateTokenIdentifier(), + 'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION), 'token' => $token ?: $token = encrypt(Str::random(ApiKey::KEY_LENGTH)), 'allowed_ips' => null, 'memo' => 'Test Function Key',