feature #17887 Show more information in the security profiler (javiereguiluz)

This PR was squashed before being merged into the 3.1-dev branch (closes #17887).

Discussion
----------

Show more information in the security profiler

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #17856
| License       | MIT
| Doc PR        | -

This is an early prototype to explore the feature of displaying more information in the security panel. Example:

![profiler_security](https://cloud.githubusercontent.com/assets/73419/13221929/0235fc46-d97e-11e5-981a-249b7148f3a6.png)

Commits
-------

b12152d Show more information in the security profiler
This commit is contained in:
Fabien Potencier 2016-03-04 08:25:19 +01:00
commit 5ebeccafa5
8 changed files with 266 additions and 4 deletions

View File

@ -18,6 +18,8 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager;
/**
* SecurityDataCollector.
@ -29,19 +31,22 @@ class SecurityDataCollector extends DataCollector
private $tokenStorage;
private $roleHierarchy;
private $logoutUrlGenerator;
private $accessDecisionManager;
/**
* Constructor.
*
* @param TokenStorageInterface|null $tokenStorage
* @param RoleHierarchyInterface|null $roleHierarchy
* @param LogoutUrlGenerator|null $logoutUrlGenerator
* @param TokenStorageInterface|null $tokenStorage
* @param RoleHierarchyInterface|null $roleHierarchy
* @param LogoutUrlGenerator|null $logoutUrlGenerator
* @param AccessDecisionManagerInterface|null $accessDecisionManager
*/
public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null)
public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->roleHierarchy = $roleHierarchy;
$this->logoutUrlGenerator = $logoutUrlGenerator;
$this->accessDecisionManager = $accessDecisionManager;
}
/**
@ -104,6 +109,20 @@ class SecurityDataCollector extends DataCollector
'supports_role_hierarchy' => null !== $this->roleHierarchy,
);
}
// collect voters and access decision manager information
if ($this->accessDecisionManager instanceof DebugAccessDecisionManager) {
$this->data['access_decision_log'] = $this->accessDecisionManager->getDecisionLog();
$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();
foreach ($this->accessDecisionManager->getVoters() as $voter) {
$this->data['voters'][] = get_class($voter);
}
} else {
$this->data['access_decision_log'] = array();
$this->data['voter_strategy'] = 'unknown';
$this->data['voters'] = array();
}
}
/**
@ -187,6 +206,36 @@ class SecurityDataCollector extends DataCollector
return $this->data['logout_url'];
}
/**
* Returns the FQCN of the security voters enabled in the application.
*
* @return string[]
*/
public function getVoters()
{
return $this->data['voters'];
}
/**
* Returns the strategy configured for the security voters.
*
* @return string
*/
public function getVoterStrategy()
{
return $this->data['voter_strategy'];
}
/**
* Returns the log of the security decisions made by the access decision manager.
*
* @return array
*/
public function getAccessDecisionLog()
{
return $this->data['access_decision_log'];
}
/**
* {@inheritdoc}
*/

View File

@ -46,5 +46,9 @@ class AddSecurityVotersPass implements CompilerPassInterface
}
$container->getDefinition('security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters)));
if ($container->hasDefinition('debug.security.access.decision_manager')) {
$container->getDefinition('debug.security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters)));
}
}
}

View File

