First round of changes to API to support simpler permissions.

This commit is contained in:
Dane Everitt 2018-01-11 22:49:46 -06:00
parent 0e24c669c4
commit a31e5875dc
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
21 changed files with 403 additions and 169 deletions

View file

@ -18,6 +18,7 @@ interface ApiKeyRepositoryInterface extends RepositoryInterface
*
* @param \Pterodactyl\Models\APIKey $model
* @param bool $refresh
* @deprecated
* @return \Pterodactyl\Models\APIKey
*/
public function loadPermissions(APIKey $model, bool $refresh = false): APIKey;

View file

@ -15,6 +15,8 @@ use Pterodactyl\Http\Requests\Admin\UserFormRequest;
use Pterodactyl\Transformers\Api\Admin\UserTransformer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Http\Requests\API\Admin\Users\GetUserRequest;
use Pterodactyl\Http\Requests\API\Admin\Users\GetUsersRequest;
class UserController extends Controller
{
@ -67,19 +69,19 @@ class UserController extends Controller
}
/**
* Handle request to list all users on the panel. Returns a JSONAPI representation
* Handle request to list all users on the panel. Returns a JSON-API representation
* of a collection of users including any defined relations passed in
* the request.
*
* @param \Illuminate\Http\Request $request
* @param \Pterodactyl\Http\Requests\API\Admin\Users\GetUsersRequest $request
* @return array
*/
public function index(Request $request): array
public function index(GetUsersRequest $request): array
{
$users = $this->repository->paginated(100);
return $this->fractal->collection($users)
->transformWith(new UserTransformer($request))
->transformWith((new UserTransformer)->setKey($request->key()))
->withResourceName('user')
->paginateWith(new IlluminatePaginatorAdapter($users))
->toArray();
@ -89,14 +91,14 @@ class UserController extends Controller
* Handle a request to view a single user. Includes any relations that
* were defined in the request.
*
* @param \Illuminate\Http\Request $request
* @param \Pterodactyl\Models\User $user
* @param \Pterodactyl\Http\Requests\API\Admin\Users\GetUserRequest $request
* @param \Pterodactyl\Models\User $user
* @return array
*/
public function view(Request $request, User $user): array
public function view(GetUserRequest $request, User $user): array
{
return $this->fractal->item($user)
->transformWith(new UserTransformer($request))
->transformWith((new UserTransformer)->setKey($request->key()))
->withResourceName('user')
->toArray();
}

View file

@ -25,7 +25,6 @@ use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess;
use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Pterodactyl\Http\Middleware\API\HasPermissionToResource;
use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser;
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
@ -98,9 +97,6 @@ class Kernel extends HttpKernel
'bindings' => SubstituteBindings::class,
'recaptcha' => VerifyReCaptcha::class,
// API specific middleware.
'api..user_level' => HasPermissionToResource::class,
// Server specific middleware (used for authenticating access to resources)
//
// These are only used for individual server authentication, and not gloabl

View file

@ -1,58 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware\API;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class HasPermissionToResource
{
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
*/
private $repository;
/**
* HasPermissionToResource constructor.
*
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
*/
public function __construct(ApiKeyRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Determine if an API key has permission to access the given route.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return mixed
*/
public function handle(Request $request, Closure $next, string $role = 'admin')
{
/** @var \Pterodactyl\Models\APIKey $model */
$model = $request->attributes->get('api_key');
if ($role === 'admin' && ! $request->user()->root_admin) {
throw new NotFoundHttpException;
}
$this->repository->loadPermissions($model);
$routeKey = str_replace(['api.', 'admin.'], '', $request->route()->getName());
$count = $model->getRelation('permissions')->filter(function ($permission) use ($routeKey) {
return $routeKey === str_replace('-', '.', $permission->permission);
})->count();
if ($count === 1) {
return $next($request);
}
throw new AccessDeniedHttpException('Cannot access resource without required `' . $routeKey . '` permission.');
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Pterodactyl\Http\Requests\API\Admin;
use Pterodactyl\Models\APIKey;
use Illuminate\Foundation\Http\FormRequest;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Acl\Api\AdminAcl as Acl;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
abstract class ApiAdminRequest extends FormRequest
{
/**
* The resource that should be checked when performing the authorization
* function for this request.
*
* @var string|null
*/
protected $resource;
/**
* The permission level that a given API key should have for accessing
* the defined $resource during the request cycle.
*
* @var int
*/
protected $permission = Acl::NONE;
/**
* Determine if the current user is authorized to perform
* the requested action aganist the API.
*
* @return bool
*
* @throws \Pterodactyl\Exceptions\PterodactylException
*/
public function authorize(): bool
{
if (is_null($this->resource)) {
throw new PterodactylException('An ACL resource must be defined on API requests.');
}
return Acl::check($this->key(), $this->resource, $this->permission);
}
/**
* Determine if the requested resource exists on the server.
*
* @return bool
*/
public function resourceExists(): bool
{
return true;
}
/**
* Default set of rules to apply to API requests.
*
* @return array
*/
public function rules(): array
{
return [];
}
/**
* Return the API key being used for the request.
*
* @return \Pterodactyl\Models\APIKey
*/
public function key(): APIKey
{
return $this->attributes->get('api_key');
}
/**
* Determine if the request passes the authorization check as well
* as the exists check.
*
* @return bool
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
protected function passesAuthorization()
{
$passes = parent::passesAuthorization();
// Only let the user know that a resource does not exist if they are
// authenticated to access the endpoint. This avoids exposing that
// an item exists (or does not exist) to the user until they can prove
// that they have permission to know about it.
if ($passes && ! $this->resourceExists()) {
throw new NotFoundHttpException('The requested resource does not exist on this server.');
}
return $passes;
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Pterodactyl\Http\Requests\API\Admin\Users;
use Pterodactyl\Models\User;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\API\Admin\ApiAdminRequest;
class GetUserRequest extends ApiAdminRequest
{
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_USERS;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
/**
* Determine if the requested user exists on the Panel.
*
* @return bool
*/
public function resourceExists(): bool
{
$user = $this->route()->parameter('user');
return $user instanceof User && $user->exists;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Pterodactyl\Http\Requests\API\Admin\Users;
use Pterodactyl\Services\Acl\Api\AdminAcl as Acl;
use Pterodactyl\Http\Requests\API\Admin\ApiAdminRequest;
class GetUsersRequest extends ApiAdminRequest
{
/**
* @var string
*/
protected $resource = Acl::RESOURCE_USERS;
/**
* @var int
*/
protected $permission = Acl::READ;
}

View file

@ -2,9 +2,9 @@
namespace Pterodactyl\Http\Requests\API\Remote;
use Pterodactyl\Http\Requests\Request;
use Illuminate\Foundation\Http\FormRequest;
class SftpAuthenticationFormRequest extends Request
class SftpAuthenticationFormRequest extends FormRequest
{
/**
* Authenticate the request.

View file

@ -1,9 +0,0 @@
<?php
namespace Pterodactyl\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{
}

View file

@ -1,17 +1,11 @@
<?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\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
@ -35,14 +29,29 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
*/
protected $casts = [
'allowed_ips' => 'json',
'user_id' => 'int',
'r_' . AdminAcl::RESOURCE_USERS => 'int',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_DATABASES => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_NESTS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_PACKS => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
];
/**
* Fields that are not mass assignable.
* Fields that are mass assignable.
*
* @var array
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
protected $fillable = [
'token',
'allowed_ips',
'memo',
'expires_at',
];
/**
* Rules defining what fields must be passed when making a model.
@ -66,6 +75,24 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
'memo' => 'nullable|string|max:500',
'allowed_ips' => 'nullable|json',
'expires_at' => 'nullable|datetime',
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_DATABASES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NESTS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_PACKS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
];
/**
* @var array
*/
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'expires_at',
];
/**

View file

@ -29,10 +29,6 @@ class RouteServiceProvider extends ServiceProvider
*/
public function map()
{
// Route::middleware(['api'])->prefix('/api/user')
// ->namespace($this->namespace . '\API\User')
// ->group(base_path('routes/api.php'));
Route::middleware(['web', 'auth', 'csrf'])
->namespace($this->namespace . '\Base')
->group(base_path('routes/base.php'));
@ -49,7 +45,7 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php'));
Route::middleware(['api', 'api..user_level:admin'])->prefix('/api/admin')
Route::middleware(['api'])->prefix('/api/admin')
->namespace($this->namespace . '\API\Admin')
->group(base_path('routes/api-admin.php'));

View file

@ -22,6 +22,7 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt
*
* @param \Pterodactyl\Models\APIKey $model
* @param bool $refresh
* @deprecated
* @return \Pterodactyl\Models\APIKey
*/
public function loadPermissions(APIKey $model, bool $refresh = false): APIKey

View file

@ -0,0 +1,66 @@
<?php
namespace Pterodactyl\Services\Acl\Api;
use Pterodactyl\Models\APIKey;
class AdminAcl
{
/**
* Resource permission columns in the api_keys table begin
* with this identifer.
*/
const COLUMN_IDENTIFER = 'r_';
/**
* The different types of permissions available for API keys. This
* implements a read/write/none permissions scheme for all endpoints.
*/
const NONE = 0;
const READ = 1;
const WRITE = 2;
/**
* 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}.
*/
const RESOURCE_SERVERS = 'servers';
const RESOURCE_NODES = 'nodes';
const RESOURCE_ALLOCATIONS = 'allocations';
const RESOURCE_USERS = 'users';
const RESOURCE_LOCATIONS = 'locations';
const RESOURCE_NESTS = 'nests';
const RESOURCE_EGGS = 'eggs';
const RESOURCE_DATABASES = 'databases';
const RESOURCE_PACKS = 'packs';
/**
* Determine if an API key has permission to perform a specific read/write operation.
*
* @param int $permission
* @param int $action
* @return bool
*/
public static function can(int $permission, int $action = self::READ)
{
if ($permission & $action) {
return true;
}
return false;
}
/**
* Determine if an API Key model has permission to access a given resource
* at a specific action level.
*
* @param \Pterodactyl\Models\APIKey $key
* @param string $resource
* @param int $action
* @return bool
*/
public static function check(APIKey $key, string $resource, int $action = self::READ)
{
return self::can(data_get($key, self::COLUMN_IDENTIFER . $resource, self::NONE), $action);
}
}

View file

@ -3,9 +3,9 @@
namespace Pterodactyl\Transformers\Api\Admin;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Transformers\Api\ApiTransformer;
use Pterodactyl\Transformers\Api\BaseTransformer;
class AllocationTransformer extends ApiTransformer
class AllocationTransformer extends BaseTransformer
{
/**
* Relationships that can be loaded onto allocation transformations.

View file

@ -0,0 +1,69 @@
<?php
namespace Pterodactyl\Transformers\Api\Admin;
use Pterodactyl\Models\APIKey;
use Illuminate\Container\Container;
use League\Fractal\TransformerAbstract;
use Pterodactyl\Services\Acl\Api\AdminAcl;
abstract class BaseTransformer extends TransformerAbstract
{
/**
* @var \Pterodactyl\Models\APIKey
*/
private $key;
/**
* Set the HTTP request class being used for this request.
*
* @param \Pterodactyl\Models\APIKey $key
* @return $this
*/
public function setKey(APIKey $key)
{
$this->key = $key;
return $this;
}
/**
* Return the request instance being used for this transformer.
*
* @return \Pterodactyl\Models\APIKey
*/
public function getKey(): APIKey
{
return $this->key;
}
/**
* Determine if the API key loaded onto the transformer has permission
* to access a different resource. This is used when including other
* models on a transformation request.
*
* @param string $resource
* @return bool
*/
protected function authorize(string $resource): bool
{
return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ);
}
/**
* Create a new instance of the transformer and pass along the currently
* set API key.
*
* @param string $abstract
* @param array $parameters
* @return \Pterodactyl\Transformers\Api\Admin\BaseTransformer
*/
protected function makeTransformer(string $abstract, array $parameters = []): self
{
/** @var \Pterodactyl\Transformers\Api\Admin\BaseTransformer $transformer */
$transformer = Container::getInstance()->makeWith($abstract, $parameters);
$transformer->setKey($this->getKey());
return $transformer;
}
}

View file

@ -3,9 +3,9 @@
namespace Pterodactyl\Transformers\Api\Admin;
use Pterodactyl\Models\Location;
use Pterodactyl\Transformers\Api\ApiTransformer;
use Pterodactyl\Transformers\Api\BaseTransformer;
class LocationTransformer extends ApiTransformer
class LocationTransformer extends BaseTransformer
{
/**
* List of resources that can be included.

View file

@ -3,9 +3,9 @@
namespace Pterodactyl\Transformers\Api\Admin;
use Pterodactyl\Models\Node;
use Pterodactyl\Transformers\Api\ApiTransformer;
use Pterodactyl\Transformers\Api\BaseTransformer;
class NodeTransformer extends ApiTransformer
class NodeTransformer extends BaseTransformer
{
/**
* List of resources that can be included.

View file

@ -3,9 +3,9 @@
namespace Pterodactyl\Transformers\Api\Admin;
use Pterodactyl\Models\User;
use Pterodactyl\Transformers\Api\ApiTransformer;
use Pterodactyl\Services\Acl\Api\AdminAcl;
class UserTransformer extends ApiTransformer
class UserTransformer extends BaseTransformer
{
/**
* List of resources that can be included.
@ -29,18 +29,16 @@ class UserTransformer extends ApiTransformer
* Return the servers associated with this user.
*
* @param \Pterodactyl\Models\User $user
* @return bool|\League\Fractal\Resource\Collection
*
* @throws \Pterodactyl\Exceptions\PterodactylException
* @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeServers(User $user)
{
if (! $this->authorize('server-list')) {
return false;
if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) {
return $this->null();
}
$user->loadMissing('servers');
return $this->collection($user->getRelation('servers'), new ServerTransformer($this->getRequest()), 'server');
return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server');
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Pterodactyl\Transformers\Api;
use Illuminate\Http\Request;
use League\Fractal\TransformerAbstract;
use Pterodactyl\Exceptions\PterodactylException;
abstract class ApiTransformer extends TransformerAbstract
{
/**
* @var \Illuminate\Http\Request
*/
private $request;
/**
* Setup request object for transformer.
*
* @param \Illuminate\Http\Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Return the request instance being used for this transformer.
*
* @return \Illuminate\Http\Request
*/
public function getRequest(): Request
{
return $this->request;
}
/**
* Determine if an API key from the request has permission to access
* a resource. This is used when loading includes on the transformed
* models.
*
* @param string $permission
* @return bool
*
* @throws \Pterodactyl\Exceptions\PterodactylException
*/
protected function authorize(string $permission): bool
{
/** @var \Pterodactyl\Models\APIKey $model */
$model = $this->request->attributes->get('api_key');
if (! $model->relationLoaded('permissions')) {
throw new PterodactylException('Permissions must be loaded onto a model before passing to transformer authorize function.');
}
$count = $model->getRelation('permissions')->filter(function ($model) use ($permission) {
return $model->permission === $permission;
})->count();
return $count > 0;
}
}

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddApiKeyPermissionColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedTinyInteger('r_servers')->default(0);
$table->unsignedTinyInteger('r_nodes')->default(0);
$table->unsignedTinyInteger('r_allocations')->default(0);
$table->unsignedTinyInteger('r_users')->default(0);
$table->unsignedTinyInteger('r_locations')->default(0);
$table->unsignedTinyInteger('r_nests')->default(0);
$table->unsignedTinyInteger('r_eggs')->default(0);
$table->unsignedTinyInteger('r_databases')->default(0);
$table->unsignedTinyInteger('r_packs')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn([
'r_servers',
'r_nodes',
'r_allocations',
'r_users',
'r_locations',
'r_nests',
'r_eggs',
'r_databases',
'r_packs',
]);
});
}
}

View file

@ -1,5 +1,7 @@
<?php
use Pterodactyl\Models\User;
/*
|--------------------------------------------------------------------------
| User Controller Routes
@ -9,6 +11,10 @@
|
*/
Route::group(['prefix' => '/users'], function () {
Route::bind('user', function ($value) {
return User::find($value) ?? new User;
});
Route::get('/', 'Users\UserController@index')->name('api.admin.user.list');
Route::get('/{user}', 'Users\UserController@view')->name('api.admin.user.view');