Implement application API Keys

This commit is contained in:
Dane Everitt 2018-01-18 21:36:15 -06:00
parent f9fc3f4370
commit c3b9738364
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
13 changed files with 454 additions and 3 deletions

View file

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

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

View file

@ -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' => [

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

View file

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

View file

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

View file

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

View file

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

View 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
&mdash;
@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

View 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 &amp; 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

View file

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

View file

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