Policy Scopes and Preconditions

In addition to allowing or restricting access to a complete operation, you may wish to only restrict the scope of the operation. For example instead of listing all users on the index page, only list the current user.

To implement a policy scope:

Create Your Scope Method

Unlike add, edit, or delete, where we are working on a single record, index works with all records in the table, so we need to create a Users Table Policy to accompany the User Policy. In /src/Policy/UsersTablePolicy.php file we can add the scopeIndex method to restrict the index query to only the logged in user.

<?php
declare(strict_types=1);

namespace App\Policy;

use App\Model\Entity\UsersTable;
use Authorization\IdentityInterface;
class UsersTablePolicy
{
  public function scopeIndex(IdentityInterface $user, $query) {
    return $query->where(['Users.id' => $user->id]);
  }
}

Apply the Scope Policy

Now in our UsersController.php in the index action implement the scope method.

public function index()
{
  $this->Authorization->skipAuthorization();
  $query = $this->Users->find();
  $users = $this->paginate($this->Authorization->applyScope($query));
  $this->set(compact('users'));
}

And now our index list only shows the currently logged in user. We can make the scopeIndex more detailed if we'd like. For example, if we want Admin users to see all users, but other users to only see themselves:

public function scopeIndex($user, $query) {
  if ($user->getOriginalData()->is_admin) {
    return;
  }
  return $query->where(['Users.id' => $user->getIdentifier()]);
}

Preconditions

In addition to policy methods related directly to the standard CRUD actions (canView(), canEdit(), etc.) you can apply common precondition checks covering all operations in a policy. This is useful to quickly allow or deny all actions to a resource.

To implement a precondition include and implement the BeforePolicyInterface in your entity policy so you can add the before() method. Here is the code for our /src/Policy/UsersPolicy.php

...
use Authorization\Policy\BeforePolicyInterface;
use Authorization\Policy\ResultInterface;
class UserPolicy implements BeforePolicyInterface
{
...

Add the before() Method

The interface requires the before() method so add it to your policy class.

  public function before(?IdentityInterface $user, mixed $resource, string $action): ResultInterface|bool|null
    if ($user->getOriginalData()->is_admin) {
      // Admin user role gets access to everything
      return true;
    } elseif ($user->getOriginalData()->is_disabled) {
      // Disabled users get access to nothing except unauthenticated and skipped actions
      return false;
    }
    // returning null indicates the before hook didn't make a decision so the
    // authorization methods below will be invoked
    return;
  }

And now, since our precondition authorizes Admins, we don't need the canAdd() and canDelete()methods and can clean up canEdit().

  public function canEdit(IdentityInterface $user, User $resource)
  {
    // Users can only edit themselves
    return $user->id === $resource->id;
  }

Documents and Phone Numbers

Now take what you've learned and apply Preconditions allowing Admin's full access to Documents and Phone Numbers.