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`
* added info about called security listeners in profiler
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\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 @@ 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()
@ -305,6 +315,11 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
return $this->data['firewall'];
}
public function getListeners()
{
return $this->data['listeners'];
}
/**
* {@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.access.decision_manager" />
<argument type="service" id="security.firewall.map" />
<argument type="service" id="debug.security.firewall" on-invalid="null" />
</service>
</services>
</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">
<argument type="service" id="debug.security.access.decision_manager.inner" />
</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>
</container>

View File

@ -150,6 +150,8 @@
</div>
{% if collector.firewall.security_enabled %}
<h4>Configuration</h4>
<table>
<thead>
<tr>
@ -188,6 +190,46 @@
</tr>
</tbody>
</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 %}
{% elseif collector.enabled %}
<div class="empty">

View File

@ -13,13 +13,19 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DataCollector;
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 @@ class SecurityDataCollectorTest extends TestCase
->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 @@ class SecurityDataCollectorTest extends TestCase
->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 @@ class SecurityDataCollectorTest extends TestCase
->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(

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": {
"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"

View File

@ -64,14 +64,7 @@ class Firewall implements EventSubscriberInterface
$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 @@ class Firewall implements EventSubscriberInterface
KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest',
);
}
protected function handleRequest(GetResponseEvent $event, $listeners)
{
foreach ($listeners as $listener) {
$listener->handle($event);
if ($event->hasResponse()) {
break;
}
}
}
}