Give info about called security listeners in profiler

This commit is contained in:
Robin Chalas 2017-06-02 16:08:16 +02:00
parent cc2363fa6c
commit 369f19fcfd
11 changed files with 317 additions and 14 deletions

View File

@ -5,6 +5,7 @@ CHANGELOG
----- -----
* [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array` * [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array`
* added info about called security listeners in profiler
3.3.0 3.3.0
----- -----

View File

@ -19,6 +19,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Security\Core\Role\RoleInterface; use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
@ -39,6 +40,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
private $logoutUrlGenerator; private $logoutUrlGenerator;
private $accessDecisionManager; private $accessDecisionManager;
private $firewallMap; private $firewallMap;
private $firewall;
private $hasVarDumper; private $hasVarDumper;
/** /**
@ -49,14 +51,16 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
* @param LogoutUrlGenerator|null $logoutUrlGenerator * @param LogoutUrlGenerator|null $logoutUrlGenerator
* @param AccessDecisionManagerInterface|null $accessDecisionManager * @param AccessDecisionManagerInterface|null $accessDecisionManager
* @param FirewallMapInterface|null $firewallMap * @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->tokenStorage = $tokenStorage;
$this->roleHierarchy = $roleHierarchy; $this->roleHierarchy = $roleHierarchy;
$this->logoutUrlGenerator = $logoutUrlGenerator; $this->logoutUrlGenerator = $logoutUrlGenerator;
$this->accessDecisionManager = $accessDecisionManager; $this->accessDecisionManager = $accessDecisionManager;
$this->firewallMap = $firewallMap; $this->firewallMap = $firewallMap;
$this->firewall = $firewall;
$this->hasVarDumper = class_exists(ClassStub::class); $this->hasVarDumper = class_exists(ClassStub::class);
} }
@ -167,6 +171,12 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
); );
} }
} }
// collect firewall listeners information
$this->data['listeners'] = array();
if ($this->firewall) {
$this->data['listeners'] = $this->firewall->getWrappedListeners();
}
} }
public function lateCollect() public function lateCollect()
@ -305,6 +315,11 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
return $this->data['firewall']; return $this->data['firewall'];
} }
public function getListeners()
{
return $this->data['listeners'];
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -0,0 +1,43 @@
<?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\Debug;
use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* Firewall collecting called listeners.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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;
}
}
}
}

View File

@ -0,0 +1,76 @@
<?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\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 <robin.chalas@gmail.com>
*/
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,
);
}
}

View File

@ -14,6 +14,7 @@
<argument type="service" id="security.logout_url_generator" /> <argument type="service" id="security.logout_url_generator" />
<argument type="service" id="security.access.decision_manager" /> <argument type="service" id="security.access.decision_manager" />
<argument type="service" id="security.firewall.map" /> <argument type="service" id="security.firewall.map" />
<argument type="service" id="debug.security.firewall" on-invalid="null" />
</service> </service>
</services> </services>
</container> </container>

View File

@ -10,5 +10,14 @@
<service id="debug.security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager" decorates="security.access.decision_manager"> <service id="debug.security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager" decorates="security.access.decision_manager">
<argument type="service" id="debug.security.access.decision_manager.inner" /> <argument type="service" id="debug.security.access.decision_manager.inner" />
</service> </service>
<service id="debug.security.firewall" class="Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener">
<tag name="kernel.event_subscriber" />
<argument type="service" id="security.firewall.map" />
<argument type="service" id="event_dispatcher" />
<argument type="service" id="security.logout_url_generator" />
</service>
<service id="security.firewall" alias="debug.security.firewall" public="true" />
</services> </services>
</container> </container>

View File

@ -150,6 +150,8 @@
</div> </div>
{% if collector.firewall.security_enabled %} {% if collector.firewall.security_enabled %}
<h4>Configuration</h4>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -188,6 +190,46 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h4>Listeners</h4>
{% if collector.listeners|default([]) is empty %}
<div class="empty">
<p>No security listeners have been recorded. Check that debugging is enabled in the kernel.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Listener</th>
<th>Duration</th>
<th>Response</th>
</tr>
</thead>
{% set previous_event = (collector.listeners|first) %}
{% for listener in collector.listeners %}
{% if loop.first or listener != previous_event %}
{% if not loop.first %}
</tbody>
{% endif %}
<tbody>
{% set previous_event = listener %}
{% endif %}
<tr>
<td class="font-normal">{{ profiler_dump(listener.stub) }}</td>
<td class="no-wrap">{{ '%0.2f'|format(listener.time * 1000) }} ms</td>
<td class="font-normal">{{ listener.response ? profiler_dump(listener.response) : '(none)' }}</td>
</tr>
{% if loop.last %}
</tbody>
{% endif %}
{% endfor %}
</table>
{% endif %}
{% endif %} {% endif %}
{% elseif collector.enabled %} {% elseif collector.enabled %}
<div class="empty"> <div class="empty">

View File

@ -13,13 +13,19 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DataCollector;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector;
use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap; 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\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy; 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\FirewallMapInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
class SecurityDataCollectorTest extends TestCase class SecurityDataCollectorTest extends TestCase
{ {
@ -89,7 +95,7 @@ class SecurityDataCollectorTest extends TestCase
->with($request) ->with($request)
->willReturn($firewallConfig); ->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->collect($request, $this->getResponse());
$collector->lateCollect(); $collector->lateCollect();
$collected = $collector->getFirewall(); $collected = $collector->getFirewall();
@ -124,7 +130,7 @@ class SecurityDataCollectorTest extends TestCase
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->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); $collector->collect($request, $response);
$this->assertNull($collector->getFirewall()); $this->assertNull($collector->getFirewall());
@ -134,11 +140,50 @@ class SecurityDataCollectorTest extends TestCase
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->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); $collector->collect($request, $response);
$this->assertNull($collector->getFirewall()); $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() public function provideRoles()
{ {
return array( return array(

View File

@ -0,0 +1,65 @@
<?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\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']);
}
}

View File

@ -17,7 +17,7 @@
], ],
"require": { "require": {
"php": ">=5.5.9", "php": ">=5.5.9",
"symfony/security": "~3.3|~4.0", "symfony/security": "~3.4|~4.0",
"symfony/dependency-injection": "~3.3|~4.0", "symfony/dependency-injection": "~3.3|~4.0",
"symfony/http-kernel": "~3.3|~4.0", "symfony/http-kernel": "~3.3|~4.0",
"symfony/polyfill-php70": "~1.0" "symfony/polyfill-php70": "~1.0"
@ -28,6 +28,7 @@
"symfony/console": "~3.2|~4.0", "symfony/console": "~3.2|~4.0",
"symfony/css-selector": "~2.8|~3.0|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0",
"symfony/dom-crawler": "~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/form": "^2.8.18|^3.2.5|~4.0",
"symfony/framework-bundle": "^3.2.8|~4.0", "symfony/framework-bundle": "^3.2.8|~4.0",
"symfony/http-foundation": "~2.8|~3.0|~4.0", "symfony/http-foundation": "~2.8|~3.0|~4.0",
@ -44,7 +45,8 @@
"twig/twig": "~1.34|~2.4" "twig/twig": "~1.34|~2.4"
}, },
"conflict": { "conflict": {
"symfony/var-dumper": "<3.3" "symfony/var-dumper": "<3.3",
"symfony/event-dispatcher": "<3.3"
}, },
"suggest": { "suggest": {
"symfony/security-acl": "For using the ACL functionality of this bundle" "symfony/security-acl": "For using the ACL functionality of this bundle"

View File

@ -64,14 +64,7 @@ class Firewall implements EventSubscriberInterface
$exceptionListener->register($this->dispatcher); $exceptionListener->register($this->dispatcher);
} }
// initiate the listener chain return $this->handleRequest($event, $listeners);
foreach ($listeners as $listener) {
$listener->handle($event);
if ($event->hasResponse()) {
break;
}
}
} }
public function onKernelFinishRequest(FinishRequestEvent $event) public function onKernelFinishRequest(FinishRequestEvent $event)
@ -94,4 +87,15 @@ class Firewall implements EventSubscriberInterface
KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest', KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest',
); );
} }
protected function handleRequest(GetResponseEvent $event, $listeners)
{
foreach ($listeners as $listener) {
$listener->handle($event);
if ($event->hasResponse()) {
break;
}
}
}
} }