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:
commit
5ebeccafa5
@ -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}
|
||||
*/
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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 %}
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user