Implement application API Keys
This commit is contained in:
parent
f9fc3f4370
commit
c3b9738364
13 changed files with 454 additions and 3 deletions
|
@ -15,6 +15,14 @@ interface ApiKeyRepositoryInterface extends RepositoryInterface
|
|||
*/
|
||||
public function getAccountKeys(User $user): Collection;
|
||||
|
||||
/**
|
||||
* Get all of the application API keys that exist for a specific user.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getApplicationKeys(User $user): Collection;
|
||||
|
||||
/**
|
||||
* Delete an account API key from the panel for a specific user.
|
||||
*
|
||||
|
@ -23,4 +31,13 @@ interface ApiKeyRepositoryInterface extends RepositoryInterface
|
|||
* @return int
|
||||
*/
|
||||
public function deleteAccountKey(User $user, string $identifier): int;
|
||||
|
||||
/**
|
||||
* Delete an application API key from the panel for a specific user.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @param string $identifier
|
||||
* @return int
|
||||
*/
|
||||
public function deleteApplicationKey(User $user, string $identifier): int;
|
||||
}
|
||||
|
|
117
app/Http/Controllers/Admin/ApplicationApiController.php
Normal file
117
app/Http/Controllers/Admin/ApplicationApiController.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Api\KeyCreationService;
|
||||
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
|
||||
use Pterodactyl\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest;
|
||||
|
||||
class ApplicationApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Prologue\Alerts\AlertsMessageBag
|
||||
*/
|
||||
private $alert;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Api\KeyCreationService
|
||||
*/
|
||||
private $keyCreationService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ApplicationApiController constructor.
|
||||
*
|
||||
* @param \Prologue\Alerts\AlertsMessageBag $alert
|
||||
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Services\Api\KeyCreationService $keyCreationService
|
||||
*/
|
||||
public function __construct(
|
||||
AlertsMessageBag $alert,
|
||||
ApiKeyRepositoryInterface $repository,
|
||||
KeyCreationService $keyCreationService
|
||||
) {
|
||||
$this->alert = $alert;
|
||||
$this->keyCreationService = $keyCreationService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render view showing all of a user's application API keys.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('admin.api.index', [
|
||||
'keys' => $this->repository->getApplicationKeys($request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render view allowing an admin to create a new application API key.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$resources = AdminAcl::getResourceList();
|
||||
sort($resources);
|
||||
|
||||
return view('admin.api.new', [
|
||||
'resources' => $resources,
|
||||
'permissions' => [
|
||||
'r' => AdminAcl::READ,
|
||||
'rw' => AdminAcl::READ | AdminAcl::WRITE,
|
||||
'n' => AdminAcl::NONE,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the new key and redirect the user back to the application key listing.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(StoreApplicationApiKeyRequest $request): RedirectResponse
|
||||
{
|
||||
$this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
|
||||
'memo' => $request->input('memo'),
|
||||
'user_id' => $request->user()->id,
|
||||
], $request->getKeyPermissions());
|
||||
|
||||
$this->alert->success('A new application API key has been generated for your account.')->flash();
|
||||
|
||||
return redirect()->route('admin.api.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an application API key from the database.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $identifier
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function delete(Request $request, string $identifier): Response
|
||||
{
|
||||
$this->repository->deleteApplicationKey($request->user(), $identifier);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
|
|||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateUser;
|
||||
use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser;
|
||||
|
@ -66,10 +67,11 @@ class Kernel extends HttpKernel
|
|||
RequireTwoFactorAuthentication::class,
|
||||
],
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'throttle:120,1',
|
||||
SubstituteBindings::class,
|
||||
SetSessionDriver::class,
|
||||
AuthenticateKey::class,
|
||||
AuthenticateUser::class,
|
||||
AuthenticateIPAccess::class,
|
||||
],
|
||||
'daemon' => [
|
||||
|
|
27
app/Http/Middleware/Api/Admin/AuthenticateUser.php
Normal file
27
app/Http/Middleware/Api/Admin/AuthenticateUser.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Admin;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AuthenticateUser
|
||||
{
|
||||
/**
|
||||
* Authenticate that the currently authenticated user is an administrator
|
||||
* and should be allowed to proceede through the application API.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (is_null($request->user()) || ! $request->user()->root_admin) {
|
||||
throw new AccessDeniedHttpException;
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Admin\Api;
|
||||
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Pterodactyl\Services\Acl\Api\AdminAcl;
|
||||
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class StoreApplicationApiKeyRequest extends AdminFormRequest
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$modelRules = ApiKey::getCreateRules();
|
||||
|
||||
return collect(AdminAcl::getResourceList())->mapWithKeys(function ($resource) use ($modelRules) {
|
||||
return [AdminAcl::COLUMN_IDENTIFER . $resource => $modelRules['r_' . $resource]];
|
||||
})->merge(['memo' => $modelRules['memo']])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function attributes()
|
||||
{
|
||||
return [
|
||||
'memo' => 'Description',
|
||||
];
|
||||
}
|
||||
|
||||
public function getKeyPermissions(): array
|
||||
{
|
||||
return collect($this->validated())->filter(function ($value, $key) {
|
||||
return substr($key, 0, strlen(AdminAcl::COLUMN_IDENTIFER)) === AdminAcl::COLUMN_IDENTIFER;
|
||||
})->toArray();
|
||||
}
|
||||
}
|
|
@ -32,6 +32,19 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt
|
|||
->get($this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the application API keys that exist for a specific user.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getApplicationKeys(User $user): Collection
|
||||
{
|
||||
return $this->getBuilder()->where('user_id', $user->id)
|
||||
->where('key_type', ApiKey::TYPE_APPLICATION)
|
||||
->get($this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an account API key from the panel for a specific user.
|
||||
*
|
||||
|
@ -46,4 +59,19 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt
|
|||
->where('identifier', $identifier)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an application API key from the panel for a specific user.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @param string $identifier
|
||||
* @return int
|
||||
*/
|
||||
public function deleteApplicationKey(User $user, string $identifier): int
|
||||
{
|
||||
return $this->getBuilder()->where('user_id', $user->id)
|
||||
->where('key_type', ApiKey::TYPE_APPLICATION)
|
||||
->where('identifier', $identifier)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Services\Acl\Api;
|
||||
|
||||
use ReflectionClass;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
|
||||
class AdminAcl
|
||||
|
@ -22,7 +23,7 @@ class AdminAcl
|
|||
|
||||
/**
|
||||
* Resources that are available on the API and can contain a permissions
|
||||
* set for each key. These are stored in the database as permission_{resource}.
|
||||
* set for each key. These are stored in the database as r_{resource}.
|
||||
*/
|
||||
const RESOURCE_SERVERS = 'servers';
|
||||
const RESOURCE_NODES = 'nodes';
|
||||
|
@ -63,4 +64,18 @@ class AdminAcl
|
|||
{
|
||||
return self::can(data_get($key, self::COLUMN_IDENTIFER . $resource, self::NONE), $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all resource constants defined in this ACL.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getResourceList(): array
|
||||
{
|
||||
$reflect = new ReflectionClass(__CLASS__);
|
||||
|
||||
return collect($reflect->getConstants())->filter(function ($value, $key) {
|
||||
return substr($key, 0, 9) === 'RESOURCE_';
|
||||
})->values()->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,12 @@ class AddLastUsedAtColumn extends Migration
|
|||
$table->unsignedTinyInteger('key_type')->after('user_id')->default(0);
|
||||
$table->timestamp('last_used_at')->after('memo')->nullable();
|
||||
$table->dropColumn('expires_at');
|
||||
|
||||
$table->dropForeign(['user_id']);
|
||||
});
|
||||
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,6 +36,11 @@ class AddLastUsedAtColumn extends Migration
|
|||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->timestamp('expires_at')->after('memo')->nullable();
|
||||
$table->dropColumn('last_used_at', 'key_type');
|
||||
$table->dropForeign(['user_id']);
|
||||
});
|
||||
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
103
resources/themes/pterodactyl/admin/api/index.blade.php
Normal file
103
resources/themes/pterodactyl/admin/api/index.blade.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title')
|
||||
Application API
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Application API<small>Control access credentials for manging this Panel via the API.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Application API</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Credentials List</h3>
|
||||
<div class="box-tools">
|
||||
<a href="{{ route('admin.api.new') }}" class="btn btn-sm btn-primary">Create New</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body table-responsive no-padding">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Memo</th>
|
||||
<th>Last Used</th>
|
||||
<th>Created</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@foreach($keys as $key)
|
||||
<tr>
|
||||
<td><code>{{ $key->identifier }}{{ decrypt($key->token) }}</code></td>
|
||||
<td>{{ $key->memo }}</td>
|
||||
<td>
|
||||
@if(!is_null($key->last_used_at))
|
||||
@datetimeHuman($key->last_used_at)
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>@datetimeHuman($key->created_at)</td>
|
||||
<td>
|
||||
<a href="#" data-action="revoke-key" data-attr="{{ $key->identifier }}">
|
||||
<i class="fa fa-trash-o text-danger"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('[data-action="revoke-key"]').click(function (event) {
|
||||
var self = $(this);
|
||||
event.preventDefault();
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Revoke API Key',
|
||||
text: 'Once this API key is revoked any applications currently using it will stop working.',
|
||||
showCancelButton: true,
|
||||
allowOutsideClick: true,
|
||||
closeOnConfirm: false,
|
||||
confirmButtonText: 'Revoke',
|
||||
confirmButtonColor: '#d9534f',
|
||||
showLoaderOnConfirm: true
|
||||
}, function () {
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: Router.route('admin.api.delete', { identifier: self.data('attr') }),
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
}).done(function () {
|
||||
swal({
|
||||
type: 'success',
|
||||
title: '',
|
||||
text: 'API Key has been revoked.'
|
||||
});
|
||||
self.parent().parent().slideUp();
|
||||
}).fail(function (jqXHR) {
|
||||
console.error(jqXHR);
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Whoops!',
|
||||
text: 'An error occured while attempting to revoke this key.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
70
resources/themes/pterodactyl/admin/api/new.blade.php
Normal file
70
resources/themes/pterodactyl/admin/api/new.blade.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title')
|
||||
Application API
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Application API<small>Create a new application API key.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li><a href="{{ route('admin.api.index') }}">Application API</a></li>
|
||||
<li class="active">New Credentials</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<form method="POST" action="{{ route('admin.api.new') }}">
|
||||
<div class="col-sm-8 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Select Permissions</h3>
|
||||
</div>
|
||||
<div class="box-body table-responsive no-padding">
|
||||
<table class="table table-hover">
|
||||
@foreach($resources as $resource)
|
||||
<tr>
|
||||
<td class="col-sm-3 strong">{{ title_case($resource) }}</td>
|
||||
<td class="col-sm-3 radio radio-primary text-center">
|
||||
<input type="radio" id="r_{{ $resource }}" name="r_{{ $resource }}" value="{{ $permissions['r'] }}">
|
||||
<label for="r_{{ $resource }}">Read</label>
|
||||
</td>
|
||||
<td class="col-sm-3 radio radio-primary text-center">
|
||||
<input type="radio" id="rw_{{ $resource }}" name="r_{{ $resource }}" value="{{ $permissions['rw'] }}">
|
||||
<label for="rw_{{ $resource }}">Read & Write</label>
|
||||
</td>
|
||||
<td class="col-sm-3 radio text-center">
|
||||
<input type="radio" id="n_{{ $resource }}" name="r_{{ $resource }}" value="{{ $permissions['n'] }}" checked>
|
||||
<label for="n_{{ $resource }}">None</label>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="memoField">Description <span class="field-required"></span></label>
|
||||
<input id="memoField" type="text" name="memo" class="form-control">
|
||||
</div>
|
||||
<p class="text-muted">Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it. If you need to make changes down the road you will need to create a new set of credentials.</p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-success btn-sm pull-right">Create Credentials</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
<script>
|
||||
</script>
|
||||
@endsection
|
|
@ -85,6 +85,11 @@
|
|||
<i class="fa fa-wrench"></i> <span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}">
|
||||
<a href="{{ route('admin.api.index')}}">
|
||||
<i class="fa fa-gamepad"></i> <span>Application API</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="header">MANAGEMENT</li>
|
||||
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.databases') ?: 'active' }}">
|
||||
<a href="{{ route('admin.databases') }}">
|
||||
|
|
|
@ -2,6 +2,23 @@
|
|||
|
||||
Route::get('/', 'BaseController@index')->name('admin.index');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Location Controller Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /admin/api
|
||||
|
|
||||
*/
|
||||
Route::group(['prefix' => 'api'], function () {
|
||||
Route::get('/', 'ApplicationApiController@index')->name('admin.api.index');
|
||||
Route::get('/new', 'ApplicationApiController@create')->name('admin.api.new');
|
||||
|
||||
Route::post('/new', 'ApplicationApiController@store');
|
||||
|
||||
Route::delete('/revoke/{identifier}', 'ApplicationApiController@delete')->name('admin.api.delete');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Location Controller Routes
|
||||
|
|
Loading…
Reference in a new issue