Simple ACL Management in Symfony
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 your application architecture.
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 is all about 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 at all with your database, 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 …
- Writing CASL React Abilities to a JSON file using the Laravel Artisan command.
- SPA GUI Session as Non-HttpOnly Cookie
- Tip for the lazy Symfony developers.