Implemented controller permissions

This commit is contained in:
Igor Scheller 2018-11-14 02:17:27 +01:00 committed by msquare
parent 55beca95cd
commit c9d7e88cc7
13 changed files with 265 additions and 7 deletions

View File

@ -4,5 +4,16 @@ namespace Engelsystem\Controllers;
abstract class BaseController abstract class BaseController
{ {
/** @var string[]|string[][] A list of Permissions required to access the controller or certain pages */
protected $permissions = [];
/**
* Returns the list of permissions
*
* @return string[]|string[][]
*/
public function getPermissions()
{
return $this->permissions;
}
} }

View File

@ -13,5 +13,6 @@ class AuthenticatorServiceProvider extends ServiceProvider
$this->app->instance(Authenticator::class, $authenticator); $this->app->instance(Authenticator::class, $authenticator);
$this->app->instance('authenticator', $authenticator); $this->app->instance('authenticator', $authenticator);
$this->app->instance('auth', $authenticator);
} }
} }

View File

@ -0,0 +1,23 @@
<?php
namespace Engelsystem\Http\Exceptions;
use Throwable;
class HttpForbidden extends HttpException
{
/**
* @param string $message
* @param array $headers
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(
string $message = '',
array $headers = [],
int $code = 0,
Throwable $previous = null
) {
parent::__construct(403, $message, $headers, $code, $previous);
}
}

View File

@ -363,7 +363,7 @@ class Request extends SymfonyRequest implements ServerRequestInterface
foreach ($uploadedFiles as $file) { foreach ($uploadedFiles as $file) {
/** @var UploadedFileInterface $file */ /** @var UploadedFileInterface $file */
$filename = tempnam(sys_get_temp_dir(), 'upload'); $filename = tempnam(sys_get_temp_dir(), 'upload');
$handle = fopen($filename, "w"); $handle = fopen($filename, 'w');
fwrite($handle, $file->getStream()->getContents()); fwrite($handle, $file->getStream()->getContents());
fclose($handle); fclose($handle);

View File

@ -74,4 +74,12 @@ class CallableHandler implements MiddlewareInterface, RequestHandlerInterface
$response = $this->container->get('response'); $response = $this->container->get('response');
return $response->withContent($return); return $response->withContent($return);
} }
/**
* @return callable
*/
public function getCallable()
{
return $this->callable;
}
} }

View File

@ -3,6 +3,9 @@
namespace Engelsystem\Middleware; namespace Engelsystem\Middleware;
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpForbidden;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@ -37,6 +40,14 @@ class RequestHandler implements MiddlewareInterface
$requestHandler = $request->getAttribute('route-request-handler'); $requestHandler = $request->getAttribute('route-request-handler');
$requestHandler = $this->resolveRequestHandler($requestHandler); $requestHandler = $this->resolveRequestHandler($requestHandler);
if ($requestHandler instanceof CallableHandler) {
$callable = $requestHandler->getCallable();
if (is_array($callable) && $callable[0] instanceof BaseController) {
$this->checkPermissions($callable[0], $callable[1]);
}
}
if ($requestHandler instanceof MiddlewareInterface) { if ($requestHandler instanceof MiddlewareInterface) {
return $requestHandler->process($request, $handler); return $requestHandler->process($request, $handler);
} }
@ -49,6 +60,8 @@ class RequestHandler implements MiddlewareInterface
} }
/** /**
* Resolve the given class
*
* @param string|callable|MiddlewareInterface|RequestHandlerInterface $handler * @param string|callable|MiddlewareInterface|RequestHandlerInterface $handler
* @return MiddlewareInterface|RequestHandlerInterface * @return MiddlewareInterface|RequestHandlerInterface
*/ */
@ -76,4 +89,36 @@ class RequestHandler implements MiddlewareInterface
return $this->resolveMiddleware($handler); return $this->resolveMiddleware($handler);
} }
/**
* Check required page permissions
*
* @param BaseController $controller
* @param string $method
* @return bool
*/
protected function checkPermissions(BaseController $controller, string $method): bool
{
/** @var Authenticator $auth */
$auth = $this->container->get('auth');
$permissions = $controller->getPermissions();
// Merge action permissions
if (isset($permissions[$method])) {
$permissions = array_merge($permissions, (array)$permissions[$method]);
}
foreach ($permissions as $key => $permission) {
// Skip all action permission entries
if (!is_int($key)) {
continue;
}
if (!$auth->can($permission)) {
throw new HttpForbidden();
}
}
return true;
}
} }

View File

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Test\Unit\Controllers\Stub\ControllerImplementation;
use PHPUnit\Framework\TestCase;
class BaseControllerTest extends TestCase
{
/**
* @covers \Engelsystem\Controllers\BaseController::getPermissions
*/
public function testGetPermissions()
{
$controller = new ControllerImplementation();
$this->assertEquals([
'foo',
'lorem' => [
'ipsum',
'dolor',
],
], $controller->getPermissions());
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Test\Unit\Controllers\Stub;
use Engelsystem\Controllers\BaseController;
class ControllerImplementation extends BaseController
{
/** @var array */
protected $permissions = [
'foo',
'lorem' => [
'ipsum',
'dolor',
],
];
/**
* @param array $permissions
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
}
}

View File

@ -24,5 +24,6 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest
$this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class)); $this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class));
$this->assertInstanceOf(Authenticator::class, $app->get('authenticator')); $this->assertInstanceOf(Authenticator::class, $app->get('authenticator'));
$this->assertInstanceOf(Authenticator::class, $app->get('auth'));
} }
} }

View File

@ -0,0 +1,22 @@
<?php
namespace Engelsystem\Test\Unit\Http\Exceptions;
use Engelsystem\Http\Exceptions\HttpForbidden;
use PHPUnit\Framework\TestCase;
class HttpForbiddenTest extends TestCase
{
/**
* @covers \Engelsystem\Http\Exceptions\HttpForbidden::__construct
*/
public function testConstruct()
{
$exception = new HttpForbidden();
$this->assertEquals(403, $exception->getStatusCode());
$this->assertEquals('', $exception->getMessage());
$exception = new HttpForbidden('Go away!');
$this->assertEquals('Go away!', $exception->getMessage());
}
}

View File

@ -11,7 +11,6 @@ use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use ReflectionClass as Reflection;
use stdClass; use stdClass;
class CallableHandlerTest extends TestCase class CallableHandlerTest extends TestCase
@ -28,17 +27,14 @@ class CallableHandlerTest extends TestCase
/** /**
* @dataProvider provideCallable * @dataProvider provideCallable
* @covers \Engelsystem\Middleware\CallableHandler::__construct * @covers \Engelsystem\Middleware\CallableHandler::__construct
* @covers \Engelsystem\Middleware\CallableHandler::getCallable
* @param callable $callable * @param callable $callable
*/ */
public function testInit($callable) public function testInit($callable)
{ {
$handler = new CallableHandler($callable); $handler = new CallableHandler($callable);
$reflection = new Reflection(get_class($handler)); $this->assertEquals($callable, $handler->getCallable());
$property = $reflection->getProperty('callable');
$property->setAccessible(true);
$this->assertEquals($callable, $property->getValue($handler));
} }
/** /**

View File

@ -3,7 +3,11 @@
namespace Engelsystem\Test\Unit\Middleware; namespace Engelsystem\Test\Unit\Middleware;
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Middleware\CallableHandler;
use Engelsystem\Middleware\RequestHandler; use Engelsystem\Middleware\RequestHandler;
use Engelsystem\Test\Unit\Middleware\Stub\ControllerImplementation;
use InvalidArgumentException; use InvalidArgumentException;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -131,6 +135,79 @@ class RequestHandlerTest extends TestCase
$this->assertEquals($return, $response); $this->assertEquals($return, $response);
} }
/**
* @covers \Engelsystem\Middleware\RequestHandler::process
* @covers \Engelsystem\Middleware\RequestHandler::checkPermissions
*/
public function testCheckPermissions()
{
/** @var Application|MockObject $container */
/** @var ServerRequestInterface|MockObject $request */
/** @var RequestHandlerInterface|MockObject $handler */
/** @var ResponseInterface|MockObject $response */
list($container, $request, $handler, $response) = $this->getMocks();
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
$class = new ControllerImplementation();
/** @var CallableHandler|MockObject $callable */
$callable = $this->getMockBuilder(CallableHandler::class)
->setConstructorArgs([[$class, 'actionStub']])
->getMock();
$callable->expects($this->exactly(2))
->method('getCallable')
->willReturn([$class, 'actionStub']);
$callable->expects($this->exactly(1))
->method('process')
->with($request, $handler)
->willReturn($response);
$request->expects($this->exactly(2))
->method('getAttribute')
->with('route-request-handler')
->willReturn($callable);
/** @var RequestHandler|MockObject $middleware */
$middleware = $this->getMockBuilder(RequestHandler::class)
->setConstructorArgs([$container])
->setMethods(['resolveRequestHandler'])
->getMock();
$middleware->expects($this->exactly(2))
->method('resolveRequestHandler')
->with($callable)
->willReturn($callable);
$container->expects($this->exactly(2))
->method('get')
->with('auth')
->willReturn($auth);
$hasPermissions = [];
$auth->expects($this->atLeastOnce())
->method('can')
->willReturnCallback(function ($permission) use (&$hasPermissions) {
return in_array($permission, $hasPermissions);
});
$hasPermissions = ['foo', 'test', 'user'];
$class->setPermissions([
'foo',
'loremIpsumAction' => ['dolor', 'sit'],
'actionStub' => ['test'],
'user',
]);
$middleware->process($request, $handler);
$class->setPermissions(array_merge(['not.existing.permission'], $hasPermissions));
$this->expectException(HttpForbidden::class);
$middleware->process($request, $handler);
}
/** /**
* @return array * @return array
*/ */

View File

@ -0,0 +1,24 @@
<?php
namespace Engelsystem\Test\Unit\Middleware\Stub;
use Engelsystem\Controllers\BaseController;
class ControllerImplementation extends BaseController
{
/**
* @param array $permissions
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
}
/**
* @return string
*/
public function actionStub()
{
return '';
}
}