1
0
mirror of https://github.com/flarum/core.git synced 2025-07-23 09:41:26 +02:00

WIP sudo mode, better error responses

This commit is contained in:
Toby Zerner
2015-11-05 16:17:00 +10:30
parent 0561629de8
commit 1787af4850
26 changed files with 266 additions and 37 deletions

View File

@@ -10,38 +10,22 @@
namespace Flarum\Admin\Middleware; namespace Flarum\Admin\Middleware;
use Flarum\Core\Access\Gate; use Flarum\Core\Access\AssertPermissionTrait;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Flarum\Core\Exception\PermissionDeniedException;
use Zend\Stratigility\MiddlewareInterface; use Zend\Stratigility\MiddlewareInterface;
class RequireAdministrateAbility implements MiddlewareInterface class RequireAdministrateAbility implements MiddlewareInterface
{ {
/** use AssertPermissionTrait;
* @var Gate
*/
protected $gate;
/**
* @param Gate $gate
*/
public function __construct(Gate $gate)
{
$this->gate = $gate;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function __invoke(Request $request, Response $response, callable $out = null)
{ {
$actor = $request->getAttribute('actor'); $this->assertAdminAndSudo($request);
if (! $this->gate->forUser($actor)->allows('administrate')) {
throw new PermissionDeniedException;
}
return $out ? $out($request, $response) : $response; return $out ? $out($request, $response) : $response;
} }

View File

@@ -18,6 +18,7 @@ use DateTime;
* @property int $user_id * @property int $user_id
* @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $expires_at * @property \Carbon\Carbon $expires_at
* @property \Carbon\Carbon $sudo_expires_at
* @property \Flarum\Core\User|null $user * @property \Flarum\Core\User|null $user
*/ */
class AccessToken extends AbstractModel class AccessToken extends AbstractModel
@@ -37,7 +38,7 @@ class AccessToken extends AbstractModel
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected $dates = ['created_at', 'expires_at']; protected $dates = ['created_at', 'expires_at', 'sudo_expires_at'];
/** /**
* Generate an access token for the specified user. * Generate an access token for the specified user.
@@ -54,6 +55,7 @@ class AccessToken extends AbstractModel
$token->user_id = $userId; $token->user_id = $userId;
$token->created_at = time(); $token->created_at = time();
$token->expires_at = time() + $minutes * 60; $token->expires_at = time() + $minutes * 60;
$token->sudo_expires_at = time() + $minutes * 30;
return $token; return $token;
} }
@@ -68,6 +70,11 @@ class AccessToken extends AbstractModel
return $this->expires_at > new DateTime; return $this->expires_at > new DateTime;
} }
public function isSudo()
{
return $this->sudo_expires_at > new DateTime;
}
/** /**
* Define the relationship with the owner of this access token. * Define the relationship with the owner of this access token.
* *

View File

@@ -45,8 +45,11 @@ class ApiServiceProvider extends AbstractServiceProvider
$handler->registerHandler(new Handler\FloodingExceptionHandler); $handler->registerHandler(new Handler\FloodingExceptionHandler);
$handler->registerHandler(new Handler\IlluminateValidationExceptionHandler); $handler->registerHandler(new Handler\IlluminateValidationExceptionHandler);
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler); $handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler);
$handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler);
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler); $handler->registerHandler(new Handler\ModelNotFoundExceptionHandler);
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler); $handler->registerHandler(new Handler\PermissionDeniedExceptionHandler);
$handler->registerHandler(new Handler\RouteNotFoundExceptionHandler);
$handler->registerHandler(new Handler\SudoRequiredExceptionHandler);
$handler->registerHandler(new Handler\ValidationExceptionHandler); $handler->registerHandler(new Handler\ValidationExceptionHandler);
$handler->registerHandler(new InvalidParameterExceptionHandler); $handler->registerHandler(new InvalidParameterExceptionHandler);
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode())); $handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));

View File

@@ -10,12 +10,15 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\Command\DeleteDiscussion; use Flarum\Core\Command\DeleteDiscussion;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeleteDiscussionController extends AbstractDeleteController class DeleteDiscussionController extends AbstractDeleteController
{ {
use AssertPermissionTrait;
/** /**
* @var Dispatcher * @var Dispatcher
*/ */
@@ -38,6 +41,8 @@ class DeleteDiscussionController extends AbstractDeleteController
$actor = $request->getAttribute('actor'); $actor = $request->getAttribute('actor');
$input = $request->getParsedBody(); $input = $request->getParsedBody();
$this->assertSudo($request);
$this->bus->dispatch( $this->bus->dispatch(
new DeleteDiscussion($id, $actor, $input) new DeleteDiscussion($id, $actor, $input)
); );

View File

@@ -10,12 +10,15 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\Command\DeleteGroup; use Flarum\Core\Command\DeleteGroup;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeleteGroupController extends AbstractDeleteController class DeleteGroupController extends AbstractDeleteController
{ {
use AssertPermissionTrait;
/** /**
* @var Dispatcher * @var Dispatcher
*/ */
@@ -34,6 +37,8 @@ class DeleteGroupController extends AbstractDeleteController
*/ */
protected function delete(ServerRequestInterface $request) protected function delete(ServerRequestInterface $request)
{ {
$this->assertSudo($request);
$this->bus->dispatch( $this->bus->dispatch(
new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
); );

View File

@@ -10,12 +10,15 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\Command\DeletePost; use Flarum\Core\Command\DeletePost;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeletePostController extends AbstractDeleteController class DeletePostController extends AbstractDeleteController
{ {
use AssertPermissionTrait;
/** /**
* @var Dispatcher * @var Dispatcher
*/ */
@@ -34,6 +37,8 @@ class DeletePostController extends AbstractDeleteController
*/ */
protected function delete(ServerRequestInterface $request) protected function delete(ServerRequestInterface $request)
{ {
$this->assertSudo($request);
$this->bus->dispatch( $this->bus->dispatch(
new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
); );

View File

@@ -10,12 +10,15 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\Command\DeleteUser; use Flarum\Core\Command\DeleteUser;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class DeleteUserController extends AbstractDeleteController class DeleteUserController extends AbstractDeleteController
{ {
use AssertPermissionTrait;
/** /**
* @var Dispatcher * @var Dispatcher
*/ */
@@ -34,6 +37,8 @@ class DeleteUserController extends AbstractDeleteController
*/ */
protected function delete(ServerRequestInterface $request) protected function delete(ServerRequestInterface $request)
{ {
$this->assertSudo($request);
$this->bus->dispatch( $this->bus->dispatch(
new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor'))
); );

View File

@@ -25,7 +25,7 @@ class SetPermissionController implements ControllerInterface
*/ */
public function handle(ServerRequestInterface $request) public function handle(ServerRequestInterface $request)
{ {
$this->assertAdmin($request->getAttribute('actor')); $this->assertAdminAndSudo($request);
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$permission = array_get($body, 'permission'); $permission = array_get($body, 'permission');

View File

@@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface
*/ */
public function handle(ServerRequestInterface $request) public function handle(ServerRequestInterface $request)
{ {
$this->assertAdmin($request->getAttribute('actor')); $this->assertAdminAndSudo($request);
$settings = $request->getParsedBody(); $settings = $request->getParsedBody();

View File

@@ -33,7 +33,7 @@ class UninstallExtensionController extends AbstractDeleteController
protected function delete(ServerRequestInterface $request) protected function delete(ServerRequestInterface $request)
{ {
$this->assertAdmin($request->getAttribute('actor')); $this->assertAdminAndSudo($request);
$name = array_get($request->getQueryParams(), 'name'); $name = array_get($request->getQueryParams(), 'name');

View File

@@ -38,7 +38,7 @@ class UpdateExtensionController implements ControllerInterface
*/ */
public function handle(ServerRequestInterface $request) public function handle(ServerRequestInterface $request)
{ {
$this->assertAdmin($request->getAttribute('actor')); $this->assertAdminAndSudo($request);
$enabled = array_get($request->getParsedBody(), 'enabled'); $enabled = array_get($request->getParsedBody(), 'enabled');
$name = array_get($request->getQueryParams(), 'name'); $name = array_get($request->getQueryParams(), 'name');

View File

@@ -0,0 +1,17 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Exception;
use Exception;
class InvalidAccessTokenException extends Exception
{
}

View File

@@ -31,7 +31,10 @@ class FloodingExceptionHandler implements ExceptionHandlerInterface
public function handle(Exception $e) public function handle(Exception $e)
{ {
$status = 429; $status = 429;
$error = []; $error = [
'status' => (string) $status,
'code' => 'too_many_requests'
];
return new ResponseBag($status, [$error]); return new ResponseBag($status, [$error]);
} }

View File

@@ -44,8 +44,10 @@ class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface
{ {
$errors = array_map(function ($field, $messages) { $errors = array_map(function ($field, $messages) {
return [ return [
'status' => '422',
'code' => 'validation_error',
'detail' => implode("\n", $messages), 'detail' => implode("\n", $messages),
'source' => ['pointer' => '/data/attributes/' . $field], 'source' => ['pointer' => "/data/attributes/$field"]
]; ];
}, array_keys($errors), $errors); }, array_keys($errors), $errors);

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Handler;
use Exception;
use Flarum\Api\Exception\InvalidAccessTokenException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class InvalidAccessTokenExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof InvalidAccessTokenException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 401;
$error = [
'status' => (string) $status,
'code' => 'invalid_access_token'
];
return new ResponseBag($status, [$error]);
}
}

View File

@@ -31,7 +31,10 @@ class InvalidConfirmationTokenExceptionHandler implements ExceptionHandlerInterf
public function handle(Exception $e) public function handle(Exception $e)
{ {
$status = 403; $status = 403;
$error = ['code' => 'invalid_confirmation_token']; $error = [
'status' => (string) $status,
'code' => 'invalid_confirmation_token'
];
return new ResponseBag($status, [$error]); return new ResponseBag($status, [$error]);
} }

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Handler;
use Exception;
use Flarum\Http\Exception\MethodNotAllowedException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class MethodNotAllowedExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof MethodNotAllowedException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 405;
$error = [
'status' => (string) $status,
'code' => 'method_not_allowed'
];
return new ResponseBag($status, [$error]);
}
}

View File

@@ -11,6 +11,7 @@
namespace Flarum\Api\Handler; namespace Flarum\Api\Handler;
use Exception; use Exception;
use Flarum\Http\Exception\RouteNotFoundException;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag; use Tobscure\JsonApi\Exception\Handler\ResponseBag;
@@ -31,7 +32,10 @@ class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface
public function handle(Exception $e) public function handle(Exception $e)
{ {
$status = 404; $status = 404;
$error = []; $error = [
'status' => '404',
'code' => 'resource_not_found'
];
return new ResponseBag($status, [$error]); return new ResponseBag($status, [$error]);
} }

View File

@@ -31,7 +31,10 @@ class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface
public function handle(Exception $e) public function handle(Exception $e)
{ {
$status = 401; $status = 401;
$error = []; $error = [
'status' => (string) $status,
'code' => 'permission_denied'
];
return new ResponseBag($status, [$error]); return new ResponseBag($status, [$error]);
} }

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Handler;
use Exception;
use Flarum\Http\Exception\RouteNotFoundException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class RouteNotFoundExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof RouteNotFoundException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 404;
$error = [
'status' => (string) $status,
'code' => 'route_not_found'
];
return new ResponseBag($status, [$error]);
}
}

View File

@@ -33,10 +33,13 @@ class ValidationExceptionHandler implements ExceptionHandlerInterface
$status = 422; $status = 422;
$messages = $e->getMessages(); $messages = $e->getMessages();
$errors = array_map(function ($path, $detail) { $errors = array_map(function ($path, $detail) use ($status) {
$source = ['pointer' => '/data/attributes/' . $path]; return [
'status' => (string) $status,
return compact('source', 'detail'); 'code' => 'validation_error',
'detail' => $detail,
'source' => ['pointer' => "/data/attributes/$path"]
];
}, array_keys($messages), $messages); }, array_keys($messages), $messages);
return new ResponseBag($status, $errors); return new ResponseBag($status, $errors);

View File

@@ -69,6 +69,8 @@ class AuthenticateWithHeader implements MiddlewareInterface
$actor = $accessToken->user; $actor = $accessToken->user;
$actor->updateLastSeen()->save(); $actor->updateLastSeen()->save();
$request = $request->withAttribute('sudo', $accessToken->isSudo());
} elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) { } elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) {
$userParts = explode('=', trim($parts[1])); $userParts = explode('=', trim($parts[1]));

View File

@@ -10,8 +10,10 @@
namespace Flarum\Core\Access; namespace Flarum\Core\Access;
use Flarum\Api\Exception\InvalidAccessTokenException;
use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Core\User; use Flarum\Core\User;
use Psr\Http\Message\ServerRequestInterface;
trait AssertPermissionTrait trait AssertPermissionTrait
{ {
@@ -61,6 +63,28 @@ trait AssertPermissionTrait
*/ */
protected function assertAdmin(User $actor) protected function assertAdmin(User $actor)
{ {
$this->assertPermission($actor->isAdmin()); $this->assertCan($actor, 'administrate');
}
/**
* @param ServerRequestInterface $request
* @throws InvalidAccessTokenException
*/
protected function assertSudo(ServerRequestInterface $request)
{
if (! $request->getAttribute('sudo')) {
throw new InvalidAccessTokenException;
}
}
/**
* @param ServerRequestInterface $request
* @throws PermissionDeniedException
*/
protected function assertAdminAndSudo(ServerRequestInterface $request)
{
$this->assertAdmin($request->getAttribute('actor'));
$this->assertSudo($request);
} }
} }

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Http\Exception;
use Exception;
class MethodNotAllowedException extends Exception
{
public function __construct($message = null, $code = 405, Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -11,6 +11,7 @@
namespace Flarum\Http\Middleware; namespace Flarum\Http\Middleware;
use Flarum\Api\AccessToken; use Flarum\Api\AccessToken;
use Flarum\Api\Exception\InvalidAccessTokenException;
use Flarum\Core\Guest; use Flarum\Core\Guest;
use Flarum\Locale\LocaleManager; use Flarum\Locale\LocaleManager;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
@@ -47,6 +48,7 @@ class AuthenticateWithCookie implements MiddlewareInterface
* *
* @param Request $request * @param Request $request
* @return Request * @return Request
* @throws InvalidAccessTokenException
*/ */
protected function logIn(Request $request) protected function logIn(Request $request)
{ {
@@ -54,9 +56,11 @@ class AuthenticateWithCookie implements MiddlewareInterface
if ($token = $this->getToken($request)) { if ($token = $this->getToken($request)) {
if (! $token->isValid()) { if (! $token->isValid()) {
// TODO: https://github.com/flarum/core/issues/253 throw new InvalidAccessTokenException;
} elseif ($actor = $token->user) { } elseif ($actor = $token->user) {
$actor->updateLastSeen()->save(); $actor->updateLastSeen()->save();
$request = $request->withAttribute('sudo', $token->isSudo());
} }
} }

View File

@@ -13,8 +13,9 @@ namespace Flarum\Http\Middleware;
use FastRoute\Dispatcher; use FastRoute\Dispatcher;
use FastRoute\RouteParser; use FastRoute\RouteParser;
use Flarum\Http\RouteCollection; use Flarum\Http\Exception\MethodNotAllowedException;
use Flarum\Http\Exception\RouteNotFoundException; use Flarum\Http\Exception\RouteNotFoundException;
use Flarum\Http\RouteCollection;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
@@ -47,6 +48,7 @@ class DispatchRoute
* @param Response $response * @param Response $response
* @param callable $out * @param callable $out
* @return Response * @return Response
* @throws MethodNotAllowedException
* @throws RouteNotFoundException * @throws RouteNotFoundException
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function __invoke(Request $request, Response $response, callable $out = null)
@@ -58,8 +60,11 @@ class DispatchRoute
switch ($routeInfo[0]) { switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND: case Dispatcher::NOT_FOUND:
case Dispatcher::METHOD_NOT_ALLOWED:
throw new RouteNotFoundException; throw new RouteNotFoundException;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException;
case Dispatcher::FOUND: case Dispatcher::FOUND:
$handler = $routeInfo[1]; $handler = $routeInfo[1];
$parameters = $routeInfo[2]; $parameters = $routeInfo[2];