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

Skip to content

Commit e090b85

Browse files
committed
feature #20677 [DX][SecurityBundle] UserPasswordEncoderCommand: ask user class choice question (ogizanagi)
This PR was merged into the 3.3-dev branch. Discussion ---------- [DX][SecurityBundle] UserPasswordEncoderCommand: ask user class choice question | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | N/A | License | MIT | Doc PR | N/A Typing the user class is quite annoying so I'd suggest to ask the user to select it using a choice question. This changes however requires to inject configured encoders' user classes. Making the command a service and providing it in the constructor from the extension is probably the cleanest way, but it deprecates: - registering the command by convention (registered as a service now, so potential commands extending this one should do the same) - relying on the fact the command extends `ContainerAwareCommand` and implements `ContainerAwareInterface` (will not extends/implements it anymore in 4.0 because it's not required anymore) Commits ------- 366aefd [SecurityBundle] UserPasswordEncoderCommand: ask user class choice question
2 parents c3230f0 + 366aefd commit e090b85

File tree

8 files changed

+174
-8
lines changed

8 files changed

+174
-8
lines changed

UPGRADE-3.3.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ SecurityBundle
9696

9797
* The `FirewallMap::$map` and `$container` properties have been deprecated and will be removed in 4.0.
9898

99+
* The `UserPasswordEncoderCommand` command expects to be registered as a service and its
100+
constructor arguments fully provided.
101+
Registering by convention the command or commands extending it is deprecated and will
102+
not be allowed anymore in 4.0.
103+
104+
* `UserPasswordEncoderCommand::getContainer()` is deprecated, and this class won't
105+
extend `ContainerAwareCommand` nor implement `ContainerAwareInterface` anymore in 4.0.
106+
99107
TwigBridge
100108
----------
101109

UPGRADE-4.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,13 @@ Ldap
437437

438438
* The `RenameEntryInterface` has been deprecated, and merged with `EntryManagerInterface`
439439

440+
SecurityBundle
441+
--------------
442+
443+
* The `UserPasswordEncoderCommand` class does not allow `null` as the first argument anymore.
444+
445+
* `UserPasswordEncoderCommand` does not implement `ContainerAwareInterface` anymore.
446+
440447
Workflow
441448
--------
442449

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* Deprecated instantiating `UserPasswordEncoderCommand` without its constructor
8+
arguments fully provided.
9+
* Deprecated `UserPasswordEncoderCommand::getContainer()` and relying on the
10+
`ContainerAwareInterface` interface for this command.
711
* Deprecated the `FirewallMap::$map` and `$container` properties.
812

913
3.2.0

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

Lines changed: 61 additions & 5 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,6 +30,31 @@
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 the 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();
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
*/
@@ -36,7 +64,7 @@ protected function configure()
3664
->setName('security:encode-password')
3765
->setDescription('Encodes a password.')
3866
->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')
67+
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.')
4068
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.')
4169
->setHelp(<<<EOF
4270
@@ -55,8 +83,9 @@ protected function configure()
5583
AppBundle\Entity\User: bcrypt
5684
</comment>
5785
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:
86+
If you execute the command non-interactively, the first available configured
87+
user class under the <comment>security.encoders</comment> key is used and a random salt is
88+
generated to encode the password:
6089
6190
<info>php %command.full_name% --no-interaction [password]</info>
6291
@@ -89,10 +118,11 @@ protected function execute(InputInterface $input, OutputInterface $output)
89118
$input->isInteractive() ? $io->title('Symfony Password Encoder Utility') : $io->newLine();
90119

91120
$password = $input->getArgument('password');
92-
$userClass = $input->getArgument('user-class');
121+
$userClass = $this->getUserClass($input, $io);
93122
$emptySalt = $input->getOption('empty-salt');
94123

95-
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
124+
$encoderFactory = $this->encoderFactory ?: parent::getContainer()->get('security.encoder_factory');
125+
$encoder = $encoderFactory->getEncoder($userClass);
96126
$bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder;
97127

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

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\Alias;
1819
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1920
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@@ -96,6 +97,11 @@ public function load(array $configs, ContainerBuilder $container)
9697

9798
if ($config['encoders']) {
9899
$this->createEncoders($config['encoders'], $container);
100+
101+
if (class_exists(Application::class)) {
102+
$loader->load('console.xml');
103+
$container->getDefinition('security.console.user_password_encoder_command')->replaceArgument(1, array_keys($config['encoders']));
104+
}
99105
}
100106

101107
// 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? [Custom\Class\Bcrypt\User]:
155+
[0] Custom\Class\Bcrypt\User
156+
[1] Custom\Class\Pbkdf2\User
157+
[2] Custom\Class\Test\User
158+
[3] Symfony\Component\Security\Core\User\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->createMock(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 the 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
@@ -25,7 +25,7 @@
2525
"require-dev": {
2626
"symfony/asset": "~2.8|~3.0",
2727
"symfony/browser-kit": "~2.8|~3.0",
28-
"symfony/console": "~2.8|~3.0",
28+
"symfony/console": "~3.2",
2929
"symfony/css-selector": "~2.8|~3.0",
3030
"symfony/dom-crawler": "~2.8|~3.0",
3131
"symfony/form": "~2.8|~3.0",

0 commit comments

Comments
 (0)