Symfony2 how to disable default voter? - symfony

I have five custom voters in my application and use strategy "consensus".
Sometimes my voters not work properly and after debugging I have found the reason.
The standard Symfony RoleHierarchyVoter always returns "1", therefore sum of "granted" results equals to sum of "deny" results. So, I need to disable this Voter, because I don't use RoleHierarchy.
1) How can I disable Voter in config?
2) Does it exist another solution for this issue?
Thanks a lot for any help!
UPDATED.
So, I have created own RoleHierarchyVoter which always return false.
This Voter replace standard Voter, but I'm not sure this solution is true way.
Maybe any other solutions?

So, currently I have solved the problem by creating own RoleHierarchyVoter, which always return false.
Currently impossible to remove definition of standard RoleHierarchyVoter, because it's registered with priority TYPE_BEFORE_OPTIMIZATION and performed before my own compiler.
Btw, you can find in SecurityBundle/DependencyInjection/SecurityExtension.php next lines:
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');
}
Even when I set role_hierarchy: ~, isset($config['role_hierarchy'] will return true.
This issue has reported as bug https://github.com/symfony/symfony/issues/16358

Documentation of the RoleVoter says:
RoleVoter votes if any attribute starts with a given prefix.
The default prefix the RoleVoter will check is ROLE_, passed as default parameter value in the constuctor. These are required, because the voter has to check the current logged in user.
Make sure your own voters implements VoterInterface and also check the voter's implementation of YourVoter::supportsClass. The FQN of the element of which you want to know the user has access to should be checked over there. Then the following config should be enough:
app.security.download_voter:
class: AppBundle\Security\Voter\DownloadVoter
public: false
tags:
- { name: security.voter }
So:
1) You should not disable this voter, because all other voters rely on the RoleHierarchy this voter does create for the current user when passing a vote.
2) For better understanding of the Voter you can let the DIC inject the logger in your voter, and add extra info to the profiler. That way your own voters aren't a black box anymore.

Related

Symfony3 Docs WSSE fail "Cannot replace arguments if none have been configured yet"

Following this
http://symfony.com/doc/current/security/custom_authentication_provider.html
Results in
Service "security.authentication.provider.wsse.wsse_secured": Cannot replace arguments if none have been configured yet.
I cannot find anything about this error anywhere. This uses the doc's WSSE code and it fails.
This repo shows it failing https://github.com/jakenoble/wsse_test
I want to get it working eventually with the FOS User Bundle. But I cannot get it to work with a basic Symfony3 install so FOS User Bundle is out of the question at the moment.
Having dug around a bit...
There is a an arg at element index_0 on class Symfony\Component\DependencyInjection\ChildDefinition the object for the arg at element index_0 has an id of fos_user.user_provider.username_email.
The replace call then attempts to get the arguments of fos_user.user_provider.username_email, but there are none. Then the error occurs.
Any ideas?
TL;DR Move your service definitions below the autoloading definitions in services.yml and change the WsseFactory code to
$container
->setDefinition($providerId, new ChildDefinition(WsseProvider::class))
->setArgument('$userProvider', new Reference($userProvider))
;
Full explanation.
The first mistake in the supplied code is that the services definitions prepend the autoloading lines. The autoloading will just override the previous definitions and will cause the AuthenticationManagerInterface failure. Moving the definitions below will fix the issue. The other way to fix the issue is aliases as #yceruto and #gintko pointed.
But only that move will not make the code work despite on your answer. You probably didnt notice how changed something else to make it work.
The second issue, the failure of replaceArgument, is related to Symfony's order of the container compilation as was correctly supposed. The order is defined in the PassConfig class:
$this->optimizationPasses = array(array(
new ExtensionCompilerPass(),
new ResolveDefinitionTemplatesPass(),
...
$autowirePass = new AutowirePass(false),
...
));
I omitted the irrelevant passes. The security.authentication.provider.wsse.wsse_secured definition created by the WsseFactory is produced first. Then ResolveDefinitionTemplatesPass will take place, and will try to replace the arguments of the definition and raise the Cannot replace arguments exception you got:
foreach ($definition->getArguments() as $k => $v) {
if (is_numeric($k)) {
$def->addArgument($v);
} elseif (0 === strpos($k, 'index_')) {
$def->replaceArgument((int) substr($k, strlen('index_')), $v);
} else {
$def->setArgument($k, $v);
}
}
The issue will appear cause the pass will call Definition::replaceArgument for index_0. As the parent definition doesn't have an argument at position 0 neither in the former services.xml nor in the fixed one. AutowirePass wasn't executed yet, the autogenerated definitions has no arguments, the manual definition has the named $cachePool argument only.
So to fix the issue you could use rather:
->setArgument(0, new Reference($userProvider)); //proposed by #yceruto
->replaceArgument('$userProvider', new Reference($userProvider)); //proposed by #gintko
->setArgument('$userProvider', new Reference($userProvider)); // by me
All of them will entail the calls of Definition::addArgument or Definition::setArgument and will work out. There are only the little difference:
- setArgument(0, ... could not work for some other scenarios;
- I like ->setArgument('$userProvider' more than ->replaceArgument('$userProvider' due to semantics. Nothing to replace yet!
Hope the details why the issue appears now clear.
PS. There are also few other funny ways to overcome the issue.
Fix the config a bit:
AppBundle\Security\Authentication\Provider\WsseProvider:
arguments:
0: ''
$cachePool: '#cache.app'
public: false
Or set an alias of Symfony\Component\Security\Core\User\UserProviderInterface to let the autowire do the rest for you.
$container
->setDefinition($providerId, new ChildDefinition(WsseProvider::class))
// ->replaceArgument(0, new Reference($userProvider))
;
$container
->setAlias('Symfony\Component\Security\Core\User\UserProviderInterface',$userProvider)
;
This look like at this moment the provider definition doesn't have the autowired arguments ready, maybe related to the order in which the "CompilerPass" are processed, by now you can solve it with these little tweaks:
change this line in WsseFactory.php:
->replaceArgument(0, new Reference($userProvider))
by:
->setArgument(0, new Reference($userProvider))
and add this alias to services.yml to complete the autowired arguments of the new provider:
Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface: '#security.authentication.manager'
EDIT: (deleted the previous contents due to the comment)
I see you don't use FOSUserBundle that makes things more comples. I did not yet create PR.
I think you are missing custom user entity. (see the link I have provided to create your own entity - my PR would be similar to these lines, if you are unable to create your own entity using my description and the link I'll provide PR, but that will take longer).
The steps you have to take:
Create your own User entity via src/AppBundle/Entity/User.php
Create DB table via php bin/console doctrine:schema:update --force
Configure Security to load your Entity - use AppBundle\Entity\User;
Then you have to create your own user. This can be tricky see password encoding for more.
(optional) Forbid interactive users If you want to login using
username OR email you can create Custom Query to Load the User
These steps should be enough for you to create your own user Entity and you don't have to use the FOSUserBundle.
EDIT:
Ok, so read some source code, and in ChildDefinition:100 you can see, that arguments are also indexed by argument's $name. So, it must be, that at compile time arguments of autowired services are passed with their named indexes, instead of numbered indexes.
WsseFactory.php
->replaceArgument(0, new Reference($userProvider));
so argument should be referenced by it's name:
->replaceArgument('$userProvider', new Reference($userProvider));
After this change, you will get new exception error, which says that it's not possible to autowire $authenticationManager in WsseListener service by it's interface. You can fix it simply by specifying alias for that interface in your services.yml:
Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface: '#security.authentication.manager'
I guess this issue is related with new autowire feature, I will try to investigate it later why it is so.
For now, you can use old way to define services:
services.yml:
app.security.wsse_provider:
class: AppBundle\Security\Authentication\Provider\WsseProvider
arguments:
- ''
- '#cache.app'
public: false
app.security.wsse_listener:
class: AppBundle\Security\Firewall\WsseListener
arguments: ['#security.token_storage', '#security.authentication.manager']
public: false
in WsseFactory.php, following lines:
new ChildDefinition(WsseFactory::class);
new ChildDefinition(WsseListener::class);
translates into:
new ChildDefinition('app.security.wsse_provider');
new ChildDefinition('app.security.wsse_listener');
It seems the issue is because my config in services.yml came before the new auto wiring stuff.
So simply moving it below the auto wiring fixes it.

Symfony isGranted not working on Inherited Roles?

I have a user that has an inherited role of PERM_USER_READ.
when i tried to call $this->isGranted('PERM_USER_READ'); it always returns false. Is it the default behavior of the isGranted() ? If so, what can i do to evaluate inherited roles on my Twig and Controllers?
Thanks!
Try to rename your role to ROLE_PERM_USER_READ
Symfony 4 answer:
I find the ROLE with inherited ROLEs very confusing, so we've adopted a "a ROLE gives you ALLOWS"-system:
ROLE_PRODUCT_MANAGEMENT:
- ALLOW_PRODUCT_EDIT
- ALLOW_ASSORTMENT_READ
We ONLY check on ALLOW_* 'roles', which made everthing 100% less confusing.
We've ran into the same problem as you have. I've fixed that by creating a service which does the following:
// /vendor/symfony/security-core/Role/RoleHierarchyInterface.php
$reachableRoles = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
// Check wether you have the required role, can you see this ENTITY in general?
if (!in_array('ALLOW_PRODUCT_EDIT', $reachableRoles, true)) {
return false;
}
Symfony 5 answer:
Unfortunally: none so far. From the source of RoleHierachyInterface:
* The getReachableRoles(Role[] $roles) method that returns an array of all reachable Role
* objects is deprecated since Symfony 4.3.
We're currently in the process of upgrading to Sym5, we havent arived at this point yet. If anyone has a neat solution for this, that would be great.
The role must start with ROLE_
As said in documentation
Every role must start with ROLE_ (otherwise, things won’t work as expected)
Other than the above rule, a role is just a string and you can invent what you need (e.g. ROLE_PRODUCT_ADMIN).

Is there a way to check controller parameters before the controller method?

My controller code has more lines to validate the received parameters than the real function code and I was wondering if there is a way to do this validations outside the controller method.
I have checked the Symfony docs but I could not found anything. I was thinking in something like a listener but just for that method.
Thank you guys.
EDIT: I'm aware about route requirements. I'm looking for a place to inject my own code.
EDIT: Added a little snippet.
public function searchAddressAction($radius, $page){
if ($radius < 5 || $radius > 50) {
throw $this->createNotFoundException('Radius not valid');
}
if ($page <= 0) {
throw $this->createNotFoundException('Page not found');
}
EDIT 3: This seems to be the way but I can't make it work (thanks to #dlondero):
search_address:
path: /{address}/{radius}/{page}
defaults:
_controller: AppBundle:Default:searchAddress
radius: 20
page: 1
options:
expose: true
requirements:
radius: \d+
page: \d+
condition: 'request.get("radius") == 50 '
Besides the default route requirements, there are 2 options to do some extra checks if a request should be passed through to a Controller.
Create a custom route loader.
http://symfony.com/doc/current/routing/custom_route_loader.html
This can be very useful if you want to get route requirements from the database for example.
Conditions (Might be what you're looking for)
https://symfony.com/doc/current/routing/conditions.html
This is using the expression language https://symfony.com/doc/current/components/expression_language/syntax.html
I don't think you can use the parameters name in here, but you can access the query in the request, which would for sure meet the requirements of the path.
If both of these are not working, than the validation has to be done in the Controller.
You can of course create a class to do the validation, and in your controller you use that class so you only have to call 1 method to do the validation.
I don't recommend a listener for this, as it would be called for all requests, and then has to do a check if it should do the validation.
Besides that it's performance wise not preferable, it doesn't make sense to do this kind of validation in a listener, so if someone else would work on your code, he has to dig in weird places to find out why a controller returns a 404, but still matches the route.
You probably are looking for kernel.request event. Then you could have your listener/subscriber and check what you need to check.

#Security annotation and user parameter

In a controller, I have an action meant to display a user. The argument is the user to be displayed and is automatically fetched through a parameter converter.
Now I want to secure this action (displaying a user profile) so that only users with the USER_VIEW permission (currently implemented as a custom Voter) have access.
Using an #Security annotation it would look like:
/**
* #Security("is_granted('USER_VIEW', user)")
*/
public function showAction(User $user) {...}
This doesn't work because the user variable in the expression refers to the authenticated user rather than the action argument.
The documentation mentions that, but this is rather unfortunate.
How can I fix this issue and get my $user argument passed to the security expression? I could rename it, but perhaps there's something better to do.
I was also wondering if this should be considered a bug in Symfony, since 'user' is a rather common word, perhaps it should be renamed to something more specific such as 'authenticated_user' or 'security_user' or something. The same goes for the other 3 variables that are passed to the expression by Symfony: 'token', 'request', 'roles'.
Thanks.

add custom logic to internal Symfony classes like SwitchUserListener or TemplateGuesser

I got a problem to add custom logic to some Symfony classes.
SwitchUserListener
I want to add a check, that a user cannot switch to a another user, which have more rights/roles, than the initial user.
First attempt
Overwrite the parameter in the security_listeners.xml with the key:
security.authentication.switchuser_listener.class But where can I overwrite it?
In the security.yml it didn't work:
security:
...
authentication:
switchuser_listener:
class: Symfony\Component\Security\Http\Firewall\SwitchUserListener
Second attempt
Overwrite the service for the SwitchUserListner service id: security.authentication.switchuser_listener
I create the same service in my service.xml of my bundle, but my class was not used / called.
Another idea was to overwrite only the class, but that only works for bundles, but the SwitchUserListener was not in the SecurityBundle, it was in the symfony component directory and that seemed to me as a really bad idea to overwrite the SecurityBundle
Third attempt
Now I get the solution: First time I didn't realize that the dispatcher call listener for the SWTICH_USER event in the SwitchUserListener:
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
So I need only to create a service with the special tag for this event type:
<tag name="kernel.event_listener" event="security.switch_user" method="onSecuritySwitchUser" />
And do the check in the given method.
This seems to be a better solution thatn the other two. But there is still a problem. In my listener for the SwitchUserEvent I need to ignore my custom check if the user wants to exit the switched user.
So I need to check the requested path: ignore if path containts '?switch_user=_exit'
But the path (URL parameter) can be changed:
# app/config/security.yml
security:
firewalls:
main:
# ...
switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
But in my bundle I can't read this parameter, because it will not be passed to the service container. It will be passed to the constructor of the SwitchUserListner class and will be saved there as private attribute, never accessable (without Reflection) from outside. (that happens here: SecurityExtension.php line 591) So what to do? Define the parameter twice go against DRY. Use Reflection?
And the other point is that there aren' every time events that will be fired on which I write a subscriber class. So what would be another / best solution for it?
I ask this question because I will get some similar problem where I want to add or overwrite something of the symfony intern components.
TemplateGuesser
I wanted to modify the TemplateGuesser: For a specific bundle all Templates which has the annotation #Tempalte the tempate file should be located with the controller TestController#showAction at this path:
Resources/views/customDir/Test/show.html.twig
So the guesser should be put and locate everything into a additional folder customDir instead of using only views. When using the render function with a specific template, the guesser should ignore the annotation.
I created my own Guesser and overwrite the service id: sensio_framework_extra.view.guesser and in comparision to the SwitchUserListener this time my class is really called instead of the original guesser. Why it works here but not with the SwitchUserListener?
Is this a good solution at all? I also tried to add a second listener, which calls the TemplateGuesser, its the service sensio_framework_extra.view.listener with the class Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener But that didn't work.
Whenever you need to add custom logic or extend the framework behaviour, you can use and abuse the container configuration. That means you can overwrite pretty much every service Symfony defines by just creating a new class that extends that service – or not, really – and creating the service definition for it with the same key as the original service you wanted to extend or change behaviour.
For instance, Symfony has a base template guesser registered as a service with the sensio_framework_extra.view.guesser id. If you want to extend that or change behaviour, you only need to create your own class and register it with the same id of the original service – remember that the bundles loading order affects the service definitons with the same id, where the last one loaded is the one that will be created.
That should solve both of your problems.

Resources