[SecurityBundle] UserPasswordEncoderCommand: ask user class choice question

This commit is contained in:
Maxime Steinhausser 2016-11-28 22:24:53 +01:00 committed by Maxime Steinhausser
parent c3230f0a97
commit 366aefd75f
8 changed files with 174 additions and 8 deletions

View File

@ -96,6 +96,14 @@ SecurityBundle
* The `FirewallMap::$map` and `$container` properties have been deprecated and will be removed in 4.0.
* The `UserPasswordEncoderCommand` command expects to be registered as a service and its
constructor arguments fully provided.
Registering by convention the command or commands extending it is deprecated and will
not be allowed anymore in 4.0.
* `UserPasswordEncoderCommand::getContainer()` is deprecated, and this class won't
extend `ContainerAwareCommand` nor implement `ContainerAwareInterface` anymore in 4.0.
TwigBridge
----------

View File

@ -437,6 +437,13 @@ Ldap
* The `RenameEntryInterface` has been deprecated, and merged with `EntryManagerInterface`
SecurityBundle
--------------
* The `UserPasswordEncoderCommand` class does not allow `null` as the first argument anymore.
* `UserPasswordEncoderCommand` does not implement `ContainerAwareInterface` anymore.
Workflow
--------

View File

@ -4,6 +4,10 @@ CHANGELOG
3.3.0
-----
* Deprecated instantiating `UserPasswordEncoderCommand` without its constructor
arguments fully provided.
* Deprecated `UserPasswordEncoderCommand::getContainer()` and relying on the
`ContainerAwareInterface` interface for this command.
* Deprecated the `FirewallMap::$map` and `$container` properties.
3.2.0

View File

