Fix up API handling logic for keys and set a prefix on all keys
This commit is contained in:
parent
8605d175d6
commit
b051718afe
11 changed files with 88 additions and 31 deletions
|
@ -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
|
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.
|
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
|
### Fixed
|
||||||
* Schedules are no longer run when a server is suspended or marked as installing.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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
|
||||||
* Added support for PHP 8.1 in addition to PHP 8.0 and 7.4.
|
* 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 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 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 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
|
### Removed
|
||||||
* Removes Google Analytics from the front end code.
|
* 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
|
## v1.7.0
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -63,7 +63,6 @@ class ApiKeyController extends ClientApiController
|
||||||
* @return array
|
* @return array
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
*/
|
*/
|
||||||
public function store(StoreApiKeyRequest $request)
|
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.');
|
throw new DisplayException('You have reached the account limit for number of API keys.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([
|
$token = $request->user()->createToken(
|
||||||
'user_id' => $request->user()->id,
|
$request->input('description'),
|
||||||
'memo' => $request->input('description'),
|
$request->input('allowed_ips')
|
||||||
'allowed_ips' => $request->input('allowed_ips') ?? [],
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->fractal->item($key)
|
return $this->fractal->item($token->accessToken)
|
||||||
->transformWith($this->getTransformer(ApiKeyTransformer::class))
|
->transformWith($this->getTransformer(ApiKeyTransformer::class))
|
||||||
->addMeta([
|
->addMeta(['secret_token' => $token->plainTextToken])
|
||||||
'secret_token' => $this->encrypter->decrypt($key->token),
|
|
||||||
])
|
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||||
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
||||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||||
|
use Pterodactyl\Http\Middleware\Api\Client\RequireClientApiKey;
|
||||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||||
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings;
|
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings;
|
||||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
|
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||||
|
@ -74,9 +75,10 @@ class Kernel extends HttpKernel
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
AuthenticateApplicationUser::class,
|
AuthenticateApplicationUser::class,
|
||||||
],
|
],
|
||||||
// TODO: don't allow an application key to use the client API, but do allow a client
|
'client-api' => [
|
||||||
// api key to access the application API.
|
SubstituteClientBindings::class,
|
||||||
'client-api' => [SubstituteClientBindings::class],
|
RequireClientApiKey::class,
|
||||||
|
],
|
||||||
'daemon' => [
|
'daemon' => [
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
DaemonAuthenticate::class,
|
DaemonAuthenticate::class,
|
||||||
|
|
|
@ -16,7 +16,9 @@ class AuthenticateApplicationUser
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next)
|
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.');
|
throw new AccessDeniedHttpException('This account does not have permission to access the API.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
27
app/Http/Middleware/Api/Client/RequireClientApiKey.php
Normal file
27
app/Http/Middleware/Api/Client/RequireClientApiKey.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Pterodactyl\Http\Requests\Api\Application;
|
namespace Pterodactyl\Http\Requests\Api\Application;
|
||||||
|
|
||||||
use Webmozart\Assert\Assert;
|
use Webmozart\Assert\Assert;
|
||||||
|
use Pterodactyl\Models\ApiKey;
|
||||||
use Laravel\Sanctum\TransientToken;
|
use Laravel\Sanctum\TransientToken;
|
||||||
use Illuminate\Validation\Validator;
|
use Illuminate\Validation\Validator;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
@ -45,6 +46,10 @@ abstract class ApplicationApiRequest extends FormRequest
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return AdminAcl::check($token, $this->resource, $this->permission);
|
return AdminAcl::check($token, $this->resource, $this->permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Pterodactyl\Models;
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
@ -194,21 +195,33 @@ class ApiKey extends Model
|
||||||
*/
|
*/
|
||||||
public static function findToken($token)
|
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();
|
$model = static::where('identifier', $identifier)->first();
|
||||||
if (!is_null($model) && decrypt($model->token) === Str::substr($token, strlen($id))) {
|
if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) {
|
||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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.
|
* 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use Illuminate\Support\Str;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
use Pterodactyl\Models\ApiKey;
|
use Pterodactyl\Models\ApiKey;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken;
|
use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,25 +14,28 @@ use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken;
|
||||||
*/
|
*/
|
||||||
trait HasAccessTokens
|
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);
|
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 */
|
/** @var \Pterodactyl\Models\ApiKey $token */
|
||||||
$token = $this->tokens()->create([
|
$token = $this->tokens()->forceCreate([
|
||||||
'user_id' => $this->id,
|
'user_id' => $this->id,
|
||||||
'key_type' => ApiKey::TYPE_ACCOUNT,
|
'key_type' => ApiKey::TYPE_ACCOUNT,
|
||||||
'identifier' => ApiKey::generateTokenIdentifier(),
|
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT),
|
||||||
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
|
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
|
||||||
'memo' => $name,
|
'memo' => $memo ?? '',
|
||||||
'allowed_ips' => [],
|
'allowed_ips' => $ips ?? [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new NewAccessToken($token, $token->identifier . $plain);
|
return new NewAccessToken($token, $plain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
namespace Pterodactyl\Models;
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
use Pterodactyl\Rules\Username;
|
use Pterodactyl\Rules\Username;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Validation\Rules\In;
|
use Illuminate\Validation\Rules\In;
|
||||||
use Illuminate\Auth\Authenticatable;
|
use Illuminate\Auth\Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Pterodactyl\Models\Traits\HasAccessTokens;
|
||||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||||
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
@ -84,7 +84,7 @@ class User extends Model implements
|
||||||
use Authorizable;
|
use Authorizable;
|
||||||
use AvailableLanguages;
|
use AvailableLanguages;
|
||||||
use CanResetPassword;
|
use CanResetPassword;
|
||||||
use HasApiTokens;
|
use HasAccessTokens;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
public const USER_LEVEL_USER = 0;
|
public const USER_LEVEL_USER = 0;
|
||||||
|
|
|
@ -56,7 +56,7 @@ class KeyCreationService
|
||||||
{
|
{
|
||||||
$data = array_merge($data, [
|
$data = array_merge($data, [
|
||||||
'key_type' => $this->keyType,
|
'key_type' => $this->keyType,
|
||||||
'identifier' => str_random(ApiKey::IDENTIFIER_LENGTH),
|
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
|
||||||
'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)),
|
'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ApiKeyFactory extends Factory
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'key_type' => ApiKey::TYPE_APPLICATION,
|
'key_type' => ApiKey::TYPE_APPLICATION,
|
||||||
'identifier' => ApiKey::generateTokenIdentifier(),
|
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION),
|
||||||
'token' => $token ?: $token = encrypt(Str::random(ApiKey::KEY_LENGTH)),
|
'token' => $token ?: $token = encrypt(Str::random(ApiKey::KEY_LENGTH)),
|
||||||
'allowed_ips' => null,
|
'allowed_ips' => null,
|
||||||
'memo' => 'Test Function Key',
|
'memo' => 'Test Function Key',
|
||||||
|
|
Loading…
Reference in a new issue