My app is CakePHP 5 with the Authentication + Authorization plugins.
I’ve written a Plugin which runs both Request and Entity Authorizations so both Controller actions and data operations can be controlled by a User’s role.
It all works, but I can’t figure out how to ‘globally’ run the Entity auth without needing to trigger the check in every Controller action like this:
// In the Articles Controller.
public function edit($id)
{
$article = $this->Articles->get($id);
$this->Authorization->authorize($article);
// Rest of the edit method.
}
At the moment if I forget to add the $this->Authorization->authorize($article); code, the “did not perform Authorization” error isn’t thrown which I assume is because i’m already checking auth on the request - this creates a potential security risk, plus it would be nice to skip having to always add this code.
How can I set it up so this check always runs on all Entity operations?
There doesn’t appear to be an immediately easy way that I know of to do this.
I was thinking of using the Events system but Model.afterFind has been removed in CakePHP 5.x
What follows is probably a terrible idea: Extend Cake\ORM\Table with a custom Table class and point all your Table classes at it then use overridden methods to perform the authorize check.
In your Table classes
// src/Model/Table/ArticlesTable.php
class ArticlesTable extends \App\Model\Table\Table
{
}
Custom Table class…
// src/Model/Table/Table.php
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Authorization\Exception\ForbiddenException;
use Cake\Datasource\EntityInterface;
use Cake\ORM\Table as CakeTable;
use Cake\Routing\Router;
use Closure;
use Psr\SimpleCache\CacheInterface;
class Table extends CakeTable
{
public function initialize(array $config): void
{
parent::initialize($config);
}
public function newEmptyEntity(): EntityInterface
{
$entity = parent::newEmptyEntity();
$this->authorize($entity);
return $entity;
}
public function get(
mixed $primaryKey,
array|string $finder = 'all',
CacheInterface|string|null $cache = null,
Closure|string|null $cacheKey = null,
mixed ...$args
): EntityInterface {
$entity = parent::get(
$primaryKey,
$finder,
$cache,
$cacheKey,
...$args
);
$this->authorize($entity);
return $entity;
}
public function authorize($resource): void
{
$request = Router::getRequest();
/**
* @var \Authorization\AuthorizationService $authService
*/
$authService = $request->getAttribute('authorization');
$action = $request->getParam('action');
$user = $request->getAttribute('identity');
$result = $authService->canResult($user, $action, $resource);
if ($result->getStatus()) {
return;
}
if (is_object($resource)) {
$name = get_class($resource);
} elseif (is_string($resource)) {
$name = $resource;
} else {
$name = gettype($resource);
}
throw new ForbiddenException($result, [$action, $name]);
}
}
// In the Articles Controller.
public function edit($id)
{
$article = $this->Articles->get($id);
// remove authorize and let the overridden get method do it
// $this->Authorization->authorize($article);
// Rest of the edit method.
}
This is the reason I have a BaseController class which all my other controllers inherit, and I add the check in the beforeFilter method of the BaseController.
I lucked out a bit in that i’m using MixerAPI in this project, so am baking controllers using it’s CRUD plugin
This means the controllers call its Services to retrieve/add/update data instead of the Cake ORM, e.g.
public function view(ReadInteface $read)
{
$this->set('data', $read->read($this));
}
By implementing my own implementation of these Create/Read/Update/Search/Delete Services in my app I was able to apply the Authentication checks in them which saved needing to add it to all the Controllers.
In other apps that don’t use something like MixerAPI CRUD I suppose the other approach would be a custom bake template for Controllers where you add the Authorization in - assuming you do so before you’ve already written a bunch of code.
Would be useful to know whats considered the Best Practice approach for CakePHP 5, or how you could ensure both Request and Entity validation are both run.