Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MakeAuthenticator : update security.yaml #261

Merged
merged 5 commits into from
Sep 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 78 additions & 7 deletions src/Maker/MakeAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,39 @@

use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;

/**
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @internal
*/
final class MakeAuthenticator extends AbstractMaker
{
private $fileManager;

private $configUpdater;

private $generator;

public function __construct(FileManager $fileManager, SecurityConfigUpdater $configUpdater, Generator $generator)
{
$this->fileManager = $fileManager;
$this->configUpdater = $configUpdater;
$this->generator = $generator;
}
nikophil marked this conversation as resolved.
Show resolved Hide resolved

public static function getCommandName(): string
{
return 'make:auth';
Expand All @@ -35,8 +56,34 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
$command
->setDescription('Creates an empty Guard authenticator')
->addArgument('authenticator-class', InputArgument::OPTIONAL, 'The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)')
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt'))
;
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt'));
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
{
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
return;
}

$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$securityData = $manipulator->getData();

$interactiveSecurityHelper = new InteractiveSecurityHelper();

$command->addOption('firewall-name', null, InputOption::VALUE_OPTIONAL, '');
$input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData));

$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL);

$authenticatorClassNameDetails = $this->generator->createClassNameDetails(
$input->getArgument('authenticator-class'),
'Security\\'
);

$input->setOption(
'entry-point',
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $authenticatorClassNameDetails->getFullName(), $firewallName)
);
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
Expand All @@ -51,14 +98,38 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'authenticator/Empty.tpl.php',
[]
);

$securityYamlUpdated = false;
$path = 'config/packages/security.yaml';
if ($this->fileManager->fileExists($path)) {
try {
$newYaml = $this->configUpdater->updateForAuthenticator(
$this->fileManager->getFileContents($path),
$input->getOption('firewall-name'),
$input->getOption('entry-point'),
$classNameDetails->getFullName()
);
$generator->dumpFile($path, $newYaml);
$securityYamlUpdated = true;
} catch (YamlManipulationFailedException $e) {
}
}

$generator->writeChanges();

$this->writeSuccessMessage($io);

$io->text([
'Next: Customize your new authenticator.',
'Then, configure the "guard" key on your firewall to use it.',
]);
$text = ['Next: Customize your new authenticator.'];
if (!$securityYamlUpdated) {
$yamlExample = $this->configUpdater->updateForAuthenticator(
'security: {}',
'main',
null,
$classNameDetails->getFullName()
);
$text[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
}
$io->text($text);
}

public function configureDependencies(DependencyBuilder $dependencies)
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/config/makers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<defaults public="false" />

<service id="maker.maker.make_authenticator" class="Symfony\Bundle\MakerBundle\Maker\MakeAuthenticator">
<argument type="service" id="maker.file_manager" />
<argument type="service" id="maker.security_config_updater" />
<argument type="service" id="maker.generator" />
<tag name="maker.command" />
</service>

Expand Down
78 changes: 78 additions & 0 deletions src/Security/InteractiveSecurityHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Security;

