Very basic working implementation of sanctum for API validation

This commit is contained in:
Dane Everitt 2021-07-27 21:23:11 -07:00
parent 4b32828423
commit d60e8a193b
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
23 changed files with 24212 additions and 88 deletions

3
.gitignore vendored
View file

@ -77,3 +77,6 @@ yarn-error.log
!.env.example
.env*
*.log
_ide_helper_models.php
_ide_helper.php
.phpstorm.meta.php

2267
.phpstorm.meta.php Normal file

File diff suppressed because it is too large Load diff

20589
_ide_helper.php Normal file

File diff suppressed because it is too large Load diff

1128
_ide_helper_models.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,14 @@ use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Transformers\Api\Client\ApiKeyTransformer;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Pterodactyl\Http\Requests\Api\Client\Account\StoreApiKeyRequest;
use Pterodactyl\Transformers\Api\Client\PersonalAccessTokenTransformer;
class ApiKeyController extends ClientApiController
{
private Encrypter $encrypter;
private ApiKeyRepository $repository;
private KeyCreationService $keyCreationService;
/**
@ -41,8 +44,8 @@ class ApiKeyController extends ClientApiController
*/
public function index(ClientApiRequest $request): array
{
return $this->fractal->collection($request->user()->apiKeys)
->transformWith($this->getTransformer(ApiKeyTransformer::class))
return $this->fractal->collection($request->user()->tokens)
->transformWith($this->getTransformer(PersonalAccessTokenTransformer::class))
->toArray();
}
@ -50,25 +53,22 @@ class ApiKeyController extends ClientApiController
* Store a new API key for a user's account.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function store(StoreApiKeyRequest $request): array
{
if ($request->user()->apiKeys->count() >= 5) {
if ($request->user()->tokens->count() >= 10) {
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') ?? [],
]);
// TODO: this should accept an array of different scopes to apply as permissions
// for the token. Right now it allows any account level permission.
$token = $request->user()->createToken($request->input('description'));
return $this->fractal->item($key)
->transformWith($this->getTransformer(ApiKeyTransformer::class))
return $this->fractal->item($token->accessToken)
->transformWith($this->getTransformer(PersonalAccessTokenTransformer::class))
->addMeta([
'secret_token' => $this->encrypter->decrypt($key->token),
'secret_token' => $token->plainTextToken,
])
->toArray();
}
@ -78,15 +78,7 @@ class ApiKeyController extends ClientApiController
*/
public function delete(ClientApiRequest $request, string $identifier): Response
{
$response = $this->repository->deleteWhere([
'key_type' => ApiKey::TYPE_ACCOUNT,
'user_id' => $request->user()->id,
'identifier' => $identifier,
]);
if (!$response) {
throw new NotFoundHttpException();
}
$request->user()->tokens()->where('id', $identifier)->delete();
return $this->returnNoContent();
}

View file

@ -59,7 +59,7 @@ abstract class ClientApiController extends ApplicationApiController
]);
if ($transformer instanceof BaseClientTransformer) {
$transformer->setKey($this->request->attributes->get('api_key'));
// $transformer->setKey($this->request->attributes->get('api_key'));
$transformer->setUser($this->request->user());
}

View file

@ -123,8 +123,6 @@ class LoginController extends AbstractLoginController
]);
}
$this->auth->guard()->login($user, true);
return $this->sendLoginResponse($user, $request);
}
}

View file

