Skip to content

Commit 7fd0580

Browse files
committed
feature #261 MakeAuthenticator : update security.yaml (nikophil)
This PR was squashed before being merged into the 1.0-dev branch (closes #261). Discussion ---------- MakeAuthenticator : update security.yaml This PR adds the newly created authenticator to `security.yaml` The process is the following : - if no firewall exist, a "main" firewall is automatically created (this should never happen) - if there is only one "real" firewall (i.e. : a firewall withou security: false"), the authenticator is added to this firewall - if there are more than one firewall, the user is asked to which firewall the authenticator should be added - if the target firewall already has an authenticator, the user is asked which authenticator should be the entry point. Commits ------- 3a26d98 add return type to InteractiveSecurityHelper::guessFirewallName a983caa fixes after PR 2186549 Add entry point if one or more authenticators exist on chosen firewall c63a86a Add authenticator : let the user chose which firewall to update 2fb5b40 add authenticator in security.yaml the simplest way
2 parents 2250722 + 3a26d98 commit 7fd0580

File tree

19 files changed

+774
-19
lines changed

19 files changed

+774
-19
lines changed

src/Maker/MakeAuthenticator.php

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,39 @@
1313

1414
use Symfony\Bundle\MakerBundle\ConsoleStyle;
1515
use Symfony\Bundle\MakerBundle\DependencyBuilder;
16+
use Symfony\Bundle\MakerBundle\FileManager;
1617
use Symfony\Bundle\MakerBundle\Generator;
1718
use Symfony\Bundle\MakerBundle\InputConfiguration;
19+
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
20+
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
21+
use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
22+
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
23+
use Symfony\Bundle\SecurityBundle\SecurityBundle;
1824
use Symfony\Component\Console\Command\Command;
1925
use Symfony\Component\Console\Input\InputArgument;
20-
use Symfony\Bundle\SecurityBundle\SecurityBundle;
2126
use Symfony\Component\Console\Input\InputInterface;
27+
use Symfony\Component\Console\Input\InputOption;
2228

2329
/**
2430
* @author Ryan Weaver <[email protected]>
31+
*
32+
* @internal
2533
*/
2634
final class MakeAuthenticator extends AbstractMaker
2735
{
36+
private $fileManager;
37+
38+
private $configUpdater;
39+
40+
private $generator;
41+
42+
public function __construct(FileManager $fileManager, SecurityConfigUpdater $configUpdater, Generator $generator)
43+
{
44+
$this->fileManager = $fileManager;
45+
$this->configUpdater = $configUpdater;
46+
$this->generator = $generator;
47+
}
48+
2849
public static function getCommandName(): string
2950
{
3051
return 'make:auth';
@@ -35,8 +56,34 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
3556
$command
3657
->setDescription('Creates an empty Guard authenticator')
3758
->addArgument('authenticator-class', InputArgument::OPTIONAL, 'The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)')
38-
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt'))
39-
;
59+
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt'));
60+
}
61+
62+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
63+
{
64+
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
65+
return;
66+
}
67+
68+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
69+
$securityData = $manipulator->getData();
70+
71+
$interactiveSecurityHelper = new InteractiveSecurityHelper();
72+
73+
$command->addOption('firewall-name', null, InputOption::VALUE_OPTIONAL, '');
74+
$input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData));
75+
76+
$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL);
77+
78+
$authenticatorClassNameDetails = $this->generator->createClassNameDetails(
79+
$input->getArgument('authenticator-class'),
80+
'Security\\'
81+
);
82+
83+
$input->setOption(
84+
'entry-point',
85+
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $authenticatorClassNameDetails->getFullName(), $firewallName)
86+
);
4087
}
4188

4289
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
@@ -51,14 +98,38 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
5198
'authenticator/Empty.tpl.php',
5299
[]
53100
);
101+
102+
$securityYamlUpdated = false;
103+
$path = 'config/packages/security.yaml';
104+
if ($this->fileManager->fileExists($path)) {
105+
try {
106+
$newYaml = $this->configUpdater->updateForAuthenticator(
107+
$this->fileManager->getFileContents($path),
108+
$input->getOption('firewall-name'),
109+
$input->getOption('entry-point'),
110+
$classNameDetails->getFullName()
111+
);
112+
$generator->dumpFile($path, $newYaml);
113+
$securityYamlUpdated = true;
114+
} catch (YamlManipulationFailedException $e) {
115+
}
116+
}
117+
54118
$generator->writeChanges();
55119

56120
$this->writeSuccessMessage($io);
57121

58-
$io->text([
59-
'Next: Customize your new authenticator.',
60-
'Then, configure the "guard" key on your firewall to use it.',
61-
]);
122+
$text = ['Next: Customize your new authenticator.'];
123+
if (!$securityYamlUpdated) {
124+
$yamlExample = $this->configUpdater->updateForAuthenticator(
125+
'security: {}',
126+
'main',
127+
null,
128+
$classNameDetails->getFullName()
129+
);
130+
$text[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
131+
}
132+
$io->text($text);
62133
}
63134

64135
public function configureDependencies(DependencyBuilder $dependencies)

src/Resources/config/makers.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<defaults public="false" />
99

