Еще раз о Security в Symfony2 подход user-resource-privilege

Не так давно взялся за Symfony2. Не смотря на то, что до этого имел достаточно богатый опыт общения с Zend1, барьер входа для меня оказался высоким. Вдоволь начитавшись у меня начало что-то получаться. Наибольшие затруднения вызвал вопрос разграничения прав доступа. Практически все мои поиски выводили меня на FOSUserBundle или обрывки информации о том, как можно расширить функционал модуля Security из стандартной поставки фреймворка. Каких-либо преимуществ для себя в громоздком FOSUserBundle я не обнаружил. Поэтому эта статья будет о том, как я допиливал Symfony2 Security под свои нужды. Цель была следующая: symfony2 + security + разграничение прав доступа на уровне объекта в зависимости от роли пользователя. В этой статье не будет ничего про наследование ролей и кумулятивные привилегии, информацию о которых вы, без труда, найдете сами. Схема прав в моем проекте: запрещено все, что не разрешено. Один пользователь имеет строго одну роль. Роль имеет доступ к различным ресурсам с различным набором привилегий. Разные роли могут иметь доступ к одним и тем же ресурсам с разными или равными наборами привилегий. Я не буду пытаться сделать код максимально абстрактным, а просто буду использовать фрагменты из своего проекта, связанные с функциональностью заказ-нарядов на обслуживание техники.

Итак, к делу. У нас есть правильно настроенный проект, в нем создан BackendWorkorderBundle, настроены все роутеры и фаерволы. Т.е. есть все, за исключением прав доступа. Включая аутификацию. Для проектирование БД использовался инструмент MySQL Workbench. Отличная штука. Есть версия под Linux. Структура таблиц выглядит так:

-- ----------------------------------------------------- -- Table `backend_role` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `backend_role` (   `role_id` INT NOT NULL AUTO_INCREMENT,   `name` VARCHAR(45) NULL,   `description` VARCHAR(45) NULL,   PRIMARY KEY (`role_id`)) ENGINE = InnoDB;   -- ----------------------------------------------------- -- Table `backend_user` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `backend_user` (   `user_id` INT NOT NULL AUTO_INCREMENT,   `role_id` INT NOT NULL,   `firstname` VARCHAR(45) NULL,   `lastname` VARCHAR(45) NULL,   `printname` VARCHAR(45) NULL,   `username` VARCHAR(45) NULL,   `salt` VARCHAR(255) NULL,   `password` VARCHAR(255) NULL,   `created` DATETIME NULL,   `updated` DATETIME NULL,   `last_login` DATETIME NULL,   `is_active` TINYINT(1) NULL,   PRIMARY KEY (`user_id`),   INDEX `fk_backend_user_backend_role1_idx` (`role_id` ASC),   CONSTRAINT `fk_backend_user_backend_role1`     FOREIGN KEY (`role_id`)     REFERENCES `parts`.`backend_role` (`role_id`)     ON DELETE NO ACTION     ON UPDATE NO ACTION) ENGINE = InnoDB;   -- ----------------------------------------------------- -- Table `backend_rule` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `backend_rule` (   `rule_id` INT NOT NULL AUTO_INCREMENT,   `role_id` INT NOT NULL,   `resource_id` VARCHAR(255) NULL,   `privileges` TEXT NULL,   PRIMARY KEY (`rule_id`),   INDEX `fk_backend_rule_backend_role1_idx` (`role_id` ASC),   CONSTRAINT `fk_backend_rule_backend_role1`     FOREIGN KEY (`role_id`)     REFERENCES `parts`.`backend_role` (`role_id`)     ON DELETE NO ACTION     ON UPDATE NO ACTION) ENGINE = InnoDB; 

Проверять наличие привилегий можно двумя способами:
1. Из twig is_granted('[наименование привилегии]', [объект])
2. Из контроллера $this->get('security.context')->isGranted('[наименование привилегии]', [объект])
Второй аргумент не обязателен, но необходим для целей моего проекта (станет понятно чуть ниже в коде voter’а). Напоминаю, что исключение объекта из html страницы не отменяет проверку данных в контроллере.

Код voter’a. Забыл упомянуть, что в проекте есть есть еще один бандл BackendCoreBundle, которые вбирает в себя наиболее общие функции для всего Backend’a

<?php // /src/Backend/CoreBundle/Security/Authorization/Voter/PrivilegeVoter.php  namespace Backend\CoreBundle\Security\Authorization\Voter;  use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;  class PrivilegeVoter implements VoterInterface  {     public function supportsAttribute($attribute)      {         return true;     }      public function supportsClass($class)     {         return in_array($class, array(           'Backend\WorkorderBundle\Entity\Workorder'        ));     }      public function vote(TokenInterface $token, $object, array $attributes)     { 	//применим ли voter к объекту определенного класса. 	//необходимо так как наш вотер будет опрашиваться во всех случаях контроля привилегий.         if ( !($this->supportsClass(get_class($object))) ) {             return VoterInterface::ACCESS_ABSTAIN;         }          foreach ($attributes as $attribute) { //необходимо адаптировать функцию под ваши нужды             if ( !$this->supportsAttribute($attribute) ) {                 return VoterInterface::ACCESS_ABSTAIN;             }         }  	//магия творится здесь         $user = $token->getUser();         $privileges = $user->getPrivileges();         $resourceId = $object->getResourceId();                  $acess_granted = false;         foreach ($attributes as $attribute) {             if (isset($privileges[$resourceId])) {                 $resource_privileges = $privileges[$resourceId];                 if (in_array($attribute, $resource_privileges)) {                     $acess_granted = true;                 } else {                     $acess_granted = false;                     break;                 }             }         }                  if ($acess_granted)             return VoterInterface::ACCESS_GRANTED;                return VoterInterface::ACCESS_DENIED;     } } 

Фунция getPrivileges для user объявлена в объекте doctrine, связанном с таблицей backend_user

<?php ///src/Backend/CoreBundle/Entity/BackendUser.php  namespace Backend\CoreBundle\Entity;  use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\AdvancedUserInterface;  /**  * BackendUser  *  * @ORM\Table(name="backend_user")  * @ORM\Entity  */ class BackendUser implements AdvancedUserInterface, \Serializable { ..     public function getPrivileges()     { 	//цепочка выглядит так: backend_user->backend_role->backend_rule 	//функция $rule->getPrivileges() возвращает значение поля privileges таблицы backend_rule 	//то есть текущая функция возвращает массив ключами которого являеются resource_id, 	//а элементами массивы привилений для доступа к этому ресурсу (хранятся через запятую)          $rules = $this->getRole()->getRules();         $result = array();         foreach ($rules as $rule){             $result[$rule->getResourceId()] = explode(",", $rule->getPrivileges());         }         return $result;     } .. } 

Регистрируем voter в /app/config/security.yml

services:     security.access.privilege_voter:         class:      Backend\CoreBundle\Security\Authorization\Voter\PrivilegeVoter         public:     false         tags:            - { name: security.voter } 

Вы, наверное, обратили внимание, что в функции vote вызывается $object->getResourceId(). Выглядит метод следующим образом

<?php // /src/Backend/WorkorderBundle/Entity/Workorder.php namespace Backend\WorkorderBundle\Entity; use Doctrine\Common\Collections\ArrayCollection;  use Doctrine\ORM\Mapping as ORM;  /**  * Workorder  *  * @ORM\Table(name="workorder")  * @ORM\Entity  */ class Workorder { ..     public function getResourceId()     { 	//функция добавлена для гибкости и в текущий момент возвращает имя класса 	//В данном случае Backend\WorkorderBundle\Entity\Workorder         return get_class($this);     } .. } 

That’s it! Критика, как обычно, привествуется, если кто-то может указать на недостатвки этого подхода и возможные проблемы при масштабировании — был бы очень рад.

ссылка на оригинал статьи http://habrahabr.ru/post/203358/

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

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