From 369f19fcfd8569015a576270c9498fe7a2095ab3 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Fri, 2 Jun 2017 16:08:16 +0200 Subject: [PATCH] Give info about called security listeners in profiler --- .../Bundle/SecurityBundle/CHANGELOG.md | 1 + .../DataCollector/SecurityDataCollector.php | 17 ++++- .../Debug/TraceableFirewallListener.php | 43 +++++++++++ .../SecurityBundle/Debug/WrappedListener.php | 76 +++++++++++++++++++ .../Resources/config/collectors.xml | 1 + .../Resources/config/security_debug.xml | 9 +++ .../views/Collector/security.html.twig | 42 ++++++++++ .../SecurityDataCollectorTest.php | 51 ++++++++++++- .../Debug/TraceableFirewallListenerTest.php | 65 ++++++++++++++++ .../Bundle/SecurityBundle/composer.json | 6 +- .../Component/Security/Http/Firewall.php | 20 +++-- 11 files changed, 317 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 32d00431262b..a9d10bd8dacc 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array` + * added info about called security listeners in profiler 3.3.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 508d98b52f67..d25a5be606ec 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; @@ -39,6 +40,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn private $logoutUrlGenerator; private $accessDecisionManager; private $firewallMap; + private $firewall; private $hasVarDumper; /** @@ -49,14 +51,16 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn * @param LogoutUrlGenerator|null $logoutUrlGenerator * @param AccessDecisionManagerInterface|null $accessDecisionManager * @param FirewallMapInterface|null $firewallMap + * @param TraceableFirewallListener|null $firewall */ - public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null) + public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null) { $this->tokenStorage = $tokenStorage; $this->roleHierarchy = $roleHierarchy; $this->logoutUrlGenerator = $logoutUrlGenerator; $this->accessDecisionManager = $accessDecisionManager; $this->firewallMap = $firewallMap; + $this->firewall = $firewall; $this->hasVarDumper = class_exists(ClassStub::class); } @@ -167,6 +171,12 @@ public function collect(Request $request, Response $response, \Exception $except ); } } + + // collect firewall listeners information + $this->data['listeners'] = array(); + if ($this->firewall) { + $this->data['listeners'] = $this->firewall->getWrappedListeners(); + } } public function lateCollect() @@ -305,6 +315,11 @@ public function getFirewall() return $this->data['firewall']; } + public function getListeners() + { + return $this->data['listeners']; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php new file mode 100644 index 000000000000..7c45a60c1a90 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; + +/** + * Firewall collecting called listeners. + * + * @author Robin Chalas + */ +final class TraceableFirewallListener extends FirewallListener +{ + private $wrappedListeners; + + public function getWrappedListeners() + { + return $this->wrappedListeners; + } + + protected function handleRequest(GetResponseEvent $event, $listeners) + { + foreach ($listeners as $listener) { + $wrappedListener = new WrappedListener($listener); + $wrappedListener->handle($event); + $this->wrappedListeners[] = $wrappedListener->getInfo(); + + if ($event->hasResponse()) { + break; + } + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php new file mode 100644 index 000000000000..435ecc5feb57 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * Wraps a security listener for calls record. + * + * @author Robin Chalas + */ +final class WrappedListener implements ListenerInterface +{ + private $response; + private $listener; + private $time; + private $stub; + private static $hasVarDumper; + + public function __construct(ListenerInterface $listener) + { + $this->listener = $listener; + + if (null === self::$hasVarDumper) { + self::$hasVarDumper = class_exists(ClassStub::class); + } + } + + /** + * {@inheritdoc} + */ + public function handle(GetResponseEvent $event) + { + $startTime = microtime(true); + $this->listener->handle($event); + $this->time = microtime(true) - $startTime; + $this->response = $event->getResponse(); + } + + /** + * Proxies all method calls to the original listener. + */ + public function __call($method, $arguments) + { + return call_user_func_array(array($this->listener, $method), $arguments); + } + + public function getWrappedListener() + { + return $this->listener; + } + + public function getInfo() + { + if (null === $this->stub) { + $this->stub = self::$hasVarDumper ? new ClassStub(get_class($this->listener)) : get_class($this->listener); + } + + return array( + 'response' => $this->response, + 'time' => $this->time, + 'stub' => $this->stub, + ); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index 50ee3bba2b89..a8170af900ff 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -14,6 +14,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml index f836925ef8fd..6087f9ee5b19 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml @@ -10,5 +10,14 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 9cd6ec4d78db..46804a4d2ed2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -150,6 +150,8 @@ {% if collector.firewall.security_enabled %} +

Configuration

+ @@ -188,6 +190,46 @@
+ +

Listeners

+ + {% if collector.listeners|default([]) is empty %} +
+

No security listeners have been recorded. Check that debugging is enabled in the kernel.

+
+ {% else %} + + + + + + + + + + {% set previous_event = (collector.listeners|first) %} + {% for listener in collector.listeners %} + {% if loop.first or listener != previous_event %} + {% if not loop.first %} + + {% endif %} + + + {% set previous_event = listener %} + {% endif %} + + + + + + + + {% if loop.last %} + + {% endif %} + {% endfor %} +
ListenerDurationResponse
{{ profiler_dump(listener.stub) }}{{ '%0.2f'|format(listener.time * 1000) }} ms{{ listener.response ? profiler_dump(listener.response) : '(none)' }}
+ {% endif %} {% endif %} {% elseif collector.enabled %}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index f155f901c9c3..1c75641daf69 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -13,13 +13,19 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; +use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\RoleHierarchy; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; use Symfony\Component\Security\Http\FirewallMapInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; class SecurityDataCollectorTest extends TestCase { @@ -89,7 +95,7 @@ public function testGetFirewall() ->with($request) ->willReturn($firewallConfig); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); $collector->collect($request, $this->getResponse()); $collector->lateCollect(); $collected = $collector->getFirewall(); @@ -124,7 +130,7 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); @@ -134,11 +140,50 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); } + /** + * @group time-sensitive + */ + public function testGetListeners() + { + $request = $this->getRequest(); + $event = new GetResponseEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); + $event->setResponse($response = $this->getResponse()); + $listener = $this->getMockBuilder(ListenerInterface::class)->getMock(); + $listener + ->expects($this->once()) + ->method('handle') + ->with($event); + $firewallMap = $this + ->getMockBuilder(FirewallMap::class) + ->disableOriginalConstructor() + ->getMock(); + $firewallMap + ->expects($this->any()) + ->method('getFirewallConfig') + ->with($request) + ->willReturn(null); + $firewallMap + ->expects($this->once()) + ->method('getListeners') + ->with($request) + ->willReturn(array(array($listener), null)); + + $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); + $firewall->onKernelRequest($event); + + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall); + $collector->collect($request, $response); + + $this->assertNotEmpty($collected = $collector->getListeners()[0]); + $collector->lateCollect(); + $this->addToAssertionCount(1); + } + public function provideRoles() { return array( diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php new file mode 100644 index 000000000000..3ddbb1fd4c43 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @group time-sensitive + */ +class TraceableFirewallListenerTest extends TestCase +{ + public function testOnKernelRequestRecordsListeners() + { + $request = new Request(); + $event = new GetResponseEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); + $event->setResponse($response = new Response()); + $listener = $this->getMockBuilder(ListenerInterface::class)->getMock(); + $listener + ->expects($this->once()) + ->method('handle') + ->with($event); + $firewallMap = $this + ->getMockBuilder(FirewallMap::class) + ->disableOriginalConstructor() + ->getMock(); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->with($request) + ->willReturn(null); + $firewallMap + ->expects($this->once()) + ->method('getListeners') + ->with($request) + ->willReturn(array(array($listener), null)); + + $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); + $firewall->onKernelRequest($event); + + $listeners = $firewall->getWrappedListeners(); + $this->assertCount(1, $listeners); + $this->assertSame($response, $listeners[0]['response']); + $this->assertInstanceOf(ClassStub::class, $listeners[0]['stub']); + $this->assertSame(get_class($listener), (string) $listeners[0]['stub']); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 19d724fc769c..31c2731e1527 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=5.5.9", - "symfony/security": "~3.3|~4.0", + "symfony/security": "~3.4|~4.0", "symfony/dependency-injection": "~3.3|~4.0", "symfony/http-kernel": "~3.3|~4.0", "symfony/polyfill-php70": "~1.0" @@ -28,6 +28,7 @@ "symfony/console": "~3.2|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0", "symfony/dom-crawler": "~2.8|~3.0|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", "symfony/form": "^2.8.18|^3.2.5|~4.0", "symfony/framework-bundle": "^3.2.8|~4.0", "symfony/http-foundation": "~2.8|~3.0|~4.0", @@ -44,7 +45,8 @@ "twig/twig": "~1.34|~2.4" }, "conflict": { - "symfony/var-dumper": "<3.3" + "symfony/var-dumper": "<3.3", + "symfony/event-dispatcher": "<3.3" }, "suggest": { "symfony/security-acl": "For using the ACL functionality of this bundle" diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php index 7bad47a5bed0..fde2a624cf0e 100644 --- a/src/Symfony/Component/Security/Http/Firewall.php +++ b/src/Symfony/Component/Security/Http/Firewall.php @@ -64,14 +64,7 @@ public function onKernelRequest(GetResponseEvent $event) $exceptionListener->register($this->dispatcher); } - // initiate the listener chain - foreach ($listeners as $listener) { - $listener->handle($event); - - if ($event->hasResponse()) { - break; - } - } + return $this->handleRequest($event, $listeners); } public function onKernelFinishRequest(FinishRequestEvent $event) @@ -94,4 +87,15 @@ public static function getSubscribedEvents() KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest', ); } + + protected function handleRequest(GetResponseEvent $event, $listeners) + { + foreach ($listeners as $listener) { + $listener->handle($event); + + if ($event->hasResponse()) { + break; + } + } + } }