Skip to content

Commit

Permalink
Add voter individual decisions to profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
l-vo committed Jul 14, 2018
1 parent eb112a5 commit f3157da
Show file tree
Hide file tree
Showing 12 changed files with 675 additions and 57 deletions.
1 change: 1 addition & 0 deletions UPGRADE-4.2.md
Expand Up @@ -104,6 +104,7 @@ SecurityBundle
`security.authentication.trust_resolver.rememberme_class` parameters to define
the token classes is deprecated. To use
custom tokens extend the existing AnonymousToken and RememberMeToken.
* `SecurityDataCollector::getVoters` is deprecated. Use the data returned by `SecurityDataCollector::getVoterDetails` instead.

Serializer
----------
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Expand Up @@ -11,6 +11,8 @@ CHANGELOG
or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`.
* Added `Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass`
* Added `json_login_ldap` authentication provider to use LDAP authentication with a REST API.
* Using `SecurityDataCollector::getVoters()` is deprecated
* Added individual voter decisions to the profiler

4.1.0
-----
Expand Down
Expand Up @@ -13,6 +13,7 @@

use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -136,15 +137,38 @@ public function collect(Request $request, Response $response, \Exception $except

// collect voters and access decision manager information
if ($this->accessDecisionManager instanceof TraceableAccessDecisionManager) {
$this->data['access_decision_log'] = $this->accessDecisionManager->getDecisionLog();
$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();

foreach ($this->accessDecisionManager->getVoters() as $voter) {
$this->data['voters'][] = $this->hasVarDumper ? new ClassStub(get_class($voter)) : get_class($voter);
$decisionLog = $this->accessDecisionManager->getDecisionLog();

// Voter constants
$reflectionClass = new \ReflectionClass(VoterInterface::class);
// Allows to get the access constant name from the vote log
$voterConstants = array_flip($reflectionClass->getConstants());

$voterDetails = array();
$voters = array();
foreach ($decisionLog as $key => $log) {
$voterDetails[$key] = array();
foreach ($log['voterDetails'] as $voterClass => $voterVote) {
$classData = $this->hasVarDumper ? new ClassStub($voterClass) : $voterClass;
$voterDetails[$key][] = array(
'class' => $classData,
'vote' => $voterVote,
);

if (!in_array($classData, $voters)) {
$voters[] = $classData;
}
}
}

$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();
$this->data['voter_details'] = $voterDetails;
$this->data['access_decision_log'] = $decisionLog;
$this->data['voters'] = $voters;
} else {
$this->data['access_decision_log'] = array();
$this->data['voter_strategy'] = 'unknown';
$this->data['voter_details'] = array();
$this->data['voters'] = array();
}

Expand Down Expand Up @@ -309,9 +333,13 @@ public function getLogoutUrl()
* Returns the FQCN of the security voters enabled in the application.
*
* @return string[]
*
* @deprecated deprecated since Symfony 4.2. Use the data returned by {@link getVoterDetails()} instead
*/
public function getVoters()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2. Use the data returned by getVoterDetails() instead', __METHOD__), E_USER_DEPRECATED);

return $this->data['voters'];
}

Expand All @@ -335,6 +363,16 @@ public function getAccessDecisionLog()
return $this->data['access_decision_log'];
}

/**
* Returns the log of the votes processed in the access decision manager.
*
* @return Data|array
*/
public function getVoterDetails(): iterable
{
return $this->data['voter_details'];
}

/**
* Returns the configuration of the current firewall context.
*
Expand Down
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

/**
Expand All @@ -41,16 +43,33 @@ public function process(ContainerBuilder $container)
throw new LogicException('No security voters found. You need to tag at least one with "security.voter".');
}

$debug = $container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug');

$decisionManagerVoters = array();
foreach ($voters as $voter) {
$definition = $container->getDefinition((string) $voter);
$voterServiceId = (string) $voter;
$definition = $container->getDefinition($voterServiceId);

$class = $container->getParameterBag()->resolveValue($definition->getClass());

if (!is_a($class, VoterInterface::class, true)) {
throw new LogicException(sprintf('%s must implement the %s when used as a voter.', $class, VoterInterface::class));
}

if ($debug) {
// Decorate original voters with TraceableVoter
$debugVoterServiceId = 'debug.'.$voterServiceId;
$container
->register($debugVoterServiceId, TraceableVoter::class)
->setDecoratedService($voterServiceId)
->addArgument(new Reference($debugVoterServiceId.'.inner'))
->setPublic(false);
}

$decisionManagerVoters[] = $voter;
}

$adm = $container->getDefinition('security.access.decision_manager');
$adm->replaceArgument(0, new IteratorArgument($voters));
$adm->replaceArgument(0, new IteratorArgument($decisionManagerVoters));
}
}
Expand Up @@ -258,8 +258,8 @@
</div>
{% endif %}

{% if collector.voters|default([]) is not empty %}
<h2>Security Voters <small>({{ collector.voters|length }})</small></h2>
{% if collector.accessDecisionLog|default([]) is not empty %}
<h2>Access decision log</h2>

<div class="metrics">
<div class="metric">
Expand All @@ -268,47 +268,22 @@
</div>
</div>

<table class="voters">
<thead>
<tr>
<th>#</th>
<th>Voter class</th>
</tr>
</thead>
{% for key, decision in collector.accessDecisionLog %}
<table class="decision-log">
<col style="width: 120px">
<col style="width: 25%">
<col style="width: 60%">

<tbody>
{% for voter in collector.voters %}
<thead>
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="font-normal">{{ profiler_dump(voter) }}</td>
<th>Result</th>
<th>Attributes</th>
<th>Object</th>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

{% if collector.accessDecisionLog|default([]) is not empty %}
<h2>Access decision log</h2>
</thead>

<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 %}
<tbody>
<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>'
Expand All @@ -331,8 +306,44 @@
</td>
<td>{{ profiler_dump(decision.seek('object')) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</tbody>
</table>
{% if collector.voterDetails[key] is not empty %}
{% set voter_details_id = 'voter-details-' ~ loop.index %}
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ voter_details_id }}" data-toggle-alt-content="Hide voter details">Show voter details</a>
<div id="{{ voter_details_id }}" class="sf-toggle-content sf-toggle-hidden">
<table class="voters">
<thead>
<tr>
<th>#</th>
<th>Voter class</th>
<th>Vote result</th>
</tr>
</thead>
<tbody>
{% for voter_detail in collector.voterDetails[key] %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="font-normal">{{ profiler_dump(voter_detail['class']) }}</td>
<td class="font-normal text-small">
{% if voter_detail['vote'] is null %}
-
{% elseif voter_detail['vote'] == 1 %}
ACCESS GRANTED
{% elseif voter_detail['vote'] == 0 %}
ACCESS ABSTAIN
{% elseif voter_detail['vote'] == -1 %}
ACCESS DENIED
{% else %}
unknown
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}

0 comments on commit f3157da

Please sign in to comment.