Authorization

Currently any logged in user can add new users and edit existing users. We want to add more finely detailed restrictions. For example we want to restrict non-Admins from editing any user but themselves, nor adding any new accounts. Also we want to restrict non-Admins from changing their role from User to Admin.

Enable Authorization

We want to first lock down the edit form to ensure Users only edit their own profile, and then ensure only Admins can add users, delete users, and change the Role that Users are assigned.

Add the following imports to /src/Application.php:

use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;

Implement the AuthorizationServiceProviderInterface on your Application.

class Application extends BaseApplication
    implements AuthenticationServiceProviderInterface, AuthorizationServiceProviderInterface
{
    ...

And add the following to the bootstrap() function:

public function bootstrap(): void
{
  // Call parent to load bootstrap from files.
  parent::bootstrap();
  $this->addPlugin('Authorization');
  ...

Add the Authorization middleware to your middleware() function after Authentiation:

  ...
  ->add(new AuthenticationMiddleware($this))
  ->add(new AuthorizationMiddleware($this))
  ...

Like Authentication, you'll need to add a getAuthorizationService() function:

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

Now we can load the component in the initialize() function of your /src/Controller/AppController.php:

 $this->loadComponent('Authorization.Authorization');

Now you'll notice if you try to access any view, you are met with an error about the request not applying any authorization checks. CakePHP wants to check every request, so you have to tell it what requests to ignore.

So in every function of /src/Controller/UsersController.php where we want unauthorized access, like logout(), login(), index(), and view(), we simply add the following at the top of the function to skip authorization:

  $this->Authorization->skipAuthorization();

You may also notice the default configuration page at http://localhost/tutorial is currently inaccessible too. Simply add the above line to the beginning of the display() function in /src/Controller/PagesController.php which is the controller for displaying static pages.

And lastly you may notice the DebugKit toolbar is broken because the request for the toolbar also did not apply any authorization checks. You can set the configuration for the DebugKit toolbar in your app_local.php file.

Append the following to the end of /config/app_local.php

  ...
  'DebugKit' => [
    'ignoreAuthorization' => true,
  ],
];

Add Authorization Checks

Now for the functions we want to restrict access to, such as add(), edit(), and delete(), we add an authorization check. Since we need to send the entity as a parameter, in the Users controller we must add our check after the user has been created or requested.

Here's an example for /src/Controller/UsersController.php Put this in each of the functions you want to restrict access.

  ...
  $user = $this->Users->get($id);
  $this->Authorization->authorize($user);
  ...

If you want to add an authorization check to a function we previously set as an "unauthenticated" action, like view you have to remove it from addUnauthenticatedActions array because all authorizations automatically include the authenticated identity stored in the request. If an action doesn't require authentication, you can't perform authorization on the action.

Create a Policy Class

The Authorization plugin models authorization and permissions as Policy classes. What this means is that for each resource, you have to identify who is authorized to perform certain actions. For our Users entity, here is a policy class that goes in /src/Policy/UserPolicy.php You can create the whole file from scratch or bake an outline to save yourself some time:

bin/cake bake policy User
<?php
declare(strict_types=1);

namespace App\Policy;

use App\Model\Entity\User;
use Authorization\IdentityInterface;

class UserPolicy
{
  // Can a user add a user?
  public function canAdd(IdentityInterface $user, User $resource)
  {
    // Only Admins can add users
    return $this->isAdmin($user);
  }
  
  // Can a user edit a user?
  public function canEdit(IdentityInterface $user, User $resource)
  {
    // Admins can edit all users
    if ($this->isAdmin($user)) {
      return true;
    } else {
      // Users can only edit themselves
      return $user->id === $resource->id;
    }
  }
  
  // Can a user delete a user?
  public function canDelete(IdentityInterface $user, User $resource)
  {
    // Only Admins can delete users
    return $this->isAdmin($user);
  }
  
  protected function isAdmin(IdentityInterface $user)
  {
    return $user->role === 'Admin';
  }
}

Our policy checks the logged in user through the IdentityInterface and can test it against the User resource we want to edit, or simply check if the logged in user has the appropriate role through our custom function. Observe there is no canView() function, that's because we've already skipped Authorization in the view() function of the UsersController.

Use an Accessor

Instead of checking our role via the isAdmin() function above, we can use our previously created is_admin accessor. Remove the isAdmin() function and replace all $this->isAdmin($user) calls with the following:

$user->getOriginalData()->is_admin

You can also remove the isAdmin() method from your policy class as well.

Finally Add the Role Field to Edit View

In addition to restricting access through authorization, we can also check against the logged in user and display, or not display, items based on the user. For example, even though we are allowing a user to edit their own User resource, we only want users in the Admin role to be able to see the Role field when editing a user.

Update /template/Users/edit.php

    ...
    // Only display this field to Admins
    if ($this->getRequest()->getAttribute('identity')->getOriginalData()->is_admin) {
      echo $this->Form->control('role');
    }
    ...

And now only users with the role of "Admin" can view that field to modify it. You can wrap any code you want to restrict in that if statement so it's only visible to the Admin role, such as the New User button.

Additional Policies

Now create a Policy for Phone Numbers and Documents that allows Users to add, edit, and delete their own phone numbers, add and delete documents, but not others. Admin's should have full access.