How to run Entity Authorization without needing to add code in every Controller action?

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?

Many thanks for any assistance.

1 Like

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.
}
1 Like

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.

p.s. still running Cake4 here…

Thank you - I appreciate it!

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.

1 Like

I like this approach… so clean.