CakePHP 5 - Run Authorization checks on user against a secondary table?

I’m feeling rather deflated and defeated at the moment. I presume my problem is quite simplistic to solve in its nature, but am still stuck. I am attempting to upgrade a CakePHP v2 application to v5. The “upgrade” process is to start over from scratch and just move the business logic over. The first model(s) I’m attempting to recreate are Users and Groups. In the old application, depending on need, actions in various controllers were authorized by a user attribute, or a user’s association to a group (User Has-And-Belongs-To-Many Groups).

I am attempting right now to figure out how to authorize a User->add action, which should only be permissible from users with a specific title (attribute), or within a specific group. Checking the title is easy enough. I am completely unable to figure out how to properly authorize against a secondary table (is the current user assigned to groups.id of 2, or 5?) for the add action. Should it be within a UserTablePolicy scope? If so, how can an empty object be passed in as a query object? Should it be within a UserPolicy can method? If so, how do I properly access the ORM from within a Policy class?

I’m finding the level of PHP OOP abstraction in v5 far outweighs my comprehension from the v2 days, unfortunately.

I’d love any help here. I’d share code attempts, but I’ve tried so many options I think I’d confuse someone helping. Would be happy to share code once I have a little direction, if I’m still stuck. Thank you!!

In the resolver configuration for the authentication service, you can specify a custom finder to use. Write a custom finder that contains the user groups. Then when your policy is checking whether it’s allowed, it will have direct access to the user’s groups through the identity object that’s passed in.

1 Like

Okay, I was a little lost at the phrase, “custom finder,” when in reference to the “resolver configuration” when referring to the CakePHP 5 docs. I figured out by resolver configuration what was meant was the Application.php’s getAuthorizationService() method setup. I’m not sure if what Zuluru was suggesting is the route I took, but considering I’m coming from a CakePHP 2 application and attempting to merge the business logic back into CakePHP 5, the closest thing (for my application) would be to take advantage of a controller method of isAuthorized(). Based on Zuluru’s mention of a custom finder (assuming resolver, as defined in the book), I adjusted my code as follows (after having already installed and setup the authentication and authorization plugins):

Application.php:


// ...

use App\Policy\ControllerResolver;

// ...
// ...
// ...

public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
{
    // $resolver = new OrmResolver();
    $resolver = new ControllerResolver();
    return new AuthorizationService($resolver);
}

Per the (Authorization) book, I created a src\Policy\ControllerResolver.php file, and a src\PolicyControllerHookPolicy.php file - each of which are exact copies of what was in the book for the custom resolver examples. In my UserController.php file, I provided the following adjustments:

    public function add()
    {
        $this->Authorization->authorize($this);
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $groups = $this->Users->Groups->find('list')->all();
        $this->set(compact('groups'));
    }

    public function isAuthorized() {
        return true;
    }

The logic for determining access rights will be added to isAuthorized (in each controller); thus far I’d only gotten so far as to add a simple die(‘We are in isAuthorized’) call to verify I made it through the proper paths of authorization, but I at least know I can access models within the controller without much hassle.

NOTE: Take note as to what parameter is passed to Authorization->authorize(); It’s $this, which is the (current) Controller object. We are not explicitly sending a user identity object.

Also, the current version of the book, for CakePHP 5, points to the Authorization version 2 documentation. The interface for that provides a selection to choose other versions, but 2 is the highest. There is a version 3, however (if you change the URL manually). I’m not 100% sure which one CakePHP v5 uses by default, though my recently configured composer file seems to show version 3 as a minimum expectation.

If this is the wrong way to do what I was proposing, please feel free to correct me! I somehow managed to skip the page in the documentation about custom resolvers prior to Zuluru’s response, so I very much appreciate the reply. I now need to go back and clean up all of the various implementations I’ve intermingled in my controller. :sweat_smile:

Seems like you’ve arrived at a method that works for you! However I think your confusion regarding the custom finder Zuluru was referring to is the resolver configuration from the Authentication service rather than Authorization.

If you look at this section in the authentication book: Identifiers - 3.x

You’ll see the following code

$service->loadIdentifier('Authentication.Password', [
    // ...
    'resolver' => [
        'className' => 'Authentication.Orm',
        'userModel' => 'Users',
        'finder' => 'active', // default: 'all'
    ],
   // ...
]);

Specifically, the 'finder' => 'active' line here is defining a custom finder for the Authentication service to use when providing the identity object.

You can create a finder in src/Model/Table/UsersTable.php, something like the following:

    /**
     * @return \Cake\ORM\Query\SelectQuery
     */
    public function findWithGroups(SelectQuery $query): SelectQuery
    {
        $query->contain(['Groups']);

        return $query;
    }

This would create a finder referenced by ‘withGroups’, and if you set that as the finder in your Authentication service then when your Policy received an $identity the $identity->groups property would be populated.

1 Like

That makes a lot more sense, and I feel bad for misunderstanding. Very much appreciate the clarification, along with code sample examples! I think the solution I ended up with will match the prior system’s business logic a bit easier for me, but the custom finder method is great to understand as well!! Thanks, both!

1 Like