1010
<service id="maker.maker.make_authenticator" class="Symfony\Bundle\MakerBundle\Maker\MakeAuthenticator">
11+
<argument type="service" id="maker.file_manager" />
12+
<argument type="service" id="maker.security_config_updater" />
13+
<argument type="service" id="maker.generator" />
1114
<tag name="maker.command" />
1215
</service>
1316

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\MakerBundle\Security;
13+
14+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
15+
use Symfony\Component\Console\Style\SymfonyStyle;
16+
17+
/**
18+
* @internal
19+
*/
20+
final class InteractiveSecurityHelper
21+
{
22+
public function guessFirewallName(SymfonyStyle $io, array $securityData): string
23+
{
24+
$realFirewalls = array_filter(
25+
$securityData['security']['firewalls'] ?? [],
26+
function ($item) {
27+
return !isset($item['security']) || true === $item['security'];
28+
}
29+
);
30+
31+
if (0 === \count($realFirewalls)) {
32+
return 'main';
33+
}
34+
35+
if (1 === \count($realFirewalls)) {
36+
return key($realFirewalls);
37+
}
38+
39+
return $io->choice('Which firewall do you want to update ?', array_keys($realFirewalls), key($realFirewalls));
40+
}
41+
42+
public function guessEntryPoint(SymfonyStyle $io, array $securityData, string $authenticatorClass, string $firewallName)
43+
{
44+
if (!isset($securityData['security'])) {
45+
$securityData['security'] = [];
46+
}
47+
48+
if (!isset($securityData['security']['firewalls'])) {
49+
$securityData['security']['firewalls'] = [];
50+
}
51+
52+
$firewalls = $securityData['security']['firewalls'];
53+
if (!isset($firewalls[$firewallName])) {
54+
throw new RuntimeCommandException(sprintf('Firewall "%s" does not exist', $firewallName));
55+
}
56+
57+
if (!isset($firewalls[$firewallName]['guard'])
58+
|| !isset($firewalls[$firewallName]['guard']['authenticators'])
59+
|| !$firewalls[$firewallName]['guard']['authenticators']
60+
|| isset($firewalls[$firewallName]['guard']['entry_point'])) {
61+
return null;
62+
}
63+
64+
$authenticators = $firewalls[$firewallName]['guard']['authenticators'];
65+
$authenticators[] = $authenticatorClass;
66+
67+
return $io->choice(
68+
'The entry point for your firewall is what should happen when an anonymous user tries to access
69+
a protected page. For example, a common "entry point" behavior is to redirect to the login page.
70+
The "entry point" behavior is controlled by the start() method on your authenticator.
71+
However, you will now have multiple authenticators. You need to choose which authenticator\'s
72+
start() method should be used as the entry point (the start() method on all other
73+
authenticators will be ignored, and can be blank.',
74+
$authenticators,
75+
current($authenticators)
76+
);
77+
}
78+
}

src/Security/SecurityConfigUpdater.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,7 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
3434
{
3535
$this->manipulator = new YamlSourceManipulator($yamlSource);
3636

37-
// normalize the top level, just in case
38-
if (!isset($this->manipulator->getData()['security'])) {
39-
$newData = $this->manipulator->getData();
40-
$newData['security'] = [];
41-
$this->manipulator->setData($newData);
42-
}
37+
$this->normalizeSecurityYamlFile();
4338

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

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

51+
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass): string
52+
{
53+
$this->manipulator = new YamlSourceManipulator($yamlSource);
54+
55+
$this->normalizeSecurityYamlFile();
56+
57+
$newData = $this->manipulator->getData();
58+
59+
if (!isset($newData['security']['firewalls'])) {
60+
$newData['security']['firewalls'] = [];
61+
}
62+
63+
if (!isset($newData['security']['firewalls'][$firewallName])) {
64+
$newData['security']['firewalls'][$firewallName] = ['anonymous' => true];
65+
}
66+
67+
$firewall = $newData['security']['firewalls'][$firewallName];
68+
69+
if (!isset($firewall['guard'])) {
70+
$firewall['guard'] = [];
71+
}
72+
73+
if (!isset($firewall['guard']['authenticators'])) {
74+
$firewall['guard']['authenticators'] = [];
75+
}
76+
77+
$firewall['guard']['authenticators'][] = $authenticatorClass;
78+
79+
if (\count($firewall['guard']['authenticators']) > 1) {
80+
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
81+
}
82+
83+
$newData['security']['firewalls'][$firewallName] = $firewall;
84+
$this->manipulator->setData($newData);
85+
$contents = $this->manipulator->getContents();
86+
87+
return $contents;
88+
}
89+
90+
private function normalizeSecurityYamlFile()
91+
{
92+
if (!isset($this->manipulator->getData()['security'])) {
93+
$newData = $this->manipulator->getData();
94+
$newData['security'] = [];
95+
$this->manipulator->setData($newData);
96+
}
97+
}
98+
5699
private function updateProviders(UserClassConfiguration $userConfig, string $userClass)
57100
{
58101
if ($this->isSingleInMemoryProviderConfigured()) {

0 commit comments

Comments
 (0)