Fix up API handling logic for keys and set a prefix on all keys

This commit is contained in:
DaneEveritt 2022-05-22 19:03:51 -04:00
parent 8605d175d6
commit b051718afe
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
11 changed files with 88 additions and 31 deletions

View file

@ -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

View file

@ -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();
}

View file

@ -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,

View file

@ -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.');
}

View file

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Http\Middleware\Api\Client;
use Illuminate\Http\Request;
use Pterodactyl\Models\ApiKey;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class RequireClientApiKey
{
/**
* Blocks a request to the Client API endpoints if the user is providing an API token
* that was created for the application API.
*
* @return mixed
*/
public function handle(Request $request, \Closure $next)
{
$token = $request->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);
}
}

View file

@ -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);
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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)),
]);

View file

@ -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',