CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Working with CakePHP Authorization

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.
     The unauthorizedHandler 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 to
    • skipAuthorization 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?

  1. 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.
  2. 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

 

Latest articles

Playing with the new CakePHP Queue

One of the topics discussed in the community is the benefit of a unified, officially supported, CakePHP Queue plugin. Queues are used in most of the projects nowadays and are a central utility plugin. During the CakeFest 2020 event, there were also a couple direct references from speakers: (https://speakerdeck.com/josegonzalez/building-and-releasing-a-cakephp-plugin/?slide=15c) and this comment from Mark Story: https://cakesf.slack.com/archives/C172CS4TE/p1602257791377500.   This motivated me to take a deeper look at the cakephp/queue plugin and write this blog post.   Here at CakeDC we've been using queues for a looooong time. Initially in CakePHP 2, we've used plugins like CakeResque with redis or custom workers tied to Amazon SQS queues. Then in CakePHP 3 & 4 we've been using mostly https://github.com/josegonzalez/cakephp-queuesadilla with redis or mongodb backends. https://github.com/cakephp/queue   First thing would be setting up the plugin in your project, we are going to use the example project we used in CakeFest 2020: https://github.com/cakephp/cakefest2020/#using-docker   So after setting up the project and running it via docker compose, we can proceed to setup the plugin via composer. We will need to add it as a repository and set  

Install via composer

  After the first release it'll be much easier, but for now you'll need to add the package to your composer.json   "repositories": [         {             "type": "vcs",             "url": "https://github.com/cakephp/queue.git"         }     ]   Then do composer require cakephp/queue -W   And install some transport as stated in the official documentation https://book.cakephp.org/queue/1/en/index.html#installation composer require enqueue/redis:^0.9 composer require predis/predis:^1   Ensure your redis server is up and running, you can check the commands sent to your local redis server using redis-cli monitor   Now we are ready to configure the queue, we'll create 1 default queue adding this to the config/app.php file       'Queue' => [         'default' => [             'url' => 'redis:',         ],     ],   Add a Job using `bin/cake bake job Example` and add some code to the execute method       public function execute(Message $message): string     {         $data = $message->getArgument('data');           // do some long operation with data         Debugger::log($data);         sleep(2);           return Processor::ACK;     }   I've added a command utility to enqueue a test message bin/cake bake command addJob       public function execute(Arguments $args, ConsoleIo $io)     {         $callable = [ExampleJob::class, 'execute'];         $arguments = ['id' => 1, 'data' => ['some' => 'data']];           QueueManager::push($callable, $arguments);     }   And finally we can start our worker using bin/cake worker to pull jobs from Redis and process them using the ExampleJob::execute method   Here's all the example code created: https://github.com/cakephp/cakefest2020/tree/feature/cakephp-queue - for your reference.   Please note the plugin is still a work in progress and there is no stable release as of now.  It's looking great so far and we plan to include it in our next projects!  

Best CakePHP Plugins

Members of our team had the privilege of helping with CakeFest 2020 this year. One added virtual feature was the giveaways from CakePHP, these were done in the form of fastest-to-answer, trivia, or participation (random draw).  One of the giveaway games was to share your favorite CakePHP plugin but like, how do we only pick one, right? Anyway… There was a lot of participation in this giveaway! A few people even named our CakeDC Users plugin as their favorite *cue blushing face*. But in all seriousness, I thought it would be a good idea to share with you some of the plugins that were named most useful/helpful by CakeFest attendees this year….   Like I mentioned, the CakeDC users Plugin: https://github.com/CakeDC/users Queue Plugin: https://github.com/dereuromark/cakephp-queue Bake: https://github.com/cakephp/bake DataTables: https://github.com/fheider/cakephp-datatables CakePHP-tools: https://github.com/dereuromark/cakephp-tools Authentication: https://github.com/cakephp/authentication CakePHP-image: https://github.com/josbeir/cakephp-image Fixturize: https://github.com/lorenzo/cakephp-fixturize CakePHP File-Storage: https://github.com/burzum/cakephp-file-storage Crud: https://github.com/FriendsOfCake/crud IDE Helper: https://github.com/dereuromark/cakephp-ide-helper Asset-Compress: https://github.com/markstory/asset_compress CakePHP Debug Kit: https://github.com/cakephp/debug_kit Plum-Search: https://github.com/skie/plum_search CakePHP API: https://github.com/cakedc/cakephp-api/ Bootstrap UI: https://github.com/friendsofcake/bootstrap-ui Trash: https://github.com/usemuffin/trash   You can check out the full list of CakePHP Plugins at Plugins.CakePHP.org.  Have you been utilizing these tools? If not, it may be a good idea to start… while each of these serve their own purpose, using all of them can make your baking process a lot easier.    Perhaps your favorite didn’t make this list? Tell us about it… email HERE. Or, tweet us @CakeDC, @CakePHPThanks for baking!  

CakeFest 2020 recap

Taking a deep breath….. We have made it through another successful CakeFest event.    We didn’t know exactly what to expect with a fully virtual event, as it opens the door for a list of things that can (and most likely will) go wrong. Would the speakers show up? Would the internet connections keep things from running smoothly? Would attendees enjoy the information?   The answer to all of those questions is yes.    The lineup this year was amazing, and we had speakers from 6 different countries! With the ever changing way of life, our team wanted to have a diverse group of speakers, with some talking about some pretty serious subjects - like Dr. Jennifer Akullian and the mental health issues faced in the tech world. Jen allowed for questions and how-to-handle scenarios, and worked through some tough issues with attendees. Workshops from Mark Story, Jorge Gonzalez, Mark Scherer and Jose Lorenzo provided incomparable information while building projects in real time. All of the talks provided useful information that we hope all attendees will take with them as they bake in the future.    Now, does all of this bragging mean we didn’t have any issues? No. As I said in our opening, I think our group is pretty awesome and perfect, but the fact of the matter is, no one is. When bringing people together from all over the world, it is hard to test and check every connection multiple times. We had our fair share of internet lag, connection problems and audio issues. You know what we did? We worked together, fixed it, switched around the schedule and made it happen.   Virtual CakeFest 2020 was a great success, and exceeded our expectations. We were able to gather bakers from all over the world, in real time, and host a live event! I couldn’t believe how interactive the audience was, and everyone is still baking together in our #cakefest chat channel. I hope everyone is as impressed with the turn out as our team is. I know what you’re thinking… when will the recordings be released. We are working on uploading, editing and releasing the videos ASAP. While we tried to find the best universal timezone, we understand that other obligations kept some from attending, and we want to share these videos with you quickly, so know that we are working on it.    To our attendees: THANK YOU. Thank you for joining, supporting our framework and keeping CakePHP alive.   

I would like to thank all of our speakers:

  Remy Bertot  Junichi Okuyama Mark Story Jorge Gonzalez Jose Lorenzo Mark Scherer Jose Gonzalez Cal Evans Ed Barnard Jennifer Akullian Mariano Iglesias Chris Nizzardini Juan Pablo Ramirez
 

A HUGE THANK YOU TO OUR SPONSORS:

   Cake Development Corporation
  Passbolt 
  Marks Software
  RingCentral  


  Here’s to planning next year’s event to be even bigger and better!  

We Bake with CakePHP