Simple ACL Management in Symfony

A translation of the article was prepared specifically for the course. Symfony Framework.


It’s no secret that ACLs (access control lists) can be quite difficult to use. Because symfony recommends voters (voters) As an alternative to ACLs, I recently decided that I would write my own easy-to-use Symfony 5 bundle for managing access control lists (ACLs) in my applications.

programarivm/easy-acl-bundle It was originally written for use in the JWT-authenticated API for single page applications (SPA), but it can also be useful in a number of other scenarios when the Security component is not required – which in most cases, in my humble opinion, is especially suitable for multi-page applications (MPA) processing sessions.

EasyAclBundle
relies heavily on Doctrine entities and repositories, which means that permissions are simply stored in the database without being tied to the architecture of your application.

However, this is how easy JWT tokens are authenticated and authorized in the event subscriber using the so-called easy ACL repositories.

// src/EventSubscriber/TokenSubscriber.php

namespace AppEventSubscriber;

use AppControllerAccessTokenController;
use DoctrineORMEntityManagerInterface;
use FirebaseJWTJWT;
use SymfonyComponentEventDispatcherEventSubscriberInterface;
use SymfonyComponentHttpKernelEventControllerEvent;
use SymfonyComponentHttpKernelExceptionAccessDeniedHttpException;
use SymfonyComponentHttpKernelKernelEvents;

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

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

        // когда класс контроллера определяет несколько action методов, контроллер
        // возвращается как [$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 requires no explanation if you are an experienced developer; basically, if the incoming access token is successfully decoded, which means that this user is authenticated, the code tries to find out if he has rights 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 (Identity and Permission) to determine if the user can access the current route.

Configuration

Now let’s see what all the magic is. In general, it all consists in determining the routes of your application:

# config/routes.yaml
api_post_create:
    path:       /api/posts
    controller: AppControllerPostCreateController::index
    methods:    POST

api_post_delete:
    path:       /api/posts/{id}
    controller: AppControllerPostDeleteController::index
    methods:    DELETE

api_post_edit:
    path:       /api/posts/{id}
    controller: AppControllerPostEditController::index
    methods:    PUT

As well as permissions:

# config/packages/programarivm_easy_acl.yaml
programarivm_easy_acl:
  target: AppEntityUser
  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 now, if your database schema is 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

This four goes hand in hand with the following entities:

  • ProgramarivmEasyAclBundleEntityIdentity
  • ProgramarivmEasyAclBundleEntityPermission
  • ProgramarivmEasyAclBundleEntityRole
  • ProgramarivmEasyAclBundleEntityRoute

And repositories:

  • ProgramarivmEasyAclBundleRepositoryIdentityRepository
  • ProgramarivmEasyAclBundleRepositoryPermissionRepository
  • ProgramarivmEasyAclBundleRepositoryRoleRepository
  • ProgramarivmEasyAclBundleRepositoryRouteRepository

Finally, the console command easy-acl:setup Designed to populate 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 IDs

The concept of user identifiers allows the package to not interfere with your database at all, which does not change it.

As you can see, three EasyAcl the tables are filled with data, but, of course, it is your task to dynamically determine the identity of your users, as in the example shown below.

// src/DataFixtures/EasyAcl/IdentityFixtures.php

namespace AppDataFixturesEasyAcl;

use AppDataFixturesUserFixtures;
use DoctrineBundleFixturesBundleFixture;
use DoctrineBundleFixturesBundleFixtureGroupInterface;
use DoctrineCommonDataFixturesDependentFixtureInterface;
use DoctrineCommonPersistenceObjectManager;
use ProgramarivmEasyAclBundleEasyAcl;
use ProgramarivmEasyAclBundleEntityIdentity;

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 more information read the documentationwhich will guide you through the installation and configuration of the easy ACL bundle.

That’s all. Was this post helpful? I hope that yes. Tell us in the comments below!

You might also be interested in …


Symfony Fast start

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *