10000 [Security] Added debug:firewall command · symfony/symfony@a9dea1d · GitHub
[go: up one dir, main page]

Skip to content

Commit a9dea1d

Browse files
committed
[Security] Added debug:firewall command
1 parent d1fbf75 commit a9dea1d

File tree

4 files changed

+318
-1
lines changed

4 files changed

+318
-1
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.3
55
---
66

7+
* Add the `debug:firewall` command.
78
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
89
use `UserPasswordHashCommand` and `user:hash-password` instead
910
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\SecurityBundle\Command;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Bundle\SecurityBundle\Security\FirewallContext;
16+
use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext;
17+
use Symfony\Component\Console\Command\Command;
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Input\InputOption;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
use Symfony\Component\Console\Style\SymfonyStyle;
23+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
24+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
25+
26+
/**
27+
* @author Timo Bakx <timobakx@gmail.com>
28+
*/
29+
final class DebugFirewallCommand extends Command
30+
{
31+
protected static $defaultName = 'debug:firewall';
32+
protected static $defaultDescription = 'Displays information about your security firewall(s)';
33+
34+
private $firewallNames;
35+
private $contexts;
36+
private $eventDispatchers;
37+
private $authenticators;
38+
private $authenticatorManagerEnabled;
39+
40+
/**
41+
* @param string[] $firewallNames
42+
* @param AuthenticatorInterface[][] $authenticators
43+
*/
44+
public function __construct(array $firewallNames, ContainerInterface $contexts, ContainerInterface $eventDispatchers, array $authenticators, bool $authenticatorManagerEnabled)
45+
{
46+
$this->firewallNames = $firewallNames;
47+
$this->contexts = $contexts;
48+
$this->eventDispatchers = $eventDispatchers;
49+
$this->authenticators = $authenticators;
50+
$this->authenticatorManagerEnabled = $authenticatorManagerEnabled;
51+
52+
parent::__construct();
53+
}
54+
55+
protected function configure(): void
56+
{
57+
$exampleName = $this->getExampleName();
58+
59+
$this
60+
->setDescription(self::$defaultDescription)
61+
->setHelp(<<<EOF
62+
The <info>%command.name%</info> command displays the firewalls that are configured
63+
in your application:
64+
65+
<info>php %command.full_name%</info>
66+
67+
You can pass a firewall name to display more detailed information about
68+
a specific firewall:
69+
70+
<info>php %command.full_name% $exampleName</info>
71+
72+
To include all events and event listeners for a specific firewall, use the
73+
<info>events</info> option:
74+
75+
<info>php %command.full_name% --events $exampleName</info>
76+
77+
EOF)
78+
->setDefinition([
79+
new InputArgument('name', InputArgument::OPTIONAL, sprintf('A firewall name (for example "%s")', $exampleName)),
80+
new InputOption('events', null, InputOption::VALUE_NONE, 'Include a list of event listeners (only available in combination with the "name" argument)'),
81+
]);
82+
}
83+
84+
protected function execute(InputInterface $input, OutputInterface $output): int
85+
{
86+
$io = new SymfonyStyle($input, $output);
87+
88+
$name = $input->getArgument('name');
89+
90+
if (null === $name) {
91+
$this->displayFirewallList($io);
92+
93+
return 0;
94+
}
95+
96+
$serviceId = sprintf('security.firewall.map.context.%s', $name);
97+
98+
if (!$this->contexts->has($serviceId)) {
99+
$io->error(sprintf('Firewall %s was not found. Available firewalls are: %s', $name, implode(', ', $this->firewallNames)));
100+
101+
return 1;
102+
}
103+
104+
/** @var FirewallContext $context */
105+
$context = $this->contexts->get($serviceId);
106+
107+
$io->title(sprintf('Firewall "%s"', $name));
108+
109+
$this->displayFirewallSummary($name, $context, $io);
110+
111+
$this->displaySwitchUser($context, $io);
112+
113+
if ($input->getOption('events')) {
114+
$this->displayEventListeners($name, $context, $io);
115+
}
116+
117+
if ($this->authenticatorManagerEnabled) {
118+
$this->displayAuthenticators($name, $io);
119+
}
120+
121+
return 0;
122+
}
123+
124+
protected function displayFirewallList(SymfonyStyle $io): void
125+
{
126+
$io->title('Firewalls');
127+
$io->text('The following firewalls are defined:');
128+
129+
$io->listing($this->firewallNames);
130+
131+
$io->comment(sprintf('To view details of a specific firewall, re-run this command with a firewall name. (e.g. <comment>debug:firewall %s</comment>)', $this->getExampleName()));
132+
}
133+
134+
protected function displayFirewallSummary(string $name, FirewallContext $context, SymfonyStyle $io): void
135+
{
136+
if (null === $context->getConfig()) {
137+
return;
138+
}
139+
140+
$rows = [
141+
['Name', $name],
142+
['Context', $context->getConfig()->getContext()],
143+
['Lazy', $context instanceof LazyFirewallContext ? 'Yes' : 'No'],
144+
['Stateless', $context->getConfig()->isStateless() ? 'Yes' : 'No'],
145+
['User Checker', $context->getConfig()->getUserChecker()],
146+
['Provider', $context->getConfig()->getProvider()],
147+
['Entry Point', $context->getConfig()->getEntryPoint()],
148+
['Access Denied URL', $context->getConfig()->getAccessDeniedUrl()],
149+
['Access Denied Handler', $context->getConfig()->getAccessDeniedHandler()],
150+
];
151+
152+
$io->table(
153+
['Option', 'Value'],
154+
$rows
155+
);
156+
}
157+
158+
private function displaySwitchUser(FirewallContext $context, SymfonyStyle $io)
159+
{
160+
if ((null === $config = $context->getConfig()) || (null === $switchUser = $config->getSwitchUser())) {
161+
return;
162+
}
163+
164+
$io->section('User switching');
165+
166+
$io->table(['Option', 'Value'], [
167+
['Parameter', $switchUser['parameter'] ?? ''],
168+
['Provider', $switchUser['provider'] ?? $config->getProvider()],
169+
['User Role', $switchUser['role'] ?? ''],
170+
]);
171+
}
172+
173+
protected function displayEventListeners(string $name, FirewallContext $context, SymfonyStyle $io): void
174+
{
175+
$io->title(sprintf('Event listeners for firewall "%s"', $name));
176+
177+
$dispatcherId = sprintf('security.event_dispatcher.%s', $name);
178+
179+
if (!$this->eventDispatchers->has($dispatcherId)) {
180+
$io->text('No event dispatcher has been registered for this firewall.');
181+
182+
return;
183+
}
184+
185+
/** @var EventDispatcherInterface $dispatcher */
186+
$dispatcher = $this->eventDispatchers->get($dispatcherId);
187+
188+
foreach ($dispatcher->getListeners() as $event => $listeners) {
189+
$io->section(sprintf('"%s" event', $event));
190+
191+
$rows = [];
192+
foreach ($listeners as $order => $listener) {
193+
$rows[] = [
194+
sprintf('#%d', $order + 1),
195+
$this->formatCallable($listener),
196+
$dispatcher->getListenerPriority($event, $listener),
197+
];
198+
}
199+
200+
$io->table(
201+
['Order', 'Callable', 'Priority'],
202+
$rows
203+
);
204+
}
205+
}
206+
207+
private function displayAuthenticators(string $name, SymfonyStyle $io): void
208+
{
209+
$io->title(sprintf('Authenticators for firewall "%s"', $name));
210+
211+
$authenticators = $this->authenticators[$name] ?? [];
212+
213+
if (0 === \count($authenticators)) {
214+
$io->text('No authenticators have been registered for this firewall.');
215+
216+
return;
217+
}
218+
219+
$io->table(
220+
['Classname'],
221+
array_map(
222+
static function ($authenticator) {
223+
return [
224+
\get_class($authenticator),
225+
];
226+
},
227+
$authenticators
228+
)
229+
);
230+
}
231+
232+
private function formatCallable($callable): string
233+
{
234+
if (\is_array($callable)) {
235+
if (\is_object($callable[0])) {
236+
return sprintf('%s::%s()', \get_class($callable[0]), $callable[1]);
237+
}
238+
239+
return sprintf('%s::%s()', $callable[0], $callable[1]);
240+
}
241+
242+
if (\is_string($callable)) {
243+
return sprintf('%s()', $callable);
244+
}
245+
246+
if ($callable instanceof \Closure) {
247+
$r = new \ReflectionFunction($callable);
248+
if (false !== strpos($r->name, '{closure}')) {
249+
return 'Closure()';
250+
}
251+
if ($class = $r->getClosureScopeClass()) {
252+
return sprintf('%s::%s()', $class->name, $r->name);
253+
}
254+
255+
return $r->name.'()';
256+
}
257+
258+
if (method_exists($callable, '__invoke')) {
259+
return sprintf('%s::__invoke()', \get_class($callable));
260+
}
261+
262+
throw new \InvalidArgumentException('Callable is not describable.');
263+
}
264+
265+
private function getExampleName(): string
266+
{
267+
$name = 'main';
268+
269+
if (!\in_array($name, $this->firewallNames, true)) {
270+
$name = reset($this->firewallNames);
271+
}
272+
273+
return $name;
274+
}
275+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ public function load(array $configs, ContainerBuilder $container)
164164
$container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']);
165165
$container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']);
166166

167+
if (class_exists(Application::class)) {
168+
$loader->load('debug_console.php');
169+
$debugCommand = $container->getDefinition('security.command.debug_firewall');
170+
$debugCommand->replaceArgument(4, $this->authenticatorManagerEnabled);
171+
}
172+
167173
$this->createFirewalls($config, $container);
168174
$this->createAuthorization($config, $container);
169175
$this->createRoleHierarchy($config, $container);
@@ -298,7 +304,10 @@ private function createFirewalls(array $config, ContainerBuilder $container)
298304
$contextRefs[$contextId] = new Reference($contextId);
299305
$map[$contextId] = $matcher;
300306
}
301-
$mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs));
307+
308+
$container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs));
309+
310+
$mapDef->replaceArgument(0, new Reference('security.firewall.context_locator'));
302311
$mapDef->replaceArgument(1, new IteratorArgument($map));
303312

304313
if (!$this->authenticatorManagerEnabled) {
@@ -503,6 +512,10 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
503512
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
504513

505514
$listeners[] = new Reference('security.firewall.authenticator.'.$id);
515+
516+
// Add authenticators to the debug:firewall command
517+
$debugCommand = $container->getDefinition('security.command.debug_firewall');
518+
$debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators]));
506519
}
507520

508521
$config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand;
15+
16+
return static function (ContainerConfigurator $container) {
17+
$container->services()
18+
->set('security.command.debug_firewall', DebugFirewallCommand::class)
19+
->args([
20+
param('security.firewalls'),
21+
service('security.firewall.context_locator'),
22+
tagged_locator('event_dispatcher.dispatcher'),
23+
[],
24+
false,
25+
])
26+
->tag('console.command', ['command' => 'debug:firewall'])
27+
;
28+
};

0 commit comments

Comments
 (0)
0