Подтвердить что ты не робот

Как изменить хранилище иерархии ролей в Symfony2?

В моем проекте мне нужно хранить иерархию роли в базе данных и динамически создавать новые роли. В Symfony2 иерархия роли по умолчанию хранится в security.yml. Что я нашел:

Существует услуга security.role_hierarchy (Symfony\Component\Security\Core\Role\RoleHierarchy); Эта служба получает массив ролей в конструкторе:

public function __construct(array $hierarchy)
{
    $this->hierarchy = $hierarchy;

    $this->buildRoleMap();
}

и свойство $hierarchy является закрытым.

Этот аргумент приходит в конструкторе из \Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension::createRoleHierarchy() который использует роли из config, как я понял:

$container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);

Мне кажется, что лучший способ - собрать массив ролей из базы данных и установить его как аргумент для службы. Но я еще не понял, как это сделать.

Второй способ, который я вижу, - определить собственный класс RoleHierarchy, унаследованный от базового. Но так как в базовом классе RoleHierarchy свойство $hierarchy определяется как личное, то мне придется переопределить все функции из базового класса RoleHierarchy. Но я не думаю, что это хороший ООП и способ Symfony...

4b9b3361

Ответ 1

Решение было простым. Сначала я создал объект Role.

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;

    ...
}

после этого создал службу RoleHierarchy, расширенную от родной Symfony. Я унаследовал конструктор, добавил там EntityManager и предоставил исходный конструктор с новым массивом ролей вместо старого:

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

... и переопределил его как службу:

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

Это все. Может быть, в моем коде есть что-то ненужное. Может быть, лучше писать лучше. Но я думаю, что основная идея сейчас очевидна.

Ответ 2

Я делал то же самое, что и zI (для хранения RoleHierarchy в базе данных), но я не могу загрузить полную иерархию роли внутри конструктора, как это сделал zI, потому что мне пришлось загрузить пользовательский фильтр доктрины внутри события kernel.request, Конструктор будет называться до kernel.request, поэтому для меня это не было вариантом.

Поэтому я проверил компонент безопасности и выяснил, что Symfony вызывает пользовательский Voter, чтобы проверить roleHierarchy в соответствии с ролью пользователей:

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 <[email protected]>
 */
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());
    }
}

Метод getReachableRoles возвращает все роли, которыми может быть пользователь. Например:

           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 ]

Если пользователю назначена роль ROLE_SUPERVISOR, метод возвращает роли ROLE_SUPERVISOR, ROLE_BRANCH и ROLE_EMP (Ролевые объекты или классы, которые реализуют RoleInterface)

Кроме того, этот пользовательский избиратель будет отключен, если нет иерархии RoleHierarchy, определенной в 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');
    }

Чтобы решить мою проблему, я создал собственный пользовательский Voter и также расширил класс RoleVoter-класса:

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

Одно примечание. Моя установка похожа на zls. Мое определение для роли (в моем случае я назвал ее 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

И определение пользователя:

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

Может быть, это помогает кому-то.

Ответ 4

Мое решение было вдохновлено решением zls. Его решение отлично работало для меня, но отношение "один ко многим" между ролями означало наличие одного огромного ролевого дерева, которое было бы трудно поддерживать. Кроме того, может возникнуть проблема, если две разные роли хотели наследовать одну и ту же роль (поскольку может быть только один родитель). Поэтому я решил создать решение "многие-ко-многим". Вместо того, чтобы иметь только родителя в классе ролей, я сначала поместил это в класс ролей:

/**
 * @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;

После этого я переписал функцию buildRolesTree следующим образом:

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

В результате получается возможность создания нескольких легко поддерживаемых деревьев. Например, у вас может быть дерево ролей, определяющее роль ROLE_SUPERADMIN и полностью отдельное дерево, определяющее роль ROLE_ADMIN, с несколькими ролями, совместно используемыми между ними. Хотя следует избегать круговых соединений (роли должны быть выложены как деревья, без каких-либо круговых связей между ними), не должно быть никаких проблем, если это происходит на самом деле. Я не тестировал это, но просматривая код buildRoleMap, очевидно, что он сбрасывает любые дубликаты. Это также должно означать, что он не застрянет в бесконечных циклах, если произойдет круговое соединение, но это определенно требует большего тестирования.

Надеюсь, это окажется полезным для кого-то.

Ответ 5

Поскольку иерархия роли не меняется часто, это быстрый класс для кэширования 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));
            }
        }
    }
}