@ -18,7 +18,10 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\User;
/**
* Encode a user's password.
@ -27,6 +30,31 @@ use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
*/
class UserPasswordEncoderCommand extends ContainerAwareCommand
{
private $encoderFactory;
private $userClasses;
public function __construct(EncoderFactoryInterface $encoderFactory = null, array $userClasses = array())
{
if (null === $encoderFactory) {
@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);
}
$this->encoderFactory = $encoderFactory;
$this->userClasses = $userClasses;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function getContainer()
{
@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);
return parent::getContainer();
}
/**
* {@inheritdoc}
*/
@ -36,7 +64,7 @@ class UserPasswordEncoderCommand extends ContainerAwareCommand
->setName('security:encode-password')
->setDescription('Encodes a password.')
->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.')
->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')
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.')
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.')
->setHelp(<<<EOF
@ -55,8 +83,9 @@ security:
AppBundle\Entity\User: bcrypt
</comment>
If you execute the command non-interactively, the default Symfony User class
is used and a random salt is generated to encode the password:
If you execute the command non-interactively, the first available configured
user class under the <comment>security.encoders</comment> key is used and a random salt is
generated to encode the password:
<info>php %command.full_name% --no-interaction [password]</info>
@ -89,10 +118,11 @@ EOF
$input->isInteractive() ? $io->title('Symfony Password Encoder Utility') : $io->newLine();
$password = $input->getArgument('password');
$userClass = $input->getArgument('user-class');
$userClass = $this->getUserClass($input, $io);
$emptySalt = $input->getOption('empty-salt');
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass);
$encoderFactory = $this->encoderFactory ?: parent::getContainer()->get('security.encoder_factory');
$encoder = $encoderFactory->getEncoder($userClass);
$bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder;
if ($bcryptWithoutEmptySalt) {
@ -166,4 +196,30 @@ EOF
{
return base64_encode(random_bytes(30));
}
private function getUserClass(InputInterface $input, SymfonyStyle $io)
{
if (null !== $userClass = $input->getArgument('user-class')) {
return $userClass;
}
if (empty($this->userClasses)) {
if (null === $this->encoderFactory) {
// BC to be removed and simply keep the exception whenever there is no configured user classes in 4.0
return User::class;
}
throw new \RuntimeException('There are no configured encoders for the "security" extension.');
}
if (!$input->isInteractive() || 1 === count($this->userClasses)) {
return reset($this->userClasses);
}
$userClasses = $this->userClasses;
natcasesort($userClasses);
$userClasses = array_values($userClasses);
return $io->choice('For which user class would you like to encode a password?', $userClasses, reset($userClasses));
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@ -96,6 +97,11 @@ class SecurityExtension extends Extension
if ($config['encoders']) {
$this->createEncoders($config['encoders'], $container);
if (class_exists(Application::class)) {
$loader->load('console.xml');
$container->getDefinition('security.console.user_password_encoder_command')->replaceArgument(1, array_keys($config['encoders']));
}
}
// load ACL

View File

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<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>
<service id="security.console.user_password_encoder_command" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand" public="false">
<argument type="service" id="security.encoder_factory"/>
<argument type="collection" /> <!-- encoders' user classes -->
<tag name="console.command" />
</service>
</services>
</container>

View File

@ -13,8 +13,10 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
/**
@ -24,6 +26,7 @@ use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
*/
class UserPasswordEncoderCommandTest extends WebTestCase
{
/** @var CommandTester */
private $passwordEncoderCommandTester;
public function testEncodePasswordEmptySalt()
@ -105,6 +108,7 @@ class UserPasswordEncoderCommandTest extends WebTestCase
array(
'command' => 'security:encode-password',
'password' => 'p@ssw0rd',
'user-class' => 'Symfony\Component\Security\Core\User\User',
'--empty-salt' => true,
)
);
@ -138,6 +142,74 @@ class UserPasswordEncoderCommandTest extends WebTestCase
), array('interactive' => false));
}
public function testEncodePasswordAsksNonProvidedUserClass()
{
$this->passwordEncoderCommandTester->setInputs(array('Custom\Class\Pbkdf2\User', "\n"));
$this->passwordEncoderCommandTester->execute(array(
'command' => 'security:encode-password',
'password' => 'password',
), array('decorated' => false));
$this->assertContains(<<<EOTXT
For which user class would you like to encode a password? [Custom\Class\Bcrypt\User]:
[0] Custom\Class\Bcrypt\User
[1] Custom\Class\Pbkdf2\User
[2] Custom\Class\Test\User
[3] Symfony\Component\Security\Core\User\User
EOTXT
, $this->passwordEncoderCommandTester->getDisplay(true));
}
public function testNonInteractiveEncodePasswordUsesFirstUserClass()
{
$this->passwordEncoderCommandTester->execute(array(
'command' => 'security:encode-password',
'password' => 'password',
), array('interactive' => false));
$this->assertContains('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $this->passwordEncoderCommandTester->getDisplay());
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage There are no configured encoders for the "security" extension.
*/
public function testThrowsExceptionOnNoConfiguredEncoders()
{
$application = new ConsoleApplication();
$application->add(new UserPasswordEncoderCommand($this->createMock(EncoderFactoryInterface::class), array()));
$passwordEncoderCommand = $application->find('security:encode-password');
$tester = new CommandTester($passwordEncoderCommand);
$tester->execute(array(
'command' => 'security:encode-password',
'password' => 'password',
), array('interactive' => false));
}
/**
* @group legacy
* @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.
*/
public function testLegacy()
{
$application = new ConsoleApplication();
$application->add(new UserPasswordEncoderCommand());
$passwordEncoderCommand = $application->find('security:encode-password');
self::bootKernel(array('test_case' => 'PasswordEncode'));
$passwordEncoderCommand->setContainer(self::$kernel->getContainer());
$tester = new CommandTester($passwordEncoderCommand);
$tester->execute(array(
'command' => 'security:encode-password',
'password' => 'password',
), array('interactive' => false));
$this->assertContains('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $tester->getDisplay());
}
protected function setUp()
{
putenv('COLUMNS='.(119 + strlen(PHP_EOL)));
@ -146,8 +218,7 @@ class UserPasswordEncoderCommandTest extends WebTestCase
$application = new Application($kernel);
$application->add(new UserPasswordEncoderCommand());
$passwordEncoderCommand = $application->find('security:encode-password');
$passwordEncoderCommand = $application->get('security:encode-password');
$this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand);
}

View File

@ -25,7 +25,7 @@
"require-dev": {
"symfony/asset": "~2.8|~3.0",
"symfony/browser-kit": "~2.8|~3.0",
"symfony/console": "~2.8|~3.0",
"symfony/console": "~3.2",
"symfony/css-selector": "~2.8|~3.0",
"symfony/dom-crawler": "~2.8|~3.0",
"symfony/form": "~2.8|~3.0",