feature #23105 [SecurityBundle][Profiler] Give info about called security listeners in profiler (chalasr)
This PR was merged into the 3.4 branch.
Discussion
----------
[SecurityBundle][Profiler] Give info about called security listeners in profiler
| Q | A
| ------------- | ---
| Branch? | 3.4
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #11134
| License | MIT
| Doc PR | n/a
Currently the profiler gives no info about security listeners (see fixed ticket), this displays each called listener with the time spent at calling it and its response if any.
![preview](https://image.prntscr.com/image/Wx-n-Ni_RQK5JGTdTZsdGw.png)
Commits
-------
369f19fcfd
Give info about called security listeners in profiler
This commit is contained in:
commit
0300412ec1
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array`
|
||||
* added info about called security listeners in profiler
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php
Normal file
76
src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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(
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user