Skip to content

Commit

Permalink
feature #23105 [SecurityBundle][Profiler] Give info about called secu…
Browse files Browse the repository at this point in the history
…rity 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
-------

369f19f Give info about called security listeners in profiler
  • Loading branch information
fabpot committed Jun 13, 2017
2 parents 17d23a7 + 369f19f commit 0300412
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 14 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* [BC BREAK] `FirewallContext::getListeners()` now returns `\Traversable|array`
* added info about called security listeners in profiler

3.3.0
-----
Expand Down
Expand Up @@ -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;
Expand All @@ -39,6 +40,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
private $logoutUrlGenerator;
private $accessDecisionManager;
private $firewallMap;
private $firewall;
private $hasVarDumper;

/**
Expand All @@ -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);
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -305,6 +315,11 @@ public function getFirewall()
return $this->data['firewall'];
}

public function getListeners()
{
return $this->data['listeners'];
}

/**
* {@inheritdoc}
*/
Expand Down
@@ -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 changes: 76 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php
@@ -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,
);
}
}
Expand Up @@ -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>
Expand Up @@ -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>
Expand Up @@ -150,6 +150,8 @@
</div>

{% if collector.firewall.security_enabled %}
<h4>Configuration</h4>

<table>
<thead>
<tr>
Expand Down Expand Up @@ -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">
Expand Down
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());

Expand All @@ -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(
Expand Down

0 comments on commit 0300412

Please sign in to comment.