@ -32,7 +32,9 @@ use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
class Kernel extends HttpKernel
@ -43,12 +45,11 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
CheckForMaintenanceMode::class,
EncryptCookies::class,
TrustProxies::class,
PreventRequestsDuringMaintenance::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
TrustProxies::class,
];
/**
@ -58,6 +59,7 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
@ -70,19 +72,23 @@ class Kernel extends HttpKernel
'api' => [
IsValidJson::class,
ApiSubstituteBindings::class,
SetSessionDriver::class,
'api..key:' . ApiKey::TYPE_APPLICATION,
EnsureFrontendRequestsAreStateful::class,
// SetSessionDriver::class,
// 'api..key:' . ApiKey::TYPE_APPLICATION,
AuthenticateApplicationUser::class,
AuthenticateIPAccess::class,
// AuthenticateIPAccess::class,
],
'client-api' => [
StartSession::class,
SetSessionDriver::class,
AuthenticateSession::class,
// StartSession::class,
// SetSessionDriver::class,
// AuthenticateSession::class,
IsValidJson::class,
EnsureFrontendRequestsAreStateful::class,
'auth:sanctum',
// 'throttle:api',
SubstituteClientApiBindings::class,
'api..key:' . ApiKey::TYPE_ACCOUNT,
AuthenticateIPAccess::class,
// 'api..key:' . ApiKey::TYPE_ACCOUNT,
// AuthenticateIPAccess::class,
// This is perhaps a little backwards with the Client API, but logically you'd be unable
// to create/get an API key without first enabling 2FA on the account, so I suppose in the
// end it makes sense.

View file

@ -14,6 +14,6 @@ class VerifyCsrfToken extends BaseVerifier
protected $except = [
'remote/*',
'daemon/*',
'api/*',
// 'api/*',
];
}

View file

@ -4,18 +4,6 @@ namespace Pterodactyl\Models;
use Pterodactyl\Services\Acl\Api\AdminAcl;
/**
* @property int $id
* @property int $user_id
* @property int $key_type
* @property string $identifier
* @property string $token
* @property array $allowed_ips
* @property string $memo
* @property \Carbon\Carbon|null $last_used_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class ApiKey extends Model
{
/**

View file

@ -0,0 +1,10 @@
<?php
namespace Pterodactyl\Models;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
public const RESOURCE_NAME = 'personal_access_token';
}

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Models;
use Pterodactyl\Rules\Username;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Support\Collection;
use Illuminate\Auth\Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -12,36 +13,12 @@ use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
/**
* @property int $id
* @property string|null $external_id
* @property string $uuid
* @property string $username
* @property string $email
* @property string $password
* @property string|null $remember_token
* @property string $language
* @property int $admin_role_id
* @property bool $root_admin
* @property bool $use_totp
* @property string|null $totp_secret
* @property \Carbon\Carbon|null $totp_authenticated_at
* @property bool $gravatar
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $name
* @property \Pterodactyl\Models\AdminRole $adminRole
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\UserSSHKey|\Illuminate\Database\Eloquent\Collection $sshKeys
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
* @property \Pterodactyl\Models\WebauthnKey[]|\Illuminate\Database\Eloquent\Collection $webauthnKeys
*/
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
@ -51,6 +28,8 @@ class User extends Model implements
use Authorizable;
use AvailableLanguages;
use CanResetPassword;
use HasApiTokens;
use HasFactory;
use Notifiable;
public const USER_LEVEL_USER = 0;

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Providers;
use Pterodactyl\Models\User;
use Laravel\Sanctum\Sanctum;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Support\Facades\Schema;
@ -10,6 +11,7 @@ use Illuminate\Support\ServiceProvider;
use Pterodactyl\Observers\UserObserver;
use Pterodactyl\Observers\ServerObserver;
use Pterodactyl\Observers\SubuserObserver;
use Pterodactyl\Models\PersonalAccessToken;
class AppServiceProvider extends ServiceProvider
{
@ -23,6 +25,11 @@ class AppServiceProvider extends ServiceProvider
User::observe(UserObserver::class);
Server::observe(ServerObserver::class);
Subuser::observe(SubuserObserver::class);
/**
* @see https://laravel.com/docs/8.x/sanctum#overriding-default-models
*/
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}
/**

View file

@ -48,7 +48,7 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/api-application.php'));
Route::middleware([
sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')),
//sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')),
'client-api',
])->prefix('/api/client')
->namespace($this->namespace . '\Api\Client')

View file

@ -84,11 +84,12 @@ abstract class BaseTransformer extends TransformerAbstract
*/
protected function authorize(string $resource): bool
{
if ($this->getKey()->key_type === ApiKey::TYPE_ACCOUNT && $this->isRootAdmin()) {
return true;
}
return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ);
// if ($this->getKey()->key_type === ApiKey::TYPE_ACCOUNT && $this->isRootAdmin()) {
// return true;
// }
//
// return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ);
}
/**
@ -104,7 +105,7 @@ abstract class BaseTransformer extends TransformerAbstract
{
/** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */
$transformer = Container::getInstance()->makeWith($abstract, $parameters);
$transformer->setKey($this->getKey());
// $transformer->setKey($this->getKey());
if (!$transformer instanceof self) {
throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__);

