get users who have a specific role - symfony

I need to get the list of all my users having a specific role, is there any way to do it easily? The solution I figured out for now would be to retrive all users then apply a filter on each using is granted function (which is hardcore)
PS: I don't like using the db request that skims over data and if the user role equals the wanted role it returns it, else it doesn't. Which means that we don't take into account users with super roles.

Because of the role hierarchy, I don't see a way to avoid grabbing all the users and then filtering. You could make a user role table and add all possible user roles but that would get out of date if you changed the hierarchy.
However, once you have all the roles for a given user then you can test if a specific one is supported.
There is a role hierarchy object to help.
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
class RoleChecker
{
protected $roleHeirarchy;
public function __construct(RoleHierarchy $roleHierarchy)
{
$this->roleHierarchy = $roleHierarchy; // serviceId = security.role_hierarchy
}
protected function hasRole($roles,$targetRole)
{
$reachableRoles = $this->roleHierarchy->getReachableRoles($roles);
foreach($reachableRoles as $role)
{
if ($role->getRole() == $targetRole) return true;
}
return false;
}
}
# services.yml
# You need to alias the security.role_hierarchy service
cerad_core__role_hierarchy:
alias: security.role_hierarchy
You need to pass an array of role objects to hasRole. This is basically the same code that the security context object uses. I could not find another Symfony service just for this.
The is also a parameter value called '%security.role_hierarchy.roles%' that comes in handy at times as well.

Symfony 5 answer, it's a little bit easier:
namespace App\Controller;
...
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
class UserController extends AbstractController
{
private $roleHierarchy;
/**
* #Route("/users", name="users")
*/
public function usersIndex(RoleHierarchyInterface $roleHierarchy)
{
$this->roleHierarchy = $roleHierarchy;
// your user service or your Doctrine code here
$users = ...
foreach ($users as $user) {
$roles = $roleHierarchy->getReachableRoleNames($user->getRoles());
\dump($roles);
if ($this->isGranted($user, 'ROLE_SUPER_ADMIN')) {
...
}
}
...
}
private function isGranted(User $user, string $role): bool
{
$reachableRoles = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
foreach ($reachableRoles as $reachableRole) {
if ($reachableRole === $role) {
return true;
}
}
return false;
}
}
Note: I put everything in the controller for the sake of simplicity here, but of course I'd recommend to move the Role Management code into a separate service.

Related

best design to make sure users access only their information

I have a symfony2.7 application with doctrine which has a large amount of entities, many of these entities are dependent of a single entity, let me put an example to clarify
School hasMany Student,
Student hasMany Lesson,
Lesson hasMany Test,
Test hasMany Grade
Please imagine a db design like that but larger and that many schools are allowed. Say you have a role "SCHOOL_DIRECTOR" which can edit any grade in the school using this url /grade/{id}/edit, using symfony security I can make it so that only users with the SCHOOL_DIRECTOR role can access that url, so far so good, but right now my problem is that a SCHOOL_DIRECTOR from one school can, if he puts the correct id in the url, edit grades from another school.
I'm looking for the best way to go, because this happens with many entities, I know I could always make queries which joins back to the school entity but I'm worried about the performance impact because I would be constantly making queries with many joins. I wonder what is the best way to make that school directors can only acces the information on their school.
When you are trying to grant access of specific users/roles to specifics object domains(school,grade,test,...) you should take a look to ACL.
In the simplest way acl allows to do what you want.
Hope it helps
You should take a look at the symfony voter
Example:
app/config/services.yml
app.security.voter.grade:
class: AppBundle\Security\Voter\GradeVoter
tags:
- { name: security.voter }
AppBundle\Security\Voter\GradeVoter
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class GradeVoter extends Voter
{
const EDIT = 'edit';
const VIEW = 'view';
protected function supports($action, $subject)
{
if(!$subject instanceof Grade) {
return false;
}
if(!in_array($action, $this->supportedActions())) {
return false;
}
return true;
}
protected function voteOnAttribute($action, $grade, TokenInterface $token)
{
/** #var Grade $grade */
switch ($action)
{
case self::VIEW:
return true;
break;
case self::EDIT:
// Do your stuff to return true if the the connected user can edit this grade
$user = $token->getUser();
// From the $user variable (which represent the connected user) you have to decide if he can edit this grade, and return true or false.
return true;
break;
default:
return false;
}
}
public function supportedActions()
{
return [self::EDIT, self::VIEW];
}
}
GradeController
public function editAction(Grade $grade)
{
$this->denyAccessUnlessGranted(GradeVoter::EDIT, $grade, 'You can\'t edit this grade');
// Do your stuff to edit
return new JsonResponse($response, Response::HTTP_OK);
}
$this->denyAccessUnlessGranted(GradeVoter::EDIT, $grade, 'You can\'t edit this grade');
This line do all the magic, Symfony will call your Voter service and do the above verification (in this case the edit switch case just return true)
If the voter return false, symfony will throw an AccessDenied exception, that will be catch and finish in a 403 response
I hope my answer helped you. Ask me if something isn't clear :)

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

how to use Symfony methods Action excluding the "Action" word

I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.

prevent some user from viewing some pages

I have a login form and after login I display some links:
I want to prevent non-admin users to click or forward to a specific page.
I don't want to use the symfony2 ROLES cause it is too complicated.
Is there something easier ?
Depending on how do you make the difference between admin and non-admin users in your User entity. If it's only a boolean flag (let's say admin attribute) :
User.php
private $admin;
// your attributes
public function isAdmin()
{
return $this->admin;
}
public function setAdmin($boolean)
{
$this->admin = $boolean;
}
// getters/setters
FooController.php
public function showAdminPanelAction()
{
if(!$this->getUser()->isAdmin()) {
throw new AccessDeniedHttpException('Forbidden Access');
}
else
{
// do your stuff
}
}
BUT Symfony2 roles are making things easier if you have more than 2 two different roles, a hierarchy, lots of users, etc...

