Простое управление ACL в Symfony

от автора

Перевод статьи подготовлен в преддверии старта курса «Symfony Framework».


Не секрет, что ACL (access control lists) могут быть достаточно сложны в использовании. Поскольку Symfony рекомендует избирателей (voters) в качестве альтернативы ACL, я недавно решил, что напишу свой собственный простой в использовании Symfony 5 бандл для управления списками контроля доступа (ACL) в моих приложениях.

programarivm/easy-acl-bundle изначально был написан для использования в JWT-аутентифицированном API для одностраничных приложений (single page applications — SPA), но он также может быть полезен в ряде других сценариев, когда не требуется Security компонент — что в большинстве случаев, по моему скромному мнению, особенно подходит для сеансов обработки многостраничных приложений (multi-page applications — MPA).

EasyAclBundle.

полностью полагается на сущности и репозитории Doctrine, что означает, что разрешения просто хранятся в базе данных без привязки к архитектуре вашего приложения.

Тем не менее, вот как легко JWT-токены аутентифицируются и авторизуются в подписчике событий с помощью так называемых easy ACL-репозиториев.

// 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();          // когда класс контроллера определяет несколько 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',         ];     } }

Большая часть этого кода не требует объяснения, если вы опытный разработчик; в основном, если входящий токен доступа успешно декодирован, что означает, что данный пользователь аутентифицирован, код пытается выяснить, имеет ли он права для доступа к текущему маршруту.

...  $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);  ...

Достаточно только двух easy ACL-репозиториев (Identity и Permission) для определения того, может ли пользователь получить доступ к текущему маршруту.

Конфигурация

Теперь давайте посмотрим в чем же вся магия. В целом, все заключается в определении маршрутов вашего приложения:

# 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

А также разрешений:

# 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

Итак, теперь, если ваша схема базы данных обновлена:

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

Четыре пустые таблицы будут добавлены в вашу базу данных:

  • easy_acl_identity
  • easy_acl_permission
  • easy_acl_role
  • easy_acl_route

Эта четверка идет рука об руку со следующими сущностями:

  • Programarivm\EasyAclBundle\Entity\Identity
  • Programarivm\EasyAclBundle\Entity\Permission
  • Programarivm\EasyAclBundle\Entity\Role
  • Programarivm\EasyAclBundle\Entity\Route

И репозиториями:

  • Programarivm\EasyAclBundle\Repository\IdentityRepository
  • Programarivm\EasyAclBundle\Repository\PermissionRepository
  • Programarivm\EasyAclBundle\Repository\RoleRepository
  • Programarivm\EasyAclBundle\Repository\RouteRepository

Наконец, консольная команда easy-acl:setup предназначена для заполнения таблиц easy ACL.

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

Консоль MySQL:

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)

Добавление идентификаторов пользователей

Концепция идентификаторов пользователей позволяет пакету вообще не вмешиваться в вашу базу данных, которая не изменяется им.

Как вы можете видеть, три EasyAcl таблицы заполнены данными, но, конечно же, это ваша задача — динамически определять идентификационные данные своих пользователей, как в примере, показанном ниже.

// 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,         ];     } } 

Для получения более подробной информации читайте документацию, которая проведет вас через процесс установки и настройки бандла easy ACL.

На этом все. Был ли этот пост полезен? Я надеюсь, что да. Расскажите нам в комментариях ниже!

Возможно, вас также заинтересует…


Узнать о курсе подробнее.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/508424/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *