📜 ⬆️ ⬇️

Once again about Security in Symfony2 the user-resource-privilege approach

Not so long ago, took up Symfony2. Despite the fact that I had had quite a lot of experience with Zend1 before, the entry barrier was high for me. I read a lot when I started to get something. The greatest difficulty was caused by the issue of differentiation of access rights. Almost all my searches led me to FOSUserBundle or scraps of information on how to extend the functionality of the Security module from the standard framework delivery. I did not find any advantages for myself in the cumbersome FOSUserBundle. Therefore, this article will be about how I finished Symfony2 Security to fit my needs. The goal was the following: symfony2 + security + differentiation of access rights at the object level, depending on the role of the user. In this article there will be nothing about the inheritance of roles and cumulative privileges, information about which you will easily find yourself. Rights scheme in my project: everything that is not allowed is prohibited. One user has strictly one role. A role has access to various resources with a different set of privileges. Different roles may have access to the same resources with different or equal sets of privileges. I will not try to make the code as abstract as possible, but simply use fragments from my project related to the functionality of service orders.

So, to the point. We have a properly configured project , the BackendWorkorderBundle is created in it, all routers and firewalls are configured. Those. there is everything except access rights. Including authentication. For the design of the database, the MySQL Workbench tool was used. Great stuff. There is a version for Linux. The structure of the tables is as follows:

-- ----------------------------------------------------- -- 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; 

You can check for privileges in two ways:
1. From twig is_granted('[ ]', [])
2. From the controller $this->get('security.context')->isGranted('[ ]', [])
The second argument is optional, but necessary for the purposes of my project (it will become clear a little lower in the voter code). I remind you that excluding an object from the html page does not cancel the data check in the controller.

Code voter'a. I forgot to mention that there is another BackendCoreBundle bundle in the project, which incorporates the most common functions for the whole Backend. Read more about voters here .
 <?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; } } 

')
The getPrivileges function for user is declared in the doctrine object associated with the backend_user table.
 <?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; } .. } 


Register voter in /app/config/security.yml

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

You probably noticed that in the vote function, $ object-> getResourceId () is called. The method looks like this:
 <?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 \Doctrine\Common\Util\ClassUtils::getClass($this); } .. } 


That's it! Criticism, as usual, is welcome, if someone can point out the shortcomings of this approach and possible problems when scaling - would be very happy.

Source: https://habr.com/ru/post/203358/


All Articles