Update interface to begin change to seperate account API keys and application keys

Main difference is permissions, cleaner UI for normal users, and account keys use permissions assigned to servers and subusers while application keys use R/W ACLs stored in the key table.
This commit is contained in:
Dane Everitt 2018-01-14 13:30:55 -06:00
parent 28ebd18f57
commit f9fc3f4370
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
18 changed files with 312 additions and 298 deletions

View file

@ -0,0 +1,58 @@
<?php
namespace Pterodactyl\Console\Commands\Migration;
use Pterodactyl\Models\ApiKey;
use Illuminate\Console\Command;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
class CleanOrphanedApiKeysCommand extends Command
{
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
*/
private $repository;
/**
* @var string
*/
protected $signature = 'p:migration:clean-orphaned-keys';
/**
* @var string
*/
protected $description = 'Cleans API keys from the database that are not assigned a specific role.';
/**
* CleanOrphanedApiKeysCommand constructor.
*
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
*/
public function __construct(ApiKeyRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
/**
* Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7.
*
* @return null|void
*/
public function handle()
{
$count = $this->repository->findCountWhere([['key_type', '=', ApiKey::TYPE_NONE]]);
$continue = $this->confirm(
'This action will remove ' . $count . ' keys from the database. Are you sure you wish to continue?', false
);
if (! $continue) {
return null;
}
$this->info('Deleting keys...');
$this->repository->deleteWhere([['key_type', '=', ApiKey::TYPE_NONE]]);
$this->info('Keys were successfully deleted.');
}
}

View file

@ -1,25 +1,26 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\ApiKey;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
interface ApiKeyRepositoryInterface extends RepositoryInterface
{
/**
* Load permissions for a key onto the model.
* Get all of the account API keys that exist for a specific user.
*
* @param \Pterodactyl\Models\ApiKey $model
* @param bool $refresh
* @deprecated
* @return \Pterodactyl\Models\ApiKey
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function loadPermissions(ApiKey $model, bool $refresh = false): ApiKey;
public function getAccountKeys(User $user): Collection;
/**
* Delete an account API key from the panel for a specific user.
*
* @param \Pterodactyl\Models\User $user
* @param string $identifier
* @return int
*/
public function deleteAccountKey(User $user, string $identifier): int;
}

View file

@ -2,14 +2,17 @@
namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\ApiKey;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Api\KeyCreationService;
use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest;
use Pterodactyl\Http\Requests\Base\StoreAccountKeyRequest;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
class APIController extends Controller
class AccountKeyController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
@ -44,49 +47,44 @@ class APIController extends Controller
}
/**
* Display base API index page.
* Display a listing of all account API keys.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request)
public function index(Request $request): View
{
return view('base.api.index', [
'keys' => $this->repository->findWhere([['user_id', '=', $request->user()->id]]),
'keys' => $this->repository->getAccountKeys($request->user()),
]);
}
/**
* Display API key creation page.
* Display account API key creation page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function create(Request $request)
public function create(Request $request): View
{
return view('base.api.new');
}
/**
* Handle saving new API key.
* Handle saving new account API key.
*
* @param \Pterodactyl\Http\Requests\Base\ApiKeyFormRequest $request
* @param \Pterodactyl\Http\Requests\Base\StoreAccountKeyRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(ApiKeyFormRequest $request)
public function store(StoreAccountKeyRequest $request)
{
$adminPermissions = [];
if ($request->user()->root_admin) {
$adminPermissions = $request->input('admin_permissions', []);
}
$secret = $this->keyService->handle([
$this->keyService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([
'user_id' => $request->user()->id,
'allowed_ips' => $request->input('allowed_ips'),
'memo' => $request->input('memo'),
], $request->input('permissions', []), $adminPermissions);
]);
$this->alert->success(trans('base.api.index.keypair_created'))->flash();
@ -94,18 +92,15 @@ class APIController extends Controller
}
/**
* @param \Illuminate\Http\Request $request
* @param string $key
* @return \Illuminate\Http\Response
* Delete an account API key from the Panel via an AJAX request.
*
* @throws \Exception
* @param \Illuminate\Http\Request $request
* @param string $identifier
* @return \Illuminate\Http\Response
*/
public function revoke(Request $request, $key)
public function revoke(Request $request, string $identifier): Response
{
$this->repository->deleteWhere([
['user_id', '=', $request->user()->id],
['token', '=', $key],
]);
$this->repository->deleteAccountKey($request->user(), $identifier);
return response('', 204);
}

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Middleware\Api\Admin;
use Closure;
use Cake\Chronos\Chronos;
use Illuminate\Http\Request;
use Pterodactyl\Models\ApiKey;
use Illuminate\Auth\AuthManager;
@ -51,8 +52,8 @@ class AuthenticateKey
* @param \Closure $next
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Request $request, Closure $next)
{
@ -65,7 +66,10 @@ class AuthenticateKey
$token = substr($raw, ApiKey::IDENTIFIER_LENGTH);
try {
$model = $this->repository->findFirstWhere([['identifier', '=', $identifier]]);
$model = $this->repository->findFirstWhere([
['identifier', '=', $identifier],
['key_type', '=', ApiKey::TYPE_APPLICATION],
]);
} catch (RecordNotFoundException $exception) {
throw new AccessDeniedHttpException;
}
@ -76,6 +80,7 @@ class AuthenticateKey
$this->auth->guard()->loginUsingId($model->user_id);
$request->attributes->set('api_key', $model);
$this->repository->withoutFreshModel()->update($model->id, ['last_used_at' => Chronos::now()]);
return $next($request);
}