@ -97,6 +97,13 @@ class SecurityExtension extends Extension
$this->aclLoad($config['acl'], $container);
}
if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) {
$loader->load('security_debug.xml');
$definition = $container->findDefinition('security.authorization_checker');
$definition->replaceArgument(2, new Reference('debug.security.access.decision_manager'));
}
// add some required classes for compilation
$this->addClassesToCompile(array(
'Symfony\Component\Security\Http\Firewall',

View File

@ -10,6 +10,7 @@
<argument type="service" id="security.token_storage" on-invalid="ignore" />
<argument type="service" id="security.role_hierarchy" />
<argument type="service" id="security.logout_url_generator" />
<argument type="service" id="debug.security.access.decision_manager" />
</service>
</services>
</container>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="debug.security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager" decorates="security.access.decision_manager" public="false">
<argument type="service" id="debug.security.access.decision_manager.inner" />
</service>
</services>
</container>

View File

@ -119,4 +119,69 @@
<p>The security component is disabled.</p>
</div>
{% endif %}
{% if collector.voters|default([]) is not empty %}
<h2>Security Voters <small>({{ collector.voters|length }})</small></h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.voterStrategy|default('unknown') }}</span>
<span class="label">Strategy</span>
</div>
</div>
<table class="voters">
<thead>
<tr>
<th>#</th>
<th>Voter class</th>
</tr>
</thead>
<tbody>
{% for voter in collector.voters %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="font-normal">{{ voter }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if collector.accessDecisionLog|default([]) is not empty %}
<h2>Access decision log</h2>
<table class="decision-log">
<col style="width: 30px">
<col style="width: 120px">
<col style="width: 25%">
<col style="width: 60%">
<thead>
<tr>
<th>#</th>
<th>Result</th>
<th>Attributes</th>
<th>Object</th>
</tr>
</thead>
<tbody>
{% for decision in collector.accessDecisionLog %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="font-normal">
{{ decision.result
? '<span class="label status-success same-width">GRANTED</span>'
: '<span class="label status-error same-width">DENIED</span>'
}}
</td>
<td>{{ decision.attributes|length == 1 ? decision.attributes|first : profiler_dump(decision.attributes) }}</td>
<td>{{ profiler_dump(decision.object) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -235,6 +235,10 @@ table tbody ul {
padding: 3px 7px;
white-space: nowrap;
}
.label.same-width {
min-width: 70px;
text-align: center;
}
.label.status-success { background: {{ colors.success|raw }}; color: #FFF; }
.label.status-warning { background: {{ colors.warning|raw }}; color: #FFF; }
.label.status-error { background: {{ colors.error|raw }}; color: #FFF; }

View File

@ -0,0 +1,120 @@
<?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\Component\Security\Core\Authorization;
use Doctrine\Common\Util\ClassUtils;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* Decorates the original AccessDecisionManager class to log information
* about the security voters and the decisions made by them.
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*
* @internal
*/
class DebugAccessDecisionManager implements AccessDecisionManagerInterface
{
private $manager;
private $strategy;
private $voters;
private $decisionLog = array();
public function __construct(AccessDecisionManager $manager)
{
$this->manager = $manager;
// The strategy is stored in a private property of the decorated service
$reflection = new \ReflectionProperty($manager, 'strategy');
$reflection->setAccessible(true);
$this->strategy = $reflection->getValue($manager);
}
/**
* {@inheritdoc}
*/
public function decide(TokenInterface $token, array $attributes, $object = null)
{
$result = $this->manager->decide($token, $attributes, $object);
$this->decisionLog[] = array(
'attributes' => $attributes,
'object' => $this->getStringRepresentation($object),
'result' => $result,
);
return $result;
}
/**
* {@inheritdoc}
*/
public function setVoters(array $voters)
{
$this->voters = $voters;
}
/**
* @return string
*/
public function getStrategy()
{
// The $strategy property is misleading because it stores the name of its
// method (e.g. 'decideAffirmative') instead of the original strategy name
// (e.g. 'affirmative')
return strtolower(substr($this->strategy, 6));
}
/**
* @return array
*/
public function getVoters()
{
return $this->voters;
}
/**
* @return array
*/
public function getDecisionLog()
{
return $this->decisionLog;
}
/**
* @param mixed $object
*
* @return string
*/
private function getStringRepresentation($object)
{
if (null === $object) {
return 'NULL';
}
if (!is_object($object)) {
return sprintf('%s (%s)', gettype($object), $object);
}
$objectClass = class_exists('Doctrine\Common\Util\ClassUtils') ? ClassUtils::getClass($object) : get_class($object);
if (method_exists($object, 'getId')) {
$objectAsString = sprintf('ID: %s', $object->getId());
} elseif (method_exists($object, '__toString')) {
$objectAsString = (string) $object;
} else {
$objectAsString = sprintf('object hash: %s', spl_object_hash($object));
}
return sprintf('%s (%s)', $objectClass, $objectAsString);
}
}