With users now able to login to our CMS, we want to apply authorization rules to ensure that each user only edits the posts they own. We’ll use the authorization plugin to do this.
Use composer to install the Authorization Plugin:
composer require "cakephp/authorization:^2.0"
Load the plugin by adding the following statement to the bootstrap()
method in src/Application.php:
$this->addPlugin('Authorization');
The Authorization plugin integrates into your application as a middleware layer and optionally a component to make checking authorization easier. First, lets apply the middleware. In src/Application.php add the following to the class imports:
use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;
Add the AuthorizationServiceProviderInterface
to the implemented interfaces on your application:
class Application extends BaseApplication
implements AuthenticationServiceProviderInterface,
AuthorizationServiceProviderInterface
Then add the following to your middleware()
method:
// Add authorization **after** authentication
$middlewareQueue->add(new AuthorizationMiddleware($this));
The AuthorizationMiddleware
will call a hook method on your application when
it starts handling the request. This hook method allows your application to
define the AuthorizationService
it wants to use. Add the following method your
src/Application.php:
public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
{
$resolver = new OrmResolver();
return new AuthorizationService($resolver);
}
The OrmResolver lets the authorization plugin find policy classes for ORM entities and queries. Other resolvers can be used to find policies for other resources types.
Next, lets add the AuthorizationComponent
to AppController
. In
src/Controller/AppController.php add the following to the initialize()
method:
$this->loadComponent('Authorization.Authorization');
Lastly we’ll mark the add, login, and logout actions as not requiring authorization by adding the following to src/Controller/UsersController.php:
// In the add, login, and logout methods
$this->Authorization->skipAuthorization();
The skipAuthorization()
method should be called in any controller action
that should be accessible to all users even those who have not logged in yet.
The Authorization plugin models authorization and permissions as Policy classes. These classes implement the logic to check whether or not a identity is allowed to perform an action on a given resource. Our identity is going to be our logged in user, and our resources are our ORM entities and queries. Lets use bake to generate a basic policy:
bin/cake bake policy --type entity Article
This will generate an empty policy class for our Article
entity. You can
find the generated policy in src/Policy/ArticlePolicy.php. Next update the
policy to look like the following:
<?php
namespace App\Policy;
use App\Model\Entity\Article;
use Authorization\IdentityInterface;
class ArticlePolicy
{
public function canAdd(IdentityInterface $user, Article $article)
{
// All logged in users can create articles.
return true;
}
public function canEdit(IdentityInterface $user, Article $article)
{
// logged in users can edit their own articles.
return $this->isAuthor($user, $article);
}
public function canDelete(IdentityInterface $user, Article $article)
{
// logged in users can delete their own articles.
return $this->isAuthor($user, $article);
}
protected function isAuthor(IdentityInterface $user, Article $article)
{
return $article->user_id === $user->getIdentifier();
}
}
While we’ve defined some very simple rules, you can use as complex logic as your application requires in your policies.
With our policy created we can start checking authorization in each controller
action. If we forget to check or skip authorization in an controller action the
Authorization plugin will raise an exception letting us know we forgot to apply
authorization. In src/Controller/ArticlesController.php add the following to
the add
, edit
and delete
methods:
public function add()
{
$article = $this->Articles->newEmptyEntity();
$this->Authorization->authorize($article);
// Rest of the method
}
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->contain('Tags') // load associated Tags
->firstOrFail();
$this->Authorization->authorize($article);
// Rest of the method.
}
public function delete($slug)
{
$this->request->allowMethod(['post', 'delete']);
$article = $this->Articles->findBySlug($slug)->firstOrFail();
$this->Authorization->authorize($article);
// Rest of the method.
}
The AuthorizationComponent::authorize()
method will use the current
controller action name to generate the policy method to call. If you’d like to
call a different policy method you can call authorize
with the operation
name:
$this->Authorization->authorize($article, 'update');
Lastly add the following to the tags
, view
, and index
methods on the
ArticlesController
:
// View, index and tags actions are public methods
// and don't require authorization checks.
$this->Authorization->skipAuthorization();
While we’ve blocked access to the edit action, we’re still open to users
changing the user_id
attribute of articles during edit. We
will solve these problems next. First up is the add
action.
When creating articles, we want to fix the user_id
to be the currently
logged in user. Replace your add action with the following:
// in src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEmptyEntity();
$this->Authorization->authorize($article);
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Changed: Set the user_id from the current user.
$article->user_id = $this->request->getAttribute('identity')->getIdentifier();
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$tags = $this->Articles->Tags->find('list')->all();
$this->set(compact('article', 'tags'));
}
Next we’ll update the edit
action. Replace the edit method with the following:
// in src/Controller/ArticlesController.php
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->contain('Tags') // load associated Tags
->firstOrFail();
$this->Authorization->authorize($article);
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData(), [
// Added: Disable modification of user_id.
'accessibleFields' => ['user_id' => false]
]);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
$tags = $this->Articles->Tags->find('list')->all();
$this->set(compact('article', 'tags'));
}
Here we’re modifying which properties can be mass-assigned, via the options
for patchEntity()
. See the Changing Accessible Fields section for
more information. Remember to remove the user_id
control from
templates/Articles/edit.php as we no longer need it.
We’ve built a simple CMS application that allows users to login, post articles, tag them, explore posted articles by tag, and applied basic access control to articles. We’ve also added some nice UX improvements by leveraging the FormHelper and ORM capabilities.
Thank you for taking the time to explore CakePHP. Next, you should learn more about the Database Access & ORM, or you peruse the Using CakePHP.