[Security] Added debug:firewall command

This commit is contained in:
Timo Bakx 2020-12-05 10:55:10 +01:00
parent d1fbf750bf
commit a9dea1db56
No known key found for this signature in database
GPG Key ID: 587E0253ECBB60C3
4 changed files with 318 additions and 1 deletions

View File

@ -4,6 +4,7 @@ CHANGELOG
5.3
---
* Add the `debug:firewall` command.
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
use `UserPasswordHashCommand` and `user:hash-password` instead
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,

View File

@ -0,0 +1,275 @@
<?php
/*
* This file is part of the Symfony 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\SecurityBundle\Command;
use Psr\Container\ContainerInterface;
use Symfony\Bundle\SecurityBundle\Security\FirewallContext;
use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
/**
* @author Timo Bakx <timobakx@gmail.com>
*/
final class DebugFirewallCommand extends Command
{
protected static $defaultName = 'debug:firewall';
protected static $defaultDescription = 'Displays information about your security firewall(s)';
private $firewallNames;
private $contexts;
private $eventDispatchers;
private $authenticators;
private $authenticatorManagerEnabled;
/**
* @param string[] $firewallNames
* @param AuthenticatorInterface[][] $authenticators
*/
public function __construct(array $firewallNames, ContainerInterface $contexts, ContainerInterface $eventDispatchers, array $authenticators, bool $authenticatorManagerEnabled)
{
$this->firewallNames = $firewallNames;
$this->contexts = $contexts;
$this->eventDispatchers = $eventDispatchers;
$this->authenticators = $authenticators;
$this->authenticatorManagerEnabled = $authenticatorManagerEnabled;
parent::__construct();
}
protected function configure(): void
{
$exampleName = $this->getExampleName();
$this
->setDescription(self::$defaultDescription)
->setHelp(<<<EOF
The <info>%command.name%</info> command displays the firewalls that are configured
in your application:
<info>php %command.full_name%</info>
You can pass a firewall name to display more detailed information about
a specific firewall:
<info>php %command.full_name% $exampleName</info>
To include all events and event listeners for a specific firewall, use the
<info>events</info> option:
<info>php %command.full_name% --events $exampleName</info>
EOF)
->setDefinition([
new InputArgument('name', InputArgument::OPTIONAL, sprintf('A firewall name (for example "%s")', $exampleName)),
new InputOption('events', null, InputOption::VALUE_NONE, 'Include a list of event listeners (only available in combination with the "name" argument)'),
]);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
if (null === $name) {
$this->displayFirewallList($io);
return 0;
}
$serviceId = sprintf('security.firewall.map.context.%s', $name);
if (!$this->contexts->has($serviceId)) {
$io->error(sprintf('Firewall %s was not found. Available firewalls are: %s', $name, implode(', ', $this->firewallNames)));
return 1;
}
/** @var FirewallContext $context */
$context = $this->contexts->get($serviceId);
$io->title(sprintf('Firewall "%s"', $name));
$this->displayFirewallSummary($name, $context, $io);
$this->displaySwitchUser($context, $io);
if ($input->getOption('events')) {
$this->displayEventListeners($name, $context, $io);
}
if ($this->authenticatorManagerEnabled) {
$this->displayAuthenticators($name, $io);
}
return 0;
}
protected function displayFirewallList(SymfonyStyle $io): void
{
$io->title('Firewalls');
$io->text('The following firewalls are defined:');
$io->listing($this->firewallNames);
$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()));
}
protected function displayFirewallSummary(string $name, FirewallContext $context, SymfonyStyle $io): void
{
if (null === $context->getConfig()) {
return;
}
$rows = [
['Name', $name],
['Context', $context->getConfig()->getContext()],
['Lazy', $context instanceof LazyFirewallContext ? 'Yes' : 'No'],
['Stateless', $context->getConfig()->isStateless() ? 'Yes' : 'No'],
['User Checker', $context->getConfig()->getUserChecker()],
['Provider', $context->getConfig()->getProvider()],
['Entry Point', $context->getConfig()->getEntryPoint()],
['Access Denied URL', $context->getConfig()->getAccessDeniedUrl()],
['Access Denied Handler', $context->getConfig()->getAccessDeniedHandler()],
];
$io->table(
['Option', 'Value'],
$rows
);
}
private function displaySwitchUser(FirewallContext $context, SymfonyStyle $io)
{
if ((null === $config = $context->getConfig()) || (null === $switchUser = $config->getSwitchUser())) {
return;
}
$io->section('User switching');
$io->table(['Option', 'Value'], [
['Parameter', $switchUser['parameter'] ?? ''],
['Provider', $switchUser['provider'] ?? $config->getProvider()],
['User Role', $switchUser['role'] ?? ''],
]);
}
protected function displayEventListeners(string $name, FirewallContext $context, SymfonyStyle $io): void
{
$io->title(sprintf('Event listeners for firewall "%s"', $name));
$dispatcherId = sprintf('security.event_dispatcher.%s', $name);
if (!$this->eventDispatchers->has($dispatcherId)) {
$io->text('No event dispatcher has been registered for this firewall.');
return;
}
/** @var EventDispatcherInterface $dispatcher */
$dispatcher = $this->eventDispatchers->get($dispatcherId);
foreach ($dispatcher->getListeners() as $event => $listeners) {
$io->section(sprintf('"%s" event', $event));
$rows = [];
foreach ($listeners as $order => $listener) {
$rows[] = [
sprintf('#%d', $order + 1),
$this->formatCallable($listener),
$dispatcher->getListenerPriority($event, $listener),
];
}
$io->table(
['Order', 'Callable', 'Priority'],
$rows
);
}
}
private function displayAuthenticators(string $name, SymfonyStyle $io): void
{
$io->title(sprintf('Authenticators for firewall "%s"', $name));
$authenticators = $this->authenticators[$name] ?? [];
if (0 === \count($authenticators)) {
$io->text('No authenticators have been registered for this firewall.');
return;
}
$io->table(
['Classname'],
array_map(
static function ($authenticator) {
return [
\get_class($authenticator),
];
},
$authenticators
)
);
}
private function formatCallable($callable): string
{
if (\is_array($callable)) {
if (\is_object($callable[0])) {
return sprintf('%s::%s()', \get_class($callable[0]), $callable[1]);
}
return sprintf('%s::%s()', $callable[0], $callable[1]);
}
if (\is_string($callable)) {
return sprintf('%s()', $callable);
}
if ($callable instanceof \Closure) {
$r = new \ReflectionFunction($callable);
if (false !== strpos($r->name, '{closure}')) {
return 'Closure()';
}
if ($class = $r->getClosureScopeClass()) {
return sprintf('%s::%s()', $class->name, $r->name);
}
return $r->name.'()';
}
if (method_exists($callable, '__invoke')) {
return sprintf('%s::__invoke()', \get_class($callable));
}
throw new \InvalidArgumentException('Callable is not describable.');
}
private function getExampleName(): string
{
$name = 'main';
if (!\in_array($name, $this->firewallNames, true)) {
$name = reset($this->firewallNames);
}
return $name;
}
}

View File

@ -164,6 +164,12 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']);
$container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']);
if (class_exists(Application::class)) {
$loader->load('debug_console.php');
$debugCommand = $container->getDefinition('security.command.debug_firewall');
$debugCommand->replaceArgument(4, $this->authenticatorManagerEnabled);
}
$this->createFirewalls($config, $container);
$this->createAuthorization($config, $container);
$this->createRoleHierarchy($config, $container);
@ -298,7 +304,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$contextRefs[$contextId] = new Reference($contextId);
$map[$contextId] = $matcher;
}
$mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs));
$container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs));
$mapDef->replaceArgument(0, new Reference('security.firewall.context_locator'));
$mapDef->replaceArgument(1, new IteratorArgument($map));
if (!$this->authenticatorManagerEnabled) {
@ -503,6 +512,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]);
$listeners[] = new Reference('security.firewall.authenticator.'.$id);
// Add authenticators to the debug:firewall command
$debugCommand = $container->getDefinition('security.command.debug_firewall');
$debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators]));
}
$config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony 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\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand;
return static function (ContainerConfigurator $container) {
$container->services()
->set('security.command.debug_firewall', DebugFirewallCommand::class)
->args([
param('security.firewalls'),
service('security.firewall.context_locator'),
tagged_locator('event_dispatcher.dispatcher'),
[],
false,
])
->tag('console.command', ['command' => 'debug:firewall'])
;
};