symfony2 override the service security.authentication.manager - symfony

I would like to add some code inside the function authenticate() of the Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager.
I tried to create a child bundle of the security bundle.
And i redefined the service for security.authentication.manager in this bundle like that
<!-- Authentication related services -->
<service id="security.authentication.manager" class="%security.authentication.manager.class%" public="false">
<argument type="collection" />
</service>
But when i relad the page, the framework throw an exception: InvalidArgumentException: You must at least add one authentication provider.
I suppose it's becaue the dependecies are created inside the parent bundle configuation.
What i must do to get it work without redefine the whole security bundle ?
Thank you.

I suppose that better would be created your own handlers.
You need to create service
Register it into service container.
Simply set handlers in your security.yml:
form_login:
success_handler: success_login_handler
failure_handler: failure_login_handler
logout:
success_handler: success_logout_handler

Ok, I found that the best way is to create my own a factory, that extends the formFactory of symfony2
Then i must create my own AuthentificationProvider that extend the DaoAuthenticationProvider and declare it as abstract service.
Then create the service inside the factory via the method createAuthProvider() and replace the parameters we need.
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'acme.authentication.provider.dao.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('acme.authentication.provider.dao'))
->replaceArgument(0, new Reference($userProviderId))//replace args on the service constructor
->replaceArgument(2, $id)
->addArgument(new Reference('acme.api'))//add some args to the service constructor
;
return $provider;
}

Related

Symfony access decision manager strategy is always affirmative

I try to change the access control decision strategy in a Symfony project but it doesn't seem to work.
I have the following in my security.yaml:
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
access_control:
- { path: ^/dev/test, roles: [COMPLEX_TEST, ROLE_OTHER] }
I need both COMPLEX_TEST and ROLE_OTHER to be granted for the route to be accessible (COMPLEX_TEST is tested by a custom role voter).
But when I try to access the route, only the COMPLEXT_TEST voter is called, and that's because it allows the access and the strategy is still affirmative.
I can say this because when debugging the code, I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is always affirmative, no matter what I set in the security.yaml.
For now I've created a compiler pass that forces the strategy to unanimous, but it's kind of a hack:
class FixAccessDecisionManagerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->hasDefinition('security.access.decision_manager')) {
$definition = $container->getDefinition('security.access.decision_manager');
$definition->setArgument('$strategy', AccessDecisionManager::STRATEGY_UNANIMOUS);
}
}
}
With the compiler pass I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is correcly set to unanimous and the access control works as expected.
Do you see what I am missing? Or is it a bug in Symfony? My Symfony version is 4.4.7.
Thank you for your time.

How does Symfony4 autowire arguments to UserPasswordEncoderCommand constructor?

This is a question about Symfony 4 autowiring
using only 'array' as a constructor argument type-hint. I give a specific case, but this may be helpful to others because this situation happens in several Symfony bundles.
This Symfony command asks for a password and encodes it:
php bin/console security:encode-password
The code for this command is in vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php
In Symfony 4.2.3, this is the constructor for UserPasswordEncoderCommand:
class UserPasswordEncoderCommand extends Command
{
protected static $defaultName = 'security:encode-password';
private $encoderFactory;
private $userClasses;
public function __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = [])
{
$this->encoderFactory = $encoderFactory;
$this->userClasses = $userClasses;
parent::__construct();
}
Symfony uses dependency injection to call the constructor, passing in automatically determined arguments. The above constructor first argument $encoderFactory is autowired using the type-hint EncoderFactoryInterface.
My question is: How is the second argument $userClasses autowired?
How does Symfony know what the array should contain? Debug print statements show the array contains a single value "App\Entity\User".
Here is my config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: argon2i
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
logout:
path: app_logout
# Where to redirect after logout
target: app_login
I think the following three files are important to answering this question.
vendor/symfony/security-bundle/Resources/config/console.xml contains:
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="false" />
<service id="security.command.user_password_encoder" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand">
<argument type="service" id="security.encoder_factory"/>
<argument type="collection" /> <!-- encoders' user classes -->
<tag name="console.command" command="security:encode-password" />
</service>
</services>
</container>
Part of vendor/symfony/security-bundle/Resources/config/security.xml contains:
<service id="security.encoder_factory" alias="security.encoder_factory.generic" />
<service id="Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface" alias="security.encoder_factory" />
<service id="security.user_password_encoder.generic" class="Symfony\Component\Security\Core\Encoder\UserPasswordEncoder">
<argument type="service" id="security.encoder_factory"></argument>
</service>
<service id="security.password_encoder" alias="security.user_password_encoder.generic" public="true" />
<service id="Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface" alias="security.password_encoder" />
Part of vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php contains:
function load(array $configs, ContainerBuilder $container)
...
if (class_exists(Application::class)) {
$loader->load('console.xml');
$container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
}
I ask this question because I would like to use UserPasswordEncoderCommand.php as a starting point for a command to create an admin user in my database. This will give the first admin user permission to login using the web browser to
create other users.
I tried copying vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php to src/Command/AddUserCommand.php and changing these lines:
< namespace Symfony\Bundle\SecurityBundle\Command;
---
> namespace App\Command;
< class UserPasswordEncoderCommand extends Command
---
> class AddUserCommand extends Command
< protected static $defaultName = 'security:encode-password';
---
> protected static $defaultName = 'app:add-user';
When I ran this command: php bin/console app:add-user
this error appeared:
There are no configured encoders for the "security" extension.
This happened because the constructor __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = [])
was called with only one argument, so the second argument defaulted to an empty array.
It seems these files also need to be copied from vendor/symfony/security-bundle to somewhere under src/
DependencyInjection/SecurityExtension.php
Resources/config/console.xml
and be somehow modified. Which other files need to be copied to src/ ? The Symfony documentation under https://symfony.com is excellent, but I can not find where this situation is described.
Note that this command works:
php bin/console app:add-user mypassword "App\Entity\User"
but I do not want the user to have to type this. And this may change if security.yaml changes.
In this particular case the data is being assembled by the SecurityBundle as part of the configuration process. When the kernel is booted all bundles are collected and their respective Extensions, usually inside the DependencyInjection subfolder, will be registered. When the container is built it is passed through all these extension so they can modify the services or add their own. This is how Symfony collects the configs from inside the bundle, when the main application itself does not know where they are stored, but the bundle does.
Getting the user classes is done by Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension when processing the encoders-section of the security.yml. If you look at the encoders section of your security.yaml you notice that each Encoder is assigned to a class name. The method will then use this array to create the appropriate encoder for the value and keep the class name as key. This array is passed to the encoder factory in createEncoders. Afterwards it will take the same array from the configuration, get the names of each user class using array_keys() and replace the argument with offset 1, i.e. the $userClasses, for the command.
This concept is called semantic configuration and inside your own application this would probably be overkill as you have to define the Configuration and then write the logic by hand. Instead you would probably just define an array with the class names in the parameters-section of your services.yaml.
If you want to pass in other services, which can come from other bundles your bundle does not know about, there is another approach called CompilerPasses. In a CompilerPass you can collect all classes, e.g. with a specific tag, and then pass their references to a method or the constructor of your service. This is for example used inside Symfony to register EventListeners or make Commands available in the Console application.

Disable object identity check in symfony ACL

I use voters that I set up based on this guide (I know it's a Sonata guide but it uses no Sonata code).
Now the voters are working fine, they grant deny as needed. One voter service definition looks like this:
services:
acme_account.security.authorization.organisation_voter:
class: %acme_account.security.authorization.organisation_voter.class%
arguments: [#service_container]
public: false
tags:
- { name: security.voter }
Now my problem is that even though the voter returns correct grants, in some cases some default ACL handler denies permission. This is in the logs:
security.DEBUG: No ACL found for the object identity. Voting to deny access. [] []
Since I want to enforce the denies coming from the voters I have set the security.access_decision_manager.strategy to unanimous. But because of the default handler this way the permissions are denied.
Now of course I could configure and start using the ACLs but it would be an overkill in this application that's why I choose the voters.
Is there any way to disable this default behaviour?
Here's a workaround for it, not sure if this is the best way but it works.
The object and security identity retrieval strategy services needed to be overwritten with noop implementations.
services.yml
security.acl.object_identity_retrieval_strategy:
class: Acme\UserBundle\Acl\ObjectIdentityRetrievalStrategy
security.acl.security_identity_retrieval_strategy:
class: Acme\UserBundle\Acl\SecurityIdentityRetrievalStrategy
Acme\UserBundle\Acl\ObjectIdentityRetrievalStrategy.php
<?php
namespace Acme\UserBundle\Acl;
use Symfony\Component\Security\Acl\Model\ObjectIdentityRetrievalStrategyInterface;
class ObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategyInterface
{
public function getObjectIdentity($domainObject)
{
}
}
Acme\UserBundle\Acl\SecurityIdentityRetrievalStrategy.php
<?php
namespace Acme\UserBundle\Acl;
use Symfony\Component\Security\Acl\Model\SecurityIdentityRetrievalStrategyInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class SecurityIdentityRetrievalStrategy implements SecurityIdentityRetrievalStrategyInterface
{
public function getSecurityIdentities(TokenInterface $token)
{
}
}

How to inject additional service to the ExceptionListener?

I want to override the setTargetPath from the default ExceptionListener (documentation). But I need additional service to handle this.
In my opinion there is only the way to override the service definition, copy it to my service definitions and create an own constructor, but I don't like this approach.
Is there any other way to do this?
As for answer, if you are using form_login type, you can set it to a constant route where the redirect after login should happen. Config
You should set these 2 keys:
always_use_default_target_path: true
default_target_path: /route_name_for_redirect
Or option B, you use a success handler service, where you simply returns a RedirectResponse
I have found another approach using a compiler pass, following steps are necessary:
Create your own class extending ExceptionListener and add for your dependencies a method
Create a compiler pass containing something like the following:
class WebCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.exception_listener');
$definition->addMethodCall('yourmethod', [ new Reference('your dependency') ]);
}
}
Register your compiler pass
Overwrite the exception listener class parameter:
security.exception_listener.class: AcmeBundle\EventListener\SecurityExceptionListener

Symfony2 - listeners on doctrine / orm

What is the best way to have an event that fires after a record is inserted in Symfony2 / Doctrine?
First, register a service as a Doctrine event listener:
app/config.yml:
services:
foo.listener:
class: Vendor\FooBundle\BarClass
tags:
- { name: doctrine.event_listener, event: postPersist, method: onPostPersist }
Then in your listener class, define an onPostPersist method (or whatever you named the method in the config) that takes a Doctrine\ORM\Event\LifecycleEventArgs argument:
public function onPostPersist(LifecycleEventArgs $eventArgs)
{
// do stuff with the entity here
}
Note that you can't pass an instance of EntityManager to the listener class, because $eventArgs contains a reference to it, and doing so will throw a CircularReferenceException.
Doctrine Project documentation here. Symfony Project documentation here (out of date, but included for reference)/
Try injecting the container itself instead of the security context. with FOS_USER, security.context depends on your listener (EM) and your listener requires security.context.
<service id="foo.listener" class="%foo.listener.class%">
<argument type="service" id="service_container"/>
<tag name="doctrine.event_listener" event="postPersist" method="fooMethod" />
</service>
By the way, at least in XML, the method name does not seem to be working, by default it call the method 'postPersist' instead and ignore whatever method name you give (fooMethod); Please let me know if that's the case with YAML config, too, or I am just wrong.

Resources