As you may know, there are 2 new plugins "recently" (not so recently) added to deal with the concepts of Authentication and Authorization in your CakePHP applications.
Over the years, both Authentication and Authorization were managed in the Controller layer, via AuthComponent. These 2 things usually grow in complexity
as your project grows too, making the AuthComponent a complex class dealing with many features at the same time.
One of the original ideas behind these new plugins was to refactor AuthComponent and create specific layers to handle:
- Authentication: who are you?
- Authorization: are you allowed?
We are going to explore the Authorization concepts in this article using a specific example:
Let's imagine we have some kind of game application where Users are going to manage Tournaments. The Users will be able to create new Tournaments, and join the Tournaments through a TournamentMemberships many to many association.
Other users won't have access to the Tournaments unless they are invited to play. Players of a Tournament can invite other Users to play. So, a quick list of the use cases we are going to cover below are:
/tournaments/add
any user can create a new Tournament/tournaments/index
browse all joined tournaments/tournaments/invite
only current Members can invite others, and only if the Tournament has not started yet
We are assuming Authorization step is done in our application and we have a logged in user available in our request.
At this point we'll also assume you've installed cakephp/authentication and cakephp/authorization and loaded both plugins.
Authorization does not impose restrictions on when the authorization checks will be done, let's quickly examine the workflow and related classes for Authorization:
- AuthorizationMiddleware is attached to your Application, and will ensure the Authorization will be checked somewhere while processing the request.
TheunauthorizedHandler
config will allow you to define what to do if the request was not authorized for some reason. - At some point in your code, you'll need to call
AuthorizationComponent
, either toskipAuthorization
when you don't require any specific condition to authorize the operation. Example:// ... somewhere in your beforeFilter...
if ($user->is_superadmin) {
$this->Authentication->skipAuthorization();
}
// ...
authorize($resource, $action)
when you need to check if a given user is allowed to do some action on a given resource. Note the resource must be an Object.
How Authorization checks are done?
- We start by checking the resource, it's an Object so we use a Resolver to map every resource with a given Policy. There are some common defaults, for example to map ORM classes.
- Once we get to a Policy class, we check the matching method, for example if the action is "invite" we would check the method
canInvite(IdentityInterface $user, Tournament $tournament)
Configuration:
After the Authentication middleware, in your src/Application.php class, add the Authorization Middleware
$authorizationService = new AuthorizationService(new OrmResolver());
...
->add(new AuthorizationMiddleware($authorizationService, [
'unauthorizedHandler' => [
'className' => 'Authorization.Redirect',
'url' => '/users/login',
'queryParam' => 'redirectUrl',
],
]));
Note the $authorizationService
is configured with one resolver to match the CakePHP typical ORM classes, like Entities or Queries. https://book.cakephp.org/authorization/2/en/policy-resolvers.html#using-ormresolver
Once the middleware is added, you'll need to ensure the Authorization is checked, or you'll get an error?: "The request to / did not apply any authorization checks" .
The first step would be to skip authorization for all the controllers and actions, for example in beforeFilter
callback that all Users are allowed to access.
About the previous Tournaments specific cases, we'll need to create a new Policy class including all the possible actions to be done, for example:
-
/tournaments/add
We need to create a new Policy for the Tournament Entity
file src/Policy/TournamentPolicy.php to define policies related to specific tournaments
class TournamentPolicy
{
public function canAdd(IdentityInterface $user, Tournament $tournament)
{
// all users can create tournaments
return true;
}
}
file src/Controller/TournamentsController.php
// ...
public function add()
{
$tournament = $this->Tournaments->newEmptyEntity();
$this->Authorization->authorize($tournament);
if ($this->request->is('post')) {
// ...
The call to $this->Authorization->authorize($tournament)
; will map the Tournament entity to the TournamentPolicy, by default the action is taken from the controller action, in this case "add" so we will need to define a canAdd() method. We allowed all Users to create Tournaments.
/tournaments/index
We'll need to create a new policy for the TournamentsTable, and additionally a scope method to filter the Tournaments based on the current User membership.
file src/Policy/TournamentsTablePolicy.php to define policies for the TournamentsTable
class TournamentsTablePolicy
{
public function canIndex(IdentityInterface $user, Query $query)
{
// all users can browse tournaments
return true;
}
public function scopeIndex(IdentityInterface $user, Query $query)
{
// scope to filter tournaments for a logged in user
return $query->matching('TournamentMemberships', function (Query $q) use ($user) {
return $q->where(['TournamentMemberships.user_id' => $user->get('id')]);
});
}
}
file src/Controller/TournamentsController.php
public function index()
{
$query = $this->Tournaments->find();
$this->Authorization->authorize($query);
$tournaments = $this->paginate($this->Authorization->applyScope($query));
$this->set(compact('tournaments'));
}
-
/tournaments/invite
file src/Policy/TournamentPolicy.php to define policies related to specific tournaments
// ...
public function canInvite(IdentityInterface $user, Tournament $tournament)
{
return TableRegistry::getTableLocator()->get('TournamentMemberships')
->exists([
'user_id' => $user->get('id'),
'tournament_id' => $tournament->get('id'),
]);
}
// ...
file src/Controller/TournamentsController.php
// ...
public function invite($tournamentId, $userId)
{
$tournament = $this->Tournaments->get($tournamentId);
$this->Authorization->authorize($tournament);
// ...
In this case, we need to check if the logged in User is already a member of the TournamentMemberships group, if so, we are allowed to invite another user.
As you can see, Authorization plugin will provide a flexible way to manage your application permissions. In the previous examples we've covered typical application use cases to handle permissions per resource and action. New classes and interfaces, like policies, resolvers and mappers will allow you to configure the Authorization and ensure all the resources in your application will provide the required permissions.
If you're looking for RBAC based on your controller actions, take a look at https://github.com/CakeDC/auth/blob/master/Docs/Documentation/Authorization.md
For additional tools and plugins, check https://github.com/FriendsOfCake/awesome-cakephp#authentication-and-authorization