Before you go, check out these stories!

0
Hackernoon logoHow to Manage ACLs in Symfony the Easy Peasy Way by@Bassaganas

How to Manage ACLs in Symfony the Easy Peasy Way

Author profile picture

@BassaganasJordi Bassagañas

Hi there! How are you today? I blog about technology, the Internet, SEO, programming tips, and more.

It's no secret ACLs can be tricky. Since voters seem to be the alternative to ACLs recommended by Symfony, I recently decided that I'd write my own easy-to-use Symfony 5 bundle to manage access control lists (ACL) in my applications.

programarivm/easy-acl-bundle was originally written to be used in a JWT-authenticated API for single page applications (SPA) but it can also help out in multiple different scenarios provided it does not require the Security component -- which in most cases would be, in my humble opinion, especially suitable for multi-page applications (MPA) handling sessions.

EasyAclBundle
purely relies on Doctrine entities and repositories to do its stuff, meaning the permissions are just stored in a database. It is agnostic in terms of your app's architecture.

Having said that, here is how JWT tokens are easily authenticated and authorised in an event subscriber with the help of the so-called easy ACL repositories.

// src/EventSubscriber/TokenSubscriber.php

namespace App\EventSubscriber;

use App\Controller\AccessTokenController;
use Doctrine\ORM\EntityManagerInterface;
use Firebase\JWT\JWT;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;

class TokenSubscriber implements EventSubscriberInterface
{
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function onKernelController(ControllerEvent $event)
    {
        $controller = $event->getController();

        // when a controller class defines multiple action methods, the controller
        // is returned as [$controllerInstance, 'methodName']
        if (is_array($controller)) {
            $controller = $controller[0];
        }

        if ($controller instanceof AccessTokenController) {
            $jwt = substr($event->getRequest()->headers->get('Authorization'), 7);

            try {
                $decoded = JWT::decode($jwt, getenv('JWT_SECRET'), ['HS256']);
            } catch (\Exception $e) {
                throw new AccessDeniedHttpException('Whoops! Access denied.');
            }

            $user = $this->em->getRepository('App:User')
                        ->findOneBy(['id' => $decoded->sub]);

            $identity = $this->em->getRepository('EasyAclBundle:Identity')
                            ->findBy(['user' => $user]);

            $rolename = $identity[0]->getRole()->getName();
            $routename = $event->getRequest()->get('_route');

            $isAllowed = $this->em->getRepository('EasyAclBundle:Permission')
                            ->isAllowed($rolename, $routename);

            if (!$isAllowed) {
                throw new AccessDeniedHttpException('Whoops! Access denied.');
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::CONTROLLER => 'onKernelController',
        ];
    }
}

Most of this code will be self-explanatory if you're a seasoned developer; basically, if the incoming access token is decoded successfully, which means a given user is authenticated, the code tries to figure out if they have the right permissions to access the current route.

...

$user = $this->em->getRepository('App:User')
        ->findOneBy(['id' => $decoded->sub]);

$identity = $this->em->getRepository('EasyAclBundle:Identity')
            ->findBy(['user' => $user]);

$rolename = $identity[0]->getRole()->getName();
$routename = $event->getRequest()->get('_route');

$isAllowed = $this->em->getRepository('EasyAclBundle:Permission')
            ->isAllowed($rolename, $routename);

...

Only two easy ACL repositories are required (

Identity
and
Permission
) to determine if the user can access the current route.

Configuration

Let's now take a look at how the magic works. In a nutshell, it's all about defining your app's routes:

# config/routes.yaml
api_post_create:
    path:       /api/posts
    controller: App\Controller\Post\CreateController::index
    methods:    POST

api_post_delete:
    path:       /api/posts/{id}
    controller: App\Controller\Post\DeleteController::index
    methods:    DELETE

api_post_edit:
    path:       /api/posts/{id}
    controller: App\Controller\Post\EditController::index
    methods:    PUT

As well as permissions:

# config/packages/programarivm_easy_acl.yaml
programarivm_easy_acl:
  target: App\Entity\User
  permission:
    -
      role: Superadmin
      routes:
        - api_post_create
        - api_post_delete
        - api_post_edit
    -
      role: Admin
      routes:
        - api_post_create
        - api_post_edit
    -
      role: Basic
      routes:
        - api_post_create

So, if your database schema is now updated:

php bin/console doctrine:schema:update --force

Four empty tables will be added to your database:

  • easy_acl_identity
  • easy_acl_permission
  • easy_acl_role
  • easy_acl_route

Those four go hand in hand with the following entities:

And repositories:

Finally, the

easy-acl:setup
console command is to populate the easy ACL tables.

php bin/console easy-acl:setup
This will reset the ACL. Are you sure to continue? (y) y

MySQL console:

mysql> select * from easy_acl_identity;
Empty set (0.01 sec)

mysql> select * from easy_acl_permission;
+----+------------+-----------------+
| id | rolename   | routename       |
+----+------------+-----------------+
|  1 | Superadmin | api_post_create |
|  2 | Superadmin | api_post_delete |
|  3 | Superadmin | api_post_edit   |
|  4 | Admin      | api_post_create |
|  5 | Admin      | api_post_edit   |
|  6 | Basic      | api_post_create |
+----+------------+-----------------+
6 rows in set (0.00 sec)

mysql> select * from easy_acl_role;
+----+------------+
| id | name       |
+----+------------+
|  1 | Superadmin |
|  2 | Admin      |
|  3 | Basic      |
+----+------------+
3 rows in set (0.00 sec)

mysql> select * from easy_acl_route;
+----+-----------------+---------+-----------------+
| id | name            | methods | path            |
+----+-----------------+---------+-----------------+
|  1 | api_post_create | POST    | /api/posts      |
|  2 | api_post_delete | DELETE  | /api/posts/{id} |
|  3 | api_post_edit   | PUT     | /api/posts/{id} |
+----+-----------------+---------+-----------------+
3 rows in set (0.00 sec)

Adding user identities

The concept of user identity makes it possible for the bundle to not interfere with your database at all, which is not changed by it.

As you can see, three

EasyAcl
tables are populated with data, but of course it is up to you to dynamically define your users' identities as in the data fixtures example shown below.

// src/DataFixtures/EasyAcl/IdentityFixtures.php

namespace App\DataFixtures\EasyAcl;

use App\DataFixtures\UserFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Programarivm\EasyAclBundle\EasyAcl;
use Programarivm\EasyAclBundle\Entity\Identity;

class IdentityFixtures extends Fixture implements FixtureGroupInterface, DependentFixtureInterface
{
    private $easyAcl;

    public function __construct(EasyAcl $easyAcl)
    {
        $this->easyAcl = $easyAcl;
    }

    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < UserFixtures::N; $i++) {
            $index = rand(0, count($this->easyAcl->getPermission())-1);
            $user = $this->getReference("user-$i");
            $role = $this->getReference("role-$index");
            $manager->persist(
                (new Identity())
                    ->setUser($user)
                    ->setRole($role)
            );
        }

        $manager->flush();
    }

    public static function getGroups(): array
    {
        return [
            'easy-acl',
        ];
    }

    public function getDependencies(): array
    {
        return [
            RoleFixtures::class,
            UserFixtures::class,
        ];
    }
}

For further details, the documentation guides you through the process of how to install and set up the easy ACL bundle.

And this is how today's post comes to an end. Was it helpful? I hope so. Tell us in the comments below!

You may also be interested in...

Author profile picture

@BassaganasJordi Bassagañas

Read my stories

Hi there! How are you today? I blog about technology, the Internet, SEO, programming tips, and more.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!