Introducing the CakePHP Authorization Plugin

In the next major release of CakePHP we’re going to be removing the AuthComponent. This component and its helpers have been part of CakePHP since the 1.2 days, but their time has come to an end. Over the years, AuthComponent has become a complex and difficult to extend piece of CakePHP. In its wake, we’re promoting two new plugins. First, cakephp/authentication will be replacing the identification parts of AuthComponent while cakephp/authorization handles access control and permissions. These plugins are intended to be used together, but can be used individually. They can also be used outside of CakePHP applications thanks to PSR-7. The new authorization plugin makes answering questions like ‘can the current user edit this record?’ and ‘what records can the current user see?’ simpler to answer from anywhere in your application, not just in controllers.

Installation

The authorization plugin is installed via composer. Currently it is in a beta release, but that will quickly change.

Show Plain Text
  1. # For now run
  2. composer require cakephp/authorization:1.0.0-beta4
  3.  
  4. # Later on run
  5. composer require cakephp/authorization:^1.0

In your application’s bootstrap process you should add the authorization plugin to your application via Plugin::load(). This will enable bake to discover tasks. Next you’ll need to add the middleware to your application:

Show Plain Text
  1. // In your Application.php, import the class.
  2. use Authorization\Middleware\AuthorizationMiddleware;
  3.  
  4. // inside your application's middleware hook.
  5. $middlewareStack->add(new AuthorizationMiddleware($this));

Concepts

Authorization requires a few possibly new concepts.

  • Identity Is a person or agent acting in your application. Generally this will be a user or an API client.
  • Resource A ‘thing’ that an Identity needs to access. For example: Articles, Tags, Comments.
  • Action An operation that an Identity takes on a Resource. For example ‘update’, ‘delete’
  • Policy The logic defining which Actions Identities can take on Resources.

The authorization plugin provides abstractions for Identities, Actions and Policies. The Resources are the domain objects in your application.

Identities

The authorization plugin hooks into your application as middleware. It expects to find an authenticated user in the identity attribute of the request. By default, the authorization middleware will decorate this identity with the Authorization\IdentityDecorator. This interface defines 3 methods, and proxies all other methods, and properties to the decorated object. The decorator adds can() and applyScope() methods. These are the main methods that allow you to check permissions on your resources. Because all access controls are attached to the current user, you can more easily perform access control checks anywhere in your application:

Show Plain Text
  1. // Get the user from the request
  2. $user = $request->getAttribute('identity');
  3.  
  4. // Check a permission
  5. if ($user->can('delete', $article)) {
  6.     echo 'User can delete this thing!';
  7. }

While the decorator approach works, you can avoid any overhead/complexity it adds by implementing the Authorization\IdentityInterface in your existing user class. The documentation has an example of how to do this. Permission checks are always executed by Policy classes.

Policies

Policy classes contain the permissions checking logic in your application. Each resource that you want to check permissions for will need a policy class defined. Policy classes are mapped to the resources in your application by the ‘policy resolver’. For CakePHP applications you can define your policy resolver in your authorization hook method. If your application primarily works with ORM Entities, you can use the following:

Show Plain Text
  1. namespace App;
  2.  
  3. use Authorization\AuthorizationService;
  4. use Authorization\Policy\OrmResolver;
  5. use Cake\Http\BaseApplication;
  6.  
  7. class Application extends BaseApplication
  8. {
  9.     public function authorization($request)
  10.     {
  11.         $resolver = new OrmResolver();
  12.  
  13.         return new AuthorizationService($resolver);
  14.     }
  15. }

If you have multiple kinds of domain objects to check, you can use the ResolverCollection to join resolvers for multiple datasources together.
Now that we have a resolver setup, we can create some policies. The simplest way to do this is with bake:

Show Plain Text
  1. bin/cake bake policy Project

This will generate a class into src/Policy/ProjectPolicy.php. As an example, our application will only allow users to see/edit/delete their own projects. In our policy class we can enforce this logic with the following method:

Show Plain Text
  1. public function canUpdate(IdentityInterface $user, Project $project)
  2. {
  3.     return $user->id == $project->user_id;
  4. }

Policy methods use the convention of can and the action name. We could implement similar logic in our canDelete method too. To show a user their list of projects we can’t check records on individual rows as it would be inefficient to look at all projects to find only those the current user can see. Instead we want to push the access control conditions into the query that creates the project list. We call this a ‘policy scope’ as it scopes a query through a policy. Before we can apply scopes to our queries, we need to create a policy class for our Table class. We can create a table policy with bake:

Show Plain Text
  1. bin/cake bake policy --type Table Projects

This will create src/Policy/ProjectsTablePolicy.php. In it we can add the following method:

Show Plain Text
  1. public function scopeIndex($user, $query)
  2. {
  3.     return $query->where(['Projects.user_id' => $user->id]);
  4. }

Then in our controller endpoints we can use our new policy:

Show Plain Text
  1. // In ProjectsController::index
  2. $user = $this->request->getAttribute('identity');
  3.  
  4. // Apply our policy scope
  5. $query = $user->applyScope('index', $this->Projects->find());
  6.  
  7. $this->set('projects', $this->paginate($query));

Policy scopes should return a new or mutated object. They are ideal to use with query builders and finder logic.

Checking Permission

Earlier we’ve seen a few examples of authorization checks in action. Once loaded the AuthorizationComponent can be used to simplify raising exceptions when authorization checks fail, and skipping authorization on actions that don’t require permissions to be checked.

Show Plain Text
  1. // In a controller
  2. public function view($id)
  3. {
  4.     $project = $this->Projects->get($id)
  5.     // Gets the 'action' from the controller action.
  6.     // Will raise an exception if the check fails.
  7.     $this->Authorization->authorize($project);
  8.  
  9.     // Rest of controller action.
  10. }

If your controller actions don’t require authorization, you can use the skipAuthorization() method to mark an action as not requiring further authorization. By default when authorization fails, or hasn’t been checked the AuthorizationMiddleware will raise an exception that your application can handle. If you’d prefer that authorization errors are converted into redirects, you can do that too. The documentation has more details

Hopefully this illustrates a bit of what the new authorization plugin can do and why we’re removing AuthComponent in favour of more powerful and flexible solutions.

Comments

There is any pattern to create Policy and Policy methods related to prefixed routes, or with route with extensions? Cases like:

/admin/products/update (not the same permissions as /products/update)

/admin/products/view.json (only some user access the json data)

Marcelo on 4/4/18

Right now there aren’t specific policies for prefixed routes. Before going down that path, I would attempt to try and have one policy for each resource. Including rules for all user types/groups in one place makes mistakes harder to make when checking authorization, and lets you look at all the authorization rules at once.

mark story 4 weeks, 2 days ago

Have your say: