From d9f4308c26d7c70d86dc24e15206d8036d4d5c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFck=20Piera?= Date: Sun, 9 Feb 2020 15:18:46 +0100 Subject: [PATCH 1/4] Add SecurityPasswordType --- .../Bundle/SecurityBundle/CHANGELOG.md | 1 + .../DependencyInjection/SecurityExtension.php | 5 ++ .../Form/SecurityPasswordType.php | 77 +++++++++++++++++++ .../SecurityBundle/Resources/config/form.xml | 16 ++++ 4 files changed, 99 insertions(+) create mode 100644 src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index de5208aa1a412..30aa845bb2929 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Added security configuration for priority-based access decision strategy + * Added `SecurityPasswordType` 5.0.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index baf395b4c4a94..9058280a02ec0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Form\AbstractType; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; @@ -149,6 +150,10 @@ public function load(array $configs, ContainerBuilder $container) $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders'])); } + if (class_exists(AbstractType::class)) { + $loader->load('form.xml'); + } + if (!class_exists(UserValueResolver::class)) { $container->getDefinition('security.user_value_resolver')->setClass(SecurityUserValueResolver::class); } diff --git a/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php b/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php new file mode 100644 index 0000000000000..f1d0dd539e73d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\SubmitEvent; +use Symfony\Component\Form\Exception\InvalidConfigurationException; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * @author Loïck Piera + */ +class SecurityPasswordType extends AbstractType +{ + private $passwordEncoder; + + public function __construct(UserPasswordEncoderInterface $passwordEncoder) + { + $this->passwordEncoder = $passwordEncoder; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventListener(FormEvents::SUBMIT, function (SubmitEvent $event) { + $securityUser = $event->getForm()->getConfig()->getOption('security_user'); + + if (!$securityUser) { + $parentData = $event->getForm()->getParent()->getData(); + + if (!$parentData instanceof UserInterface) { + throw new InvalidConfigurationException(sprintf('You should either use "%s" inside a parent form where data is an instance of "%s" or specify the user in "security_user" option', self::class, UserInterface::class)); + } + + $securityUser = $parentData; + } + + $plainPassword = $event->getData(); + + $event->setData($plainPassword ? $this->passwordEncoder->encodePassword($securityUser, $plainPassword) : $securityUser->getPassword()); + }); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['required'] = false; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('security_user', null); + $resolver->setAllowedTypes('security_user', [ + 'null', + UserInterface::class, + ]); + } + + public function getParent() + { + return PasswordType::class; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml new file mode 100644 index 0000000000000..a9c7751288f77 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + From 8910840b08af8b53938acbdd1d794256cfd07e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFck=20Piera?= Date: Sun, 9 Feb 2020 17:21:38 +0100 Subject: [PATCH 2/4] Remove required option override --- .../Bundle/SecurityBundle/Form/SecurityPasswordType.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php b/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php index f1d0dd539e73d..5d976482cbf05 100644 --- a/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php +++ b/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php @@ -56,11 +56,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) }); } - public function buildView(FormView $view, FormInterface $form, array $options) - { - $view->vars['required'] = false; - } - public function configureOptions(OptionsResolver $resolver) { $resolver->setDefault('security_user', null); From 1782e1459cd0200493ce06ba2aaff5b9795e98a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFck=20Piera?= Date: Sun, 16 Feb 2020 21:24:49 +0100 Subject: [PATCH 3/4] Move to Form component --- .../FrameworkBundle/Resources/config/form.xml | 10 +++++ .../Bundle/SecurityBundle/CHANGELOG.md | 1 - .../DependencyInjection/SecurityExtension.php | 5 --- .../SecurityBundle/Resources/config/form.xml | 16 -------- src/Symfony/Component/Form/CHANGELOG.md | 1 + .../Extension/Security/SecurityExtension.php | 41 +++++++++++++++++++ .../Security/Type}/SecurityPasswordType.php | 4 +- 7 files changed, 53 insertions(+), 25 deletions(-) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml create mode 100644 src/Symfony/Component/Form/Extension/Security/SecurityExtension.php rename src/Symfony/{Bundle/SecurityBundle/Form => Component/Form/Extension/Security/Type}/SecurityPasswordType.php (94%) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 17598fa95815c..d05214758c092 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -111,5 +111,15 @@ %validator.translation_domain% + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 30aa845bb2929..de5208aa1a412 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,7 +5,6 @@ CHANGELOG ----- * Added security configuration for priority-based access decision strategy - * Added `SecurityPasswordType` 5.0.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 9058280a02ec0..baf395b4c4a94 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -26,7 +26,6 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Form\AbstractType; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; @@ -150,10 +149,6 @@ public function load(array $configs, ContainerBuilder $container) $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders'])); } - if (class_exists(AbstractType::class)) { - $loader->load('form.xml'); - } - if (!class_exists(UserValueResolver::class)) { $container->getDefinition('security.user_value_resolver')->setClass(SecurityUserValueResolver::class); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml deleted file mode 100644 index a9c7751288f77..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/form.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 24935f0449025..5db9837f7e126 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG is deprecated. The method will be added to the interface in 6.0. * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method is deprecated. The method will be added to the interface in 6.0. + * Added `SecurityExtension` and `SecurityPasswordType` 5.0.0 ----- diff --git a/src/Symfony/Component/Form/Extension/Security/SecurityExtension.php b/src/Symfony/Component/Form/Extension/Security/SecurityExtension.php new file mode 100644 index 0000000000000..842896a03fe51 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Security/SecurityExtension.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Security; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + +/** + * Integrates the Security (Core) component with the Form library. + * + * @author Loïck Piera + */ +class SecurityExtension extends AbstractExtension +{ + private $passwordEncoder; + + public function __construct(UserPasswordEncoderInterface $passwordEncoder = null) + { + $this->passwordEncoder = $passwordEncoder; + } + + protected function loadTypes() + { + if (!$this->passwordEncoder) { + return []; + } + + return [ + new Type\SecurityPasswordType($this->passwordEncoder), + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php b/src/Symfony/Component/Form/Extension/Security/Type/SecurityPasswordType.php similarity index 94% rename from src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php rename to src/Symfony/Component/Form/Extension/Security/Type/SecurityPasswordType.php index 5d976482cbf05..6cc373c6fa2c3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Form/SecurityPasswordType.php +++ b/src/Symfony/Component/Form/Extension/Security/Type/SecurityPasswordType.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\SecurityBundle\Form; +namespace Symfony\Component\Form\Extension\Security\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Event\SubmitEvent; @@ -17,8 +17,6 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Core\User\UserInterface; From 16bc5b1472811676203099924a13fdf6dafd38f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFck=20Piera?= Date: Sun, 16 Feb 2020 23:36:41 +0100 Subject: [PATCH 4/4] Add tests --- .../Security/SecurityExtensionTest.php | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/Symfony/Component/Form/Tests/Extension/Security/SecurityExtensionTest.php diff --git a/src/Symfony/Component/Form/Tests/Extension/Security/SecurityExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Security/SecurityExtensionTest.php new file mode 100644 index 0000000000000..fb426aa51d9ea --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Security/SecurityExtensionTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Security; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Security\SecurityExtension; +use Symfony\Component\Form\Extension\Security\Type\SecurityPasswordType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryBuilder; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\User\User; + +class SecurityExtensionTest extends TestCase +{ + public function testSecurityPasswordTypeWorks() + { + $user = new User('email@example.com', 'previous_password'); + + $this->assertSame('previous_password', $user->getPassword()); + + $form = $this->getFormFactory() + ->createBuilder(FormType::class, $user) + ->add('password', SecurityPasswordType::class, [ + 'security_user' => $user, + ]) + ->getForm() + ; + + $form->submit(['password' => 'new_password']); + + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isValid()); + $this->assertSame('encoded_password', $user->getPassword()); + } + + public function testSecurityPasswordTypeDetectsUserObject() + { + $user = new User('email@example.com', 'previous_password'); + + $this->assertSame('previous_password', $user->getPassword()); + + $form = $this->getFormFactory()->create(PasswordResetType::class, $user); + $form->submit(['password' => 'new_password']); + + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isValid()); + $this->assertSame('encoded_password', $user->getPassword()); + } + + public function testSecurityPasswordTypeDoesNotUpdatePasswordWhenEmpty() + { + $user = new User('email@example.com', 'previous_password'); + + $this->assertSame('previous_password', $user->getPassword()); + + $form = $this->getFormFactory()->create(PasswordResetType::class, $user); + $form->submit([]); + + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isValid()); + $this->assertSame('previous_password', $user->getPassword()); + } + + private function getFormFactory() + { + $userPasswordEncoder = $this->getMockBuilder('Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface')->getMock(); + + $userPasswordEncoder + ->method('encodePassword') + ->willReturn('encoded_password'); + + $formFactoryBuilder = new FormFactoryBuilder(); + $formFactoryBuilder->addExtension(new SecurityExtension($userPasswordEncoder)); + + return $formFactoryBuilder->getFormFactory(); + } +} + +class PasswordResetType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('password', SecurityPasswordType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', User::class); + } +}