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 have I found:
There is a service security.role_hierarchy
(Symfony\Component\Security\Core\Role\RoleHierarchy
);
This service receives a roles array in constructor:
public function __construct(array $hierarchy)
{
$this->hierarchy = $hierarchy;
$this->buildRoleMap();
}
and the $hierarchy
property is private.
This argument comes in constructor from \Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension::createRoleHierarchy()
which uses roles from config, as I understood:
$container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
It seems me that the best way is to compile an array of roles from database and set it as an argument for the service. But I haven't yet understood how to do it.
The second way I see is to define my own RoleHierarchy
class inherited from the base one. But since in the base RoleHierarchy
class the $hierarchy
property is defined as private, than I would have to redefine all the functions from the base RoleHierarchy
class. But I don't think it is a good OOP and Symfony way...
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 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 <[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());
}
}
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.
I developped a bundle.
You can find it at https://github.com/Spomky-Labs/RoleHierarchyBundle
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With