View file

@ -0,0 +1,23 @@
<?php
namespace Pterodactyl\Http\Requests\Base;
use Pterodactyl\Http\Requests\FrontendUserFormRequest;
class StoreAccountKeyRequest extends FrontendUserFormRequest
{
/**
* Rules to validate the request input aganist before storing
* an account API key.
*
* @return array
*/
public function rules()
{
return [
'memo' => 'required|nullable|string|max:500',
'allowed_ips' => 'present',
'allowed_ips.*' => 'sometimes|string',
];
}
}

View file

@ -6,7 +6,6 @@ use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Illuminate\Contracts\Encryption\Encrypter;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
@ -18,7 +17,7 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract
* Different API keys that can exist on the system.
*/
const TYPE_NONE = 0;
const TYPE_USER = 1;
const TYPE_ACCOUNT = 1;
const TYPE_APPLICATION = 2;
const TYPE_DAEMON_USER = 3;
const TYPE_DAEMON_APPLICATION = 4;
@ -70,6 +69,7 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract
'token',
'allowed_ips',
'memo',
'last_used_at',
];
/**
@ -90,6 +90,7 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract
'memo' => 'required',
'user_id' => 'required',
'token' => 'required',
'key_type' => 'present',
];
/**
@ -99,6 +100,7 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract
*/
protected static $dataIntegrityRules = [
'user_id' => 'exists:users,id',
'key_type' => 'integer|min:0|max:4',
'identifier' => 'string|size:16|unique:api_keys,identifier',
'token' => 'string',
'memo' => 'nullable|string|max:500',
@ -123,14 +125,4 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract
self::UPDATED_AT,
'last_used_at',
];
/**
* Return a decrypted version of the token.
*
* @return string
*/
public function getDecryptedTokenAttribute()
{
return app()->make(Encrypter::class)->decrypt($this->token);
}
}

View file

@ -2,7 +2,9 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Pterodactyl\Models\ApiKey;
use Illuminate\Support\Collection;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInterface
@ -18,19 +20,30 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt
}
/**
* Load permissions for a key onto the model.
* Get all of the account API keys that exist for a specific user.
*
* @param \Pterodactyl\Models\ApiKey $model
* @param bool $refresh
* @deprecated
* @return \Pterodactyl\Models\ApiKey
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function loadPermissions(ApiKey $model, bool $refresh = false): ApiKey
public function getAccountKeys(User $user): Collection
{
if (! $model->relationLoaded('permissions') || $refresh) {
$model->load('permissions');
}
return $this->getBuilder()->where('user_id', $user->id)
->where('key_type', ApiKey::TYPE_ACCOUNT)
->get($this->getColumns());
}
return $model;
/**
* Delete an account API key from the panel for a specific user.
*
* @param \Pterodactyl\Models\User $user
* @param string $identifier
* @return int
*/
public function deleteAccountKey(User $user, string $identifier): int
{
return $this->getBuilder()->where('user_id', $user->id)
->where('key_type', ApiKey::TYPE_ACCOUNT)
->where('identifier', $identifier)
->delete();
}
}

View file

@ -13,6 +13,11 @@ class KeyCreationService
*/
private $encrypter;
/**
* @var int
*/
private $keyType = ApiKey::TYPE_NONE;
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
*/
@ -30,23 +35,43 @@ class KeyCreationService
$this->repository = $repository;
}
/**
* Set the type of key that should be created. By default an orphaned key will be
* created. These keys cannot be used for anything, and will not render in the UI.
*
* @param int $type
* @return \Pterodactyl\Services\Api\KeyCreationService
*/
public function setKeyType(int $type)
{
$this->keyType = $type;
return $this;
}
/**
* Create a new API key for the Panel using the permissions passed in the data request.
* This will automatically generate an identifer and an encrypted token that are
* stored in the database.
*
* @param array $data
* @param array $permissions
* @return \Pterodactyl\Models\ApiKey
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(array $data): ApiKey
public function handle(array $data, array $permissions = []): ApiKey
{
$data = array_merge($data, [
'key_type' => $this->keyType,
'identifier' => str_random(ApiKey::IDENTIFIER_LENGTH),
'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)),
]);
if ($this->keyType === ApiKey::TYPE_APPLICATION) {
$data = array_merge($data, $permissions);
}
$instance = $this->repository->create($data, true, true);
return $instance;