use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @internal
*/
final class InteractiveSecurityHelper
{
public function guessFirewallName(SymfonyStyle $io, array $securityData): string
{
$realFirewalls = array_filter(
$securityData['security']['firewalls'] ?? [],
function ($item) {
return !isset($item['security']) || true === $item['security'];
}
);

if (0 === \count($realFirewalls)) {
return 'main';
}

if (1 === \count($realFirewalls)) {
return key($realFirewalls);
}

return $io->choice('Which firewall do you want to update ?', array_keys($realFirewalls), key($realFirewalls));
}

public function guessEntryPoint(SymfonyStyle $io, array $securityData, string $authenticatorClass, string $firewallName)
nikophil marked this conversation as resolved.
Show resolved Hide resolved
{
if (!isset($securityData['security'])) {
$securityData['security'] = [];
}

if (!isset($securityData['security']['firewalls'])) {
$securityData['security']['firewalls'] = [];
}

$firewalls = $securityData['security']['firewalls'];
if (!isset($firewalls[$firewallName])) {
throw new RuntimeCommandException(sprintf('Firewall "%s" does not exist', $firewallName));
}

if (!isset($firewalls[$firewallName]['guard'])
|| !isset($firewalls[$firewallName]['guard']['authenticators'])
|| !$firewalls[$firewallName]['guard']['authenticators']
|| isset($firewalls[$firewallName]['guard']['entry_point'])) {
return null;
}

$authenticators = $firewalls[$firewallName]['guard']['authenticators'];
$authenticators[] = $authenticatorClass;

return $io->choice(
'The entry point for your firewall is what should happen when an anonymous user tries to access
a protected page. For example, a common "entry point" behavior is to redirect to the login page.
The "entry point" behavior is controlled by the start() method on your authenticator.
However, you will now have multiple authenticators. You need to choose which authenticator\'s
start() method should be used as the entry point (the start() method on all other
authenticators will be ignored, and can be blank.',
$authenticators,
current($authenticators)
);
}
}
55 changes: 49 additions & 6 deletions src/Security/SecurityConfigUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
{
$this->manipulator = new YamlSourceManipulator($yamlSource);

// normalize the top level, just in case
if (!isset($this->manipulator->getData()['security'])) {
$newData = $this->manipulator->getData();
$newData['security'] = [];
$this->manipulator->setData($newData);
}
$this->normalizeSecurityYamlFile();

$this->updateProviders($userConfig, $userClass);

Expand All @@ -53,6 +48,54 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
return $contents;
}

public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass): string
{
$this->manipulator = new YamlSourceManipulator($yamlSource);

$this->normalizeSecurityYamlFile();

$newData = $this->manipulator->getData();

if (!isset($newData['security']['firewalls'])) {
$newData['security']['firewalls'] = [];
}

if (!isset($newData['security']['firewalls'][$firewallName])) {
$newData['security']['firewalls'][$firewallName] = ['anonymous' => true];
}

$firewall = $newData['security']['firewalls'][$firewallName];

if (!isset($firewall['guard'])) {
$firewall['guard'] = [];
}

if (!isset($firewall['guard']['authenticators'])) {
$firewall['guard']['authenticators'] = [];
}

$firewall['guard']['authenticators'][] = $authenticatorClass;

if (\count($firewall['guard']['authenticators']) > 1) {
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
}

$newData['security']['firewalls'][$firewallName] = $firewall;
$this->manipulator->setData($newData);
$contents = $this->manipulator->getContents();

return $contents;
}

private function normalizeSecurityYamlFile()
{
if (!isset($this->manipulator->getData()['security'])) {
$newData = $this->manipulator->getData();
$newData['security'] = [];
$this->manipulator->setData($newData);
}
}

private function updateProviders(UserClassConfiguration $userConfig, string $userClass)
{
if ($this->isSingleInMemoryProviderConfigured()) {
Expand Down
6 changes: 3 additions & 3 deletions src/Util/YamlSourceManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ private function changeValueInYaml($value)
$endValuePosition = $this->findEndPositionOfValue($originalVal);

$newYamlValue = $this->convertToYaml($value);
if (!is_array($originalVal) && is_array($value)) {
if (!\is_array($originalVal) && \is_array($value)) {
// we're converting from a scalar to a (multiline) array
// this means we need to break onto the next line

Expand Down Expand Up @@ -798,7 +798,7 @@ private function advanceCurrentPosition(int $newPosition)
* to look for indentation.
*/
if ($this->isCharLineBreak(substr($this->contents, $originalPosition - 1, 1))) {
$originalPosition--;
--$originalPosition;
}

// look for empty lines and track the current indentation
Expand Down Expand Up @@ -890,7 +890,7 @@ private function guessNextArrayTypeAndAdvance(): string
// because we are either moving along one line until [ {
// or we are finding a line break and stopping: indentation
// should not be calculated
$this->currentPosition++;
++$this->currentPosition;

if ($this->isCharLineBreak($nextCharacter)) {
return self::ARRAY_FORMAT_MULTILINE;
Expand Down
Loading