View file

@ -58,7 +58,7 @@ abstract class BaseClientTransformer extends BaseApplicationTransformer
{
/** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */
$transformer = Container::getInstance()->makeWith($abstract, $parameters);
$transformer->setKey($this->getKey());
// $transformer->setKey($this->getKey());
if (!$transformer instanceof self) {
throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__);

View file

@ -0,0 +1,32 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\PersonalAccessToken;
class PersonalAccessTokenTransformer extends BaseClientTransformer
{
/**
* @return string
*/
public function getResourceName(): string
{
return PersonalAccessToken::RESOURCE_NAME;
}
/**
* @param \Pterodactyl\Models\PersonalAccessToken $model
* @return array
*/
public function transform(PersonalAccessToken $model): array
{
return [
'id' => $model->tokenable_id,
'name' => $model->name,
'abilities' => $model->abilities ?? [],
'last_used_at' => $model->last_used_at ? $model->last_used_at->toIso8601String() : null,
'created_at' => $model->created_at->toIso8601String(),
'updated_at' => $model->updated_at->toIso8601String(),
];
}
}

View file

@ -38,7 +38,8 @@ class ServerTransformer extends BaseClientTransformer
$service = Container::getInstance()->make(StartupCommandService::class);
return [
'server_owner' => $this->getKey()->user_id === $server->owner_id,
'server_owner' => true,
// 'server_owner' => $this->getKey()->user_id === $server->owner_id,
'identifier' => $server->uuidShort,
'internal_id' => $server->id,
'uuid' => $server->uuid,

View file

@ -26,6 +26,7 @@
"laracasts/utilities": "^3.2",
"laravel/framework": "^8.52",
"laravel/helpers": "^1.4",
"laravel/sanctum": "^2.11",
"laravel/tinker": "^2.6",
"laravel/ui": "^3.3",
"lcobucci/jwt": "^4.1",

33
config/cors.php Normal file
View file

@ -0,0 +1,33 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 7200,
'supports_credentials' => true,
];

51
config/sanctum.php Normal file
View file

@ -0,0 +1,51 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,pterodactyl.test',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'verify_csrf_token' => Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => Pterodactyl\Http\Middleware\EncryptCookies::class,
],
];

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}

View file

@ -1,12 +1,11 @@
<?php
use Illuminate\Support\Facades\Route;
use Pterodactyl\Http\Controllers\Api\Client;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
use Pterodactyl\Http\Middleware\Api\Client\Server\ResourceBelongsToServer;
use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
use Pterodactyl\Http\Controllers\Api\Client;
/*
|--------------------------------------------------------------------------
| Client Control API
@ -27,9 +26,9 @@ Route::group(['prefix' => '/account'], function () {
Route::put('/email', 'AccountController@updateEmail')->name('api:client.account.update-email');
Route::put('/password', 'AccountController@updatePassword')->name('api:client.account.update-password');
Route::get('/api-keys', 'ApiKeyController@index');
Route::post('/api-keys', 'ApiKeyController@store');
Route::delete('/api-keys/{identifier}', 'ApiKeyController@delete');
Route::get('/api-keys', [Client\ApiKeyController::class, 'index']);
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
Route::get('/webauthn', 'WebauthnController@index')->withoutMiddleware(RequireTwoFactorAuthentication::class);
Route::get('/webauthn/register', 'WebauthnController@register')->withoutMiddleware(RequireTwoFactorAuthentication::class);
@ -49,7 +48,10 @@ Route::group(['prefix' => '/account'], function () {
| Endpoint: /api/client/servers/{server}
|
*/
Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServerAccess::class, ResourceBelongsToServer::class]], function () {
Route::group([
'prefix' => '/servers/{server}',
'middleware' => [AuthenticateServerAccess::class, ResourceBelongsToServer::class],
], function () {
Route::get('/', 'Servers\ServerController@index')->name('api:client:server.view');
Route::get('/websocket', 'Servers\WebsocketController')->name('api:client:server.ws');
Route::get('/resources', 'Servers\ResourceUtilizationController')->name('api:client:server.resources');