Skip to content

Commit

Permalink
feature #17887 Show more information in the security profiler (javier…
Browse files Browse the repository at this point in the history
…eguiluz)

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
  • Loading branch information
fabpot committed Mar 4, 2016
2 parents 0e5ac97 + b12152d commit 5ebecca
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 4 deletions.
Expand Up @@ -18,6 +18,8 @@
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.
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -104,6 +109,20 @@ public function collect(Request $request, Response $response, \Exception $except
'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();
}
}

/**
Expand Down Expand Up @@ -187,6 +206,36 @@ public function getLogoutUrl()
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}
*/
Expand Down
Expand Up @@ -46,5 +46,9 @@ public function process(ContainerBuilder $container)
}

$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)));
}
}
}
Expand Up @@ -97,6 +97,13 @@ public function load(array $configs, ContainerBuilder $container)
$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',
Expand Down
Expand Up @@ -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>
Expand Up @@ -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 %}
Expand Up @@ -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; }
Expand Down
@@ -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);
}
}

0 comments on commit 5ebecca

Please sign in to comment.