Symfony2: creating a new SecurityIdentity

I'm using ACL in Symfony 2.1, and I need to create a new SecurityIdentity, so that my ACL can be set in function of some sort of groups.
Picture the following situation: there are groups with users (with different roles) that each have user information. In group 1, users with the ROLE_ADMIN can't edit other users from the same group's information, but in group 2, users with ROLE_ADMIN can edit others information.
So basically my ACL will vary in function of what group the user is in.
I thought I'd start solving this problem with the creation of a new "GroupSecurityIdentity". However the class itself doesn't suffice, as I get this exception when I use it:
$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.
My question is: how do I "register" my new SecurityIdentity so I can use it as RoleSecurityIdentity and UserSecurityIdentity?
What better ways are there to implement a system similar to this I want to do?
2 years ago I went down that path, it turned out to be a bad decision. Modifying the ACL system is difficult and might cause problems when updating Symfony. There are at least 2 better solutions. I'll list them all so you can decide which best suits your needs.
New security identity
I'm using the GroupInterface from FOSUserBundle, but I guess you could use your own too. The following files need to be added:
AclProvider.php
The method to change is private - the whole file has to be copied, but the only change has to be made to hydrateObjectIdentities
GroupSecurityIdentity.php
MutableAclProvider.php
We have to duplicate the whole file as it must extend AclProvider, but we're using a custom one and can't therefore extend the stock MutableAclProvider. The methods changed are getInsertSecurityIdentitySql and getSelectSecurityIdentityIdSql.
SecurityIdentityRetrievalStrategy.php
Next up: rewire the dependency injection container by providing the following parameters:
<parameter key="security.acl.dbal.provider.class">
Acme\Bundle\DemoBundle\Security\Acl\Dbal\MutableAclProvider
</parameter>
<parameter key="security.acl.security_identity_retrieval_strategy.class">
Acme\Bundle\DemoBundle\Security\Acl\Domain\SecurityIdentityRetrievalStrategy
</parameter>
Time to cross fingers and see whether it works. Since this is old code I might have forgotten something.
Use roles for groups
The idea is to have group names correspond to roles.
A simple way is to have your User entity re-implement UserInterface::getRoles:
public function getRoles()
{
$roles = parent::getRoles();
// This can be cached should there be any performance issues
// which I highly doubt there would be.
foreach ($this->getGroups() as $group) {
// GroupInterface::getRole() would probably have to use its
// canonical name to get something like `ROLE_GROUP_NAME_OF_GROUP`
$roles[] = $group->getRole();
}
return $roles;
}
A possible implementation of GroupInterface::getRole():
public function getRole()
{
$name = $this->getNameCanonical();
return 'ROLE_GROUP_'.mb_convert_case($name, MB_CASE_UPPER, 'UTF-8');
}
It's now just a matter of creating the required ACE-s as written in the cookbook article.
Create a voter
Finally, you could use custom voters that check for the presence of specific groups and whether the user has access to said object. A possible implementation:
<?php
namespace Acme\Bundle\DemoBundle\Authorization\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class MySecureObjectVoter implements VoterInterface
{
/**
* {#inheritDoc}
*/
public function supportsAttribute($attribute)
{
$supported = array('VIEW');
return in_array($attribute, $supported);
}
/**
* {#inheritDoc}
*/
public function supportsClass($class)
{
return $class instanceof GroupableInterface;
}
/**
* {#inheritDoc}
*/
public function vote(TokenInterface $token, $object, array $attributes)
{
$result = VoterInterface::ACCESS_ABSTAIN;
if (!$object instanceof MySecureObject) {
return VoterInterface::ACCESS_ABSTAIN;
}
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}
// Access is granted, if the user and object have at least 1
// group in common.
if ('VIEW' === $attribute) {
$objGroups = $object->getGroups();
$userGroups = $token->getUser()->getGroups();
foreach ($userGroups as $userGroup) {
foreach ($objGroups as $objGroup) {
if ($userGroup->equals($objGroup)) {
return VoterInterface::ACCESS_GRANTED;
}
}
}
return voterInterface::ACCESS_DENIED;
}
}
}
}
For more details on voters please refer to the cookbook example.
I would avoid creating a custom security identity. Use the two other methods provided. The second solution works best, if you will be having lots of records and each of them must have different access settings. Voters could be used for setting up simple access granting logic (which most smaller systems seem to fall under) or when flexibility is necessary.
I write my answer here to keep a track of this error message.
I implemented group support with ACL and i had to hack a bit the symfony core "MutableAclProvider.php"
protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid)
{
if ($sid instanceof UserSecurityIdentity) {
$identifier = $sid->getClass().'-'.$sid->getUsername();
$username = true;
} elseif ($sid instanceof RoleSecurityIdentity) {
$identifier = $sid->getRole();
$username = false;
}else {
//throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
$identifier = $sid->getClass().'-'.$sid->getGroupname();
$username = true;
}
return sprintf(
'SELECT id FROM %s WHERE identifier = %s AND username = %s',
$this->options['sid_table_name'],
$this->connection->quote($identifier),
$this->connection->getDatabasePlatform()->convertBooleans($username)
);
}
Even if the provided object is not an instance of UserSecurityIdentity or RoleSecurityIdentity it return a value. So now i can use a custom "GroupSecurityIdentity"
It's not easy to put in place but was much adapted to my system.

Resources