How to change role hierarchy storage in Symfony2?

前端 未结 6 1209
醉话见心
醉话见心 2020-12-22 21:32

In my project I need to store role hierarchy in database and create new roles dynamically. In Symfony2 role hierarchy is stored in security.yml by default. What

相关标签:
6条回答
  • 2020-12-22 21:44

    I developped a bundle.

    You can find it at https://github.com/Spomky-Labs/RoleHierarchyBundle

    0 讨论(0)
  • 2020-12-22 21:45

    My solution was inspired by the solution provided by zls. His solution worked perfectly for me, but the one-to-many relation between the roles meant having one huge role tree, which would become hard to maintain. Also, a problem might occur if two different roles wanted to inherit one same role (as there could only be one parent). That's why I decided to create a many-to-many solution. Instead of having only the parent in the role class, I have first put this in the role class:

    /**
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="role_permission",
     *      joinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="permission_id", referencedColumnName="id")}
     *      )
     */
    protected $children;
    

    After that I rewrote the buildRolesTree function like so:

    private function buildRolesTree()
    {
        $hierarchy = array();
        $roles = $this->em->createQuery('select r, p from AltGrBaseBundle:Role r JOIN r.children p')->execute();
    
        foreach ($roles as $role)
        {
            /* @var $role Role */
            if (count($role->getChildren()) > 0)
            {
                $roleChildren = array();
    
                foreach ($role->getChildren() as $child)
                {
                    /* @var $child Role */
                    $roleChildren[] = $child->getRole();
                }
    
                $hierarchy[$role->getRole()] = $roleChildren;
            }
        }
    
        return $hierarchy;
    }
    

    The result is the ability to create several easily maintained trees. For instance, you can have a tree of roles defining the ROLE_SUPERADMIN role and entirely separate tree defining a ROLE_ADMIN role with several roles shared between them. Although circular connections should be avoided (roles should be laid out as trees, without any circular connections between them), there should be no problems if it actually happens. I haven't tested this, but going through the buildRoleMap code, it is obvious it dumps any duplicates. This should also mean it won't get stuck in endless loops if the circular connection occurs, but this definitely needs more testing.

    I hope this proves helpful to someone.

    0 讨论(0)
  • 2020-12-22 21:46

    I had do the same thing like zIs (to store the RoleHierarchy in the database) but i cannot load the complete role hierarchy inside the Constructor like zIs did, because i had to load a custom doctrine filter inside the kernel.request event. The Constructor will be called before the kernel.request so it was no option for me.

    Therefore I checked the security component and found out that Symfony calls a custom Voter to check the roleHierarchy according to the users role:

    namespace Symfony\Component\Security\Core\Authorization\Voter;
    
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
    
    /**
     * RoleHierarchyVoter uses a RoleHierarchy to determine the roles granted to
     * the user before voting.
     *
     * @author Fabien Potencier <fabien@symfony.com>
     */
    class RoleHierarchyVoter extends RoleVoter
    {
        private $roleHierarchy;
    
        public function __construct(RoleHierarchyInterface $roleHierarchy, $prefix = 'ROLE_')
        {
            $this->roleHierarchy = $roleHierarchy;
    
            parent::__construct($prefix);
        }
    
        /**
         * {@inheritdoc}
         */
        protected function extractRoles(TokenInterface $token)
        {
            return $this->roleHierarchy->getReachableRoles($token->getRoles());
        }
    }
    

    The getReachableRoles Method returns all roles the user can be. For example:

               ROLE_ADMIN
             /             \
         ROLE_SUPERVISIOR  ROLE_BLA
            |               |
         ROLE_BRANCH       ROLE_BLA2
           |
         ROLE_EMP
    
    or in Yaml:
    ROLE_ADMIN:       [ ROLE_SUPERVISIOR, ROLE_BLA ]
    ROLE_SUPERVISIOR: [ ROLE_BRANCH ]
    ROLE_BLA:         [ ROLE_BLA2 ]
    

    If the user has the ROLE_SUPERVISOR role assigned the Method returns the roles ROLE_SUPERVISOR, ROLE_BRANCH and ROLE_EMP (Role-Objects or Classes, which implementing RoleInterface)

    Furthermore this custom voter will be disabled if there is no RoleHierarchy defined in the security.yaml

    private function createRoleHierarchy($config, ContainerBuilder $container)
        {
            if (!isset($config['role_hierarchy'])) {
                $container->removeDefinition('security.access.role_hierarchy_voter');
    
                return;
            }
    
            $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
            $container->removeDefinition('security.access.simple_role_voter');
        }
    

    To solve my issue I created my own custom Voter and extended the RoleVoter-Class, too:

    use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Acme\Foundation\UserBundle\Entity\Group;
    use Doctrine\ORM\EntityManager;
    
    class RoleHierarchyVoter extends RoleVoter {
    
        private $em;
    
        public function __construct(EntityManager $em, $prefix = 'ROLE_') {
    
            $this->em = $em;
    
            parent::__construct($prefix);
        }
    
        /**
         * {@inheritdoc}
         */
        protected function extractRoles(TokenInterface $token) {
    
            $group = $token->getUser()->getGroup();
    
            return $this->getReachableRoles($group);
        }
    
        public function getReachableRoles(Group $group, &$groups = array()) {
    
            $groups[] = $group;
    
            $children = $this->em->getRepository('AcmeFoundationUserBundle:Group')->createQueryBuilder('g')
                            ->where('g.parent = :group')
                            ->setParameter('group', $group->getId())
                            ->getQuery()
                            ->getResult();
    
            foreach($children as $child) {
                $this->getReachableRoles($child, $groups);
            }
    
            return $groups;
        }
    }
    

    One Note: My Setup is similar to zls ones. My Definition for the role (in my case I called it Group):

    Acme\Foundation\UserBundle\Entity\Group:
        type: entity
        table: sec_groups
        id: 
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            name:
                type: string
                length: 50
            role:
                type: string
                length: 20
        manyToOne:
            parent:
                targetEntity: Group
    

    And the userdefinition:

    Acme\Foundation\UserBundle\Entity\User:
        type: entity
        table: sec_users
        repositoryClass: Acme\Foundation\UserBundle\Entity\UserRepository
        id:
            id:
                type: integer
                generator: { strategy: AUTO }
        fields:
            username:
                type: string
                length: 30
            salt:
                type: string
                length: 32
            password:
                type: string
                length: 100
            isActive:
                type: boolean
                column: is_active
        manyToOne:
            group:
                targetEntity: Group
                joinColumn:
                    name: group_id
                    referencedColumnName: id
                    nullable: false
    

    Maybe this helps someone.

    0 讨论(0)
  • 2020-12-22 21:48

    The solution was simple. First I created a Role entity.

    class Role
    {
        /**
         * @var integer $id
         *
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @var string $name
         *
         * @ORM\Column(name="name", type="string", length=255)
         */
        private $name;
    
        /**
         * @ORM\ManyToOne(targetEntity="Role")
         * @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
         **/
        private $parent;
    
        ...
    }
    

    after that created a RoleHierarchy service, extended from the Symfony native one. I inherited the constructor, added an EntityManager there and provided an original constructor with a new roles array instead of the old one:

    class RoleHierarchy extends Symfony\Component\Security\Core\Role\RoleHierarchy
    {
        private $em;
    
        /**
         * @param array $hierarchy
         */
        public function __construct(array $hierarchy, EntityManager $em)
        {
            $this->em = $em;
            parent::__construct($this->buildRolesTree());
        }
    
        /**
         * Here we build an array with roles. It looks like a two-levelled tree - just 
         * like original Symfony roles are stored in security.yml
         * @return array
         */
        private function buildRolesTree()
        {
            $hierarchy = array();
            $roles = $this->em->createQuery('select r from UserBundle:Role r')->execute();
            foreach ($roles as $role) {
                /** @var $role Role */
                if ($role->getParent()) {
                    if (!isset($hierarchy[$role->getParent()->getName()])) {
                        $hierarchy[$role->getParent()->getName()] = array();
                    }
                    $hierarchy[$role->getParent()->getName()][] = $role->getName();
                } else {
                    if (!isset($hierarchy[$role->getName()])) {
                        $hierarchy[$role->getName()] = array();
                    }
                }
            }
            return $hierarchy;
        }
    }
    

    ... and redefined it as a service:

    <services>
        <service id="security.role_hierarchy" class="Acme\UserBundle\Security\Role\RoleHierarchy" public="false">
            <argument>%security.role_hierarchy.roles%</argument>
            <argument type="service" id="doctrine.orm.default_entity_manager"/>
        </service>
    </services>
    

    That's all. Maybe, there is something unnecessary in my code. Maybe it is possible to write better. But I think, that main idea is evident now.

    0 讨论(0)
  • 2020-12-22 21:56

    I hope this will help you.

    function getRoles()
    {
    
      //  return array(1=>'ROLE_ADMIN',2=>'ROLE_USER'); 
       return array(new UserRole($this));
    }
    

    You can get a good idea from, Where to define security roles?

    http://php-and-symfony.matthiasnoback.nl/ ( 2012 July 28 )

    0 讨论(0)
  • 2020-12-22 22:07

    Since role hierarchy don't change often, this a quick class to cache to memcached.

    <?php
    
    namespace .....;
    
    use Symfony\Component\Security\Core\Role\Role;
    use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
    use Lsw\MemcacheBundle\Cache\MemcacheInterface;
    
    /**
     * RoleHierarchy defines a role hierarchy.
     */
    class RoleHierarchy implements RoleHierarchyInterface
    {
        /**
         *
         * @var MemcacheInterface 
         */
        private $memcache;
    
        /**
         *
         * @var array 
         */
        private $hierarchy;
    
        /**
         *
         * @var array 
         */
        protected $map;
    
        /**
         * Constructor.
         *
         * @param array $hierarchy An array defining the hierarchy
         */
        public function __construct(array $hierarchy, MemcacheInterface $memcache)
        {
            $this->hierarchy = $hierarchy;
    
            $roleMap = $memcache->get('roleMap');
    
            if ($roleMap) {
                $this->map = unserialize($roleMap);
            } else {
                $this->buildRoleMap();
                // cache to memcache
                $memcache->set('roleMap', serialize($this->map));
            }
        }
    
        /**
         * {@inheritdoc}
         */
        public function getReachableRoles(array $roles)
        {
            $reachableRoles = $roles;
            foreach ($roles as $role) {
                if (!isset($this->map[$role->getRole()])) {
                    continue;
                }
    
                foreach ($this->map[$role->getRole()] as $r) {
                    $reachableRoles[] = new Role($r);
                }
            }
    
            return $reachableRoles;
        }
    
        protected function buildRoleMap()
        {
            $this->map = array();
            foreach ($this->hierarchy as $main => $roles) {
                $this->map[$main] = $roles;
                $visited = array();
                $additionalRoles = $roles;
                while ($role = array_shift($additionalRoles)) {
                    if (!isset($this->hierarchy[$role])) {
                        continue;
                    }
    
                    $visited[] = $role;
                    $this->map[$main] = array_unique(array_merge($this->map[$main], $this->hierarchy[$role]));
                    $additionalRoles = array_merge($additionalRoles, array_diff($this->hierarchy[$role], $visited));
                }
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题