From a9dea1db562a118b47f45019f5f0bfe96a14fe9d Mon Sep 17 00:00:00 2001 From: Timo Bakx Date: Sat, 5 Dec 2020 10:55:10 +0100 Subject: [PATCH] [Security] Added debug:firewall command --- .../Bundle/SecurityBundle/CHANGELOG.md | 1 + .../Command/DebugFirewallCommand.php | 275 ++++++++++++++++++ .../DependencyInjection/SecurityExtension.php | 15 +- .../Resources/config/debug_console.php | 28 ++ 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index d50c23959b74c..2473dfd9ced74 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -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, diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php new file mode 100644 index 0000000000000..56c3cf14476f3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -0,0 +1,275 @@ + + * + * 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 + */ +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(<<%command.name% command displays the firewalls that are configured +in your application: + + php %command.full_name% + +You can pass a firewall name to display more detailed information about +a specific firewall: + + php %command.full_name% $exampleName + +To include all events and event listeners for a specific firewall, use the +events option: + + php %command.full_name% --events $exampleName + +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. debug:firewall %s)', $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; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 368f4acf9f3d1..5c782a25bf87e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -164,6 +164,12 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ private function createFirewalls(array $config, ContainerBuilder $container) $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 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ ->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); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php new file mode 100644 index 0000000000000..242722f72452a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php @@ -0,0 +1,28 @@ + + * + * 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']) + ; +};