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
I developped a bundle.
You can find it at https://github.com/Spomky-Labs/RoleHierarchyBundle
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.
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.
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.
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 )
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));
}
}
}
}