Как изменить хранилище иерархии ролей в 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...
Ответы
Ответ 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
Может быть, это помогает кому-то.
Ответ 3
Я разработал пакет.
Вы можете найти его на https://github.com/Spomky-Labs/RoleHierarchyBundle
Ответ 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));
}
}
}
}
Ответ 6
Надеюсь, это поможет вам.
function getRoles()
{
// return array(1=>'ROLE_ADMIN',2=>'ROLE_USER');
return array(new UserRole($this));
}
Вы можете получить хорошую идею,
Где определить роли безопасности?
http://php-and-symfony.matthiasnoback.nl/ (2012 28 июля)