Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit da015f5

Browse files
committed
[SecurityBundle] UserPasswordEncoderCommand: ask user class choice question
1 parent 635d77b commit da015f5

File tree

7 files changed

+166
-9
lines changed

7 files changed

+166
-9
lines changed

UPGRADE-3.3.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ SecurityBundle
1818

1919
* The `FirewallContext::getContext()` method has been deprecated and will be removed in 4.0.
2020
Use the `getListeners()` method instead.
21+
22+
* The `UserPasswordEncoderCommand` command expects to be registered as a service and its
23+
constructor arguments fully provided.
24+
Registering by convention the command or commands extending it is deprecated and will
25+
not be allowed anymore in 4.0.
26+
27+
* `UserPasswordEncoderCommand::getContainer()` is deprecated, and this class won't
28+
extend `ContainerAwareCommand` nor implement `ContainerAwareInterface` anymore in 4.0.

UPGRADE-4.0.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,11 @@ Yaml
330330
the `!php/object` tag.
331331

332332
* Duplicate mapping keys lead to a `ParseException`.
333+
334+
SecurityBundle
335+
--------------
336+
337+
* The `UserPasswordEncoderCommand` class does not allow `null` as first argument anymore.
338+
339+
* `UserPasswordEncoderCommand` does not extend `ContainerAwareCommand` nor implement
340+
`ContainerAwareInterface` anymore.

src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
use Symfony\Component\Console\Output\OutputInterface;
1919
use Symfony\Component\Console\Question\Question;
2020
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
2122
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
23+
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
24+
use Symfony\Component\Security\Core\User\User;
2225

2326
/**
2427
* Encode a user's password.
@@ -27,16 +30,40 @@
2730
*/
2831
class UserPasswordEncoderCommand extends ContainerAwareCommand
2932
{
33+
private $encoderFactory;
34+
private $userClasses;
35+
36+
public function __construct(EncoderFactoryInterface $encoderFactory = null, array $userClasses = array())
37+
{
38+
if (null === $encoderFactory) {
39+
@trigger_error(sprintf('Passing null as first argument of "%s" is deprecated since version 3.3 and will be removed in 4.0. If the command was registered by convention, make it a service instead.', __METHOD__), E_USER_DEPRECATED);
40+
}
41+
42+
$this->encoderFactory = $encoderFactory;
43+
$this->userClasses = $userClasses;
44+
45+
parent::__construct('security:encode-password');
46+
}
47+
48+
/**
49+
* {@inheritdoc}
50+
*/
51+
protected function getContainer()
52+
{
53+
@trigger_error(sprintf('Method "%s" is deprecated since version 3.3 and "%s" won\'t implement "%s" anymore in 4.0.', __METHOD__, __CLASS__, ContainerAwareInterface::class), E_USER_DEPRECATED);
54+
55+
return parent::getContainer();
56+
}
57+
3058
/**
3159
* {@inheritdoc}
3260
*/
3361
protected function configure()
3462
{
3563
$this
36-
->setName('security:encode-password')
3764
->setDescription('Encodes a password.')
3865
->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.')
39-
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.', 'Symfony\Component\Security\Core\User\User')
66+
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.')
4067
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.')
4168
->setHelp(<<<EOF
4269
@@ -55,8 +82,8 @@ protected function configure()
5582
AppBundle\Entity\User: bcrypt
5683
</comment>
5784
58-
If you execute the command non-interactively, the default Symfony User class
59-
is used and a random salt is generated to encode the password:
85+
If you execute the command non-interactively, the first available configured user class under
86+
the security.encoders key is used and a random salt is generated to encode the password:
6087
6188
<info>php %command.full_name% --no-interaction [password]</info>
6289
@@ -89,10 +116,11 @@ protected function execute(InputInterface $input, OutputInterface $output)
89116
$input->isInteractive() ? $io->title('Symfony Password Encoder Utility') : $io->newLine();
90117

91118
$password = $input->getArgument('password');
92-
$userClass = $input->getArgument('user-class');
119+
$userClass = $this->getUserClass($input, $io);
93120
$emptySalt = $input->getOption('empty-salt');
94121

95-
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
122+
$encoderFactory = $this->encoderFactory ?: parent::getContainer()->get('security.encoder_factory');
123+
$encoder = $encoderFactory->getEncoder($userClass);
96124
$bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder;
97125

98126
if ($bcryptWithoutEmptySalt) {
@@ -166,4 +194,26 @@ private function generateSalt()
166194
{
167195
return base64_encode(random_bytes(30));
168196
}
197+
198+
private function getUserClass(InputInterface $input, SymfonyStyle $io)
199+
{
200+
if (null !== $userClass = $input->getArgument('user-class')) {
201+
return $userClass;
202+
}
203+
204+
if (empty($this->userClasses)) {
205+
if (null === $this->encoderFactory) {
206+
// BC to be removed and simply keep the exception whenever there is no configured user classes in 4.0
207+
return User::class;
208+
}
209+
210+
throw new \RuntimeException('There are no configured encoders for the "security" extension.');
211+
}
212+
213+
if (!$input->isInteractive() || 1 === count($this->userClasses)) {
214+
return reset($this->userClasses);
215+
}
216+
217+
return $io->choice('For which user class would you like to encode a password?', $this->userClasses, reset($this->userClasses));
218+
}
169219
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
1616
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17+
use Symfony\Component\Console\Application;
1718
use Symfony\Component\DependencyInjection\DefinitionDecorator;
1819
use Symfony\Component\DependencyInjection\Alias;
1920
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -94,6 +95,11 @@ public function load(array $configs, ContainerBuilder $container)
9495

9596
if ($config['encoders']) {
9697
$this->createEncoders($config['encoders'], $container);
98+
99+
if (class_exists(Application::class)) {
100+
$loader->load('console.xml');
101+
$container->getDefinition('security.console.user_password_encoder_command')->replaceArgument(1, array_keys($config['encoders']));
102+
}
97103
}
98104

99105
// load ACL
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="security.console.user_password_encoder_command" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand" public="false">
9+
<argument type="service" id="security.encoder_factory"/>
10+
<argument type="collection" /> <!-- encoders' user classes -->
11+
<tag name="console.command" />
12+
</service>
13+
</services>
14+
</container>

src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Console\Application;
1515
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
16+
use Symfony\Component\Console\Application as ConsoleApplication;
1617
use Symfony\Component\Console\Tester\CommandTester;
1718
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
19+
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
1820
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
1921

2022
/**
@@ -24,6 +26,7 @@
2426
*/
2527
class UserPasswordEncoderCommandTest extends WebTestCase
2628
{
29+
/** @var CommandTester */
2730
private $passwordEncoderCommandTester;
2831

2932
public function testEncodePasswordEmptySalt()
@@ -105,6 +108,7 @@ public function testEncodePasswordEmptySaltOutput()
105108
array(
106109
'command' => 'security:encode-password',
107110
'password' => 'p@ssw0rd',
111+
'user-class' => 'Symfony\Component\Security\Core\User\User',
108112
'--empty-salt' => true,
109113
)
110114
);
@@ -138,6 +142,74 @@ public function testEncodePasswordNoConfigForGivenUserClass()
138142
), array('interactive' => false));
139143
}
140144

145+
public function testEncodePasswordAsksNonProvidedUserClass()
146+
{
147+
$this->passwordEncoderCommandTester->setInputs(array('Custom\Class\Pbkdf2\User', "\n"));
148+
$this->passwordEncoderCommandTester->execute(array(
149+
'command' => 'security:encode-password',
150+
'password' => 'password',
151+
), array('decorated' => false));
152+
153+
$this->assertContains(<<<EOTXT
154+
For which user class would you like to encode a password? [Symfony\Component\Security\Core\User\User]:
155+
[0] Symfony\Component\Security\Core\User\User
156+
[1] Custom\Class\Bcrypt\User
157+
[2] Custom\Class\Pbkdf2\User
158+
[3] Custom\Class\Test\User
159+
EOTXT
160+
, $this->passwordEncoderCommandTester->getDisplay(true));
161+
}
162+
163+
public function testNonInteractiveEncodePasswordUsesFirstUserClass()
164+
{
165+
$this->passwordEncoderCommandTester->execute(array(
166+
'command' => 'security:encode-password',
167+
'password' => 'password',
168+
), array('interactive' => false));
169+
170+
$this->assertContains('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $this->passwordEncoderCommandTester->getDisplay());
171+
}
172+
173+
/**
174+
* @expectedException \RuntimeException
175+
* @expectedExceptionMessage There are no configured encoders for the "security" extension.
176+
*/
177+
public function testThrowsExceptionOnNoConfiguredEncoders()
178+
{
179+
$application = new ConsoleApplication();
180+
$application->add(new UserPasswordEncoderCommand($this->getMock(EncoderFactoryInterface::class), array()));
181+
182+
$passwordEncoderCommand = $application->find('security:encode-password');
183+
184+
$tester = new CommandTester($passwordEncoderCommand);
185+
$tester->execute(array(
186+
'command' => 'security:encode-password',
187+
'password' => 'password',
188+
), array('interactive' => false));
189+
}
190+
191+
/**
192+
* @group legacy
193+
* @expectedDeprecation Passing null as first argument of "Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand::__construct" is deprecated since version 3.3 and will be removed in 4.0. If the command was registered by convention, make it a service instead.
194+
*/
195+
public function testLegacy()
196+
{
197+
$application = new ConsoleApplication();
198+
$application->add(new UserPasswordEncoderCommand());
199+
200+
$passwordEncoderCommand = $application->find('security:encode-password');
201+
self::bootKernel(array('test_case' => 'PasswordEncode'));
202+
$passwordEncoderCommand->setContainer(self::$kernel->getContainer());
203+
204+
$tester = new CommandTester($passwordEncoderCommand);
205+
$tester->execute(array(
206+
'command' => 'security:encode-password',
207+
'password' => 'password',
208+
), array('interactive' => false));
209+
210+
$this->assertContains('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $tester->getDisplay());
211+
}
212+
141213
protected function setUp()
142214
{
143215
putenv('COLUMNS='.(119 + strlen(PHP_EOL)));
@@ -146,8 +218,7 @@ protected function setUp()
146218

147219
$application = new Application($kernel);
148220

149-
$application->add(new UserPasswordEncoderCommand());
150-
$passwordEncoderCommand = $application->find('security:encode-password');
221+
$passwordEncoderCommand = $application->get('security:encode-password');
151222

152223
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
153224
}

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"require-dev": {
2525
"symfony/asset": "~2.8|~3.0",
2626
"symfony/browser-kit": "~2.8|~3.0",
27-
"symfony/console": "~2.8|~3.0",
27+
"symfony/console": "~3.2",
2828
"symfony/css-selector": "~2.8|~3.0",
2929
"symfony/dom-crawler": "~2.8|~3.0",
3030
"symfony/form": "~2.8|~3.0",

0 commit comments

Comments
 (0)