8000 [Security][SecurityBundle] Add voter individual decisions to profiler · symfony/symfony@42b0de2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 42b0de2

Browse files
committed
[Security][SecurityBundle] Add voter individual decisions to profiler
1 parent 5e90b4a commit 42b0de2

File tree

18 files changed

+833
-22
lines changed

18 files changed

+833
-22
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ CHANGELOG
1616
* Deprecated the `simple_form` and `simple_preauth` authentication listeners, use Guard instead.
1717
* Deprecated the `SimpleFormFactory` and `SimplePreAuthenticationFactory` classes, use Guard instead.
1818
* Added `port` in access_control
19+
* Added individual voter decisions to the profiler
1920

2021
4.1.0
2122
-----

src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2121
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
2222
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
23+
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
2324
use Symfony\Component\Security\Core\Role\Role;
2425
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
2526
use Symfony\Component\Security\Core\Role\SwitchUserRole;
@@ -136,12 +137,33 @@ public function collect(Request $request, Response $response, \Exception $except
136137

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

142142
foreach ($this->accessDecisionManager->getVoters() as $voter) {
143+
if ($voter instanceof TraceableVoter) {
144+
$voter = $voter->getDecoratedVoter();
145+
}
146+
143147
$this->data['voters'][] = $this->hasVarDumper ? new ClassStub(\get_class($voter)) : \get_class($voter);
144148
}
149+
150+
// collect voter details
151+
$decisionLog = $this->accessDecisionManager->getDecisionLog();
152+
foreach ($decisionLog as $key => $log) {
153+
$decisionLog[$key]['voter_details'] = array();
154+
foreach ($log['voterDetails'] as $voterDetail) {
155+
$voterClass = \get_class($voterDetail['voter']);
156+
$classData = $this->hasVarDumper ? new ClassStub($voterClass) : $voterClass;
157+
$decisionLog[$key]['voter_details'][] = array(
158+
'class' => $classData,
159+
'attributes' => $voterDetail['attributes'], // Only displayed for unanimous strategy
160+
'vote' => $voterDetail['vote'],
161+
);
162+
}
163+
unset($decisionLog[$key]['voterDetails']);
164+
}
165+
166+
$this->data['access_decision_log'] = $decisionLog;
145167
} else {
146168
$this->data['access_decision_log'] = array();
147169
$this->data['voter_strategy'] = 'unknown';

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
1818
use Symfony\Component\DependencyInjection\Exception\LogicException;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
1921
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
2022

2123
/**
@@ -41,13 +43,27 @@ public function process(ContainerBuilder $container)
4143
throw new LogicException('No security voters found. You need to tag at least one with "security.voter".');
4244
}
4345

46+
$debug = $container->getParameter('kernel.debug');
47+
4448
foreach ($voters as $voter) {
45-
$definition = $container->getDefinition((string) $voter);
49+
$voterServiceId = (string) $voter;
50+
$definition = $container->getDefinition($voterServiceId);
51+
4652
$class = $container->getParameterBag()->resolveValue($definition->getClass());
4753

4854
if (!is_a($class, VoterInterface::class, true)) {
4955
throw new LogicException(sprintf('%s must implement the %s when used as a voter.', $class, VoterInterface::class));
5056
}
57+
58+
if ($debug) {
59+
// Decorate original voters with TraceableVoter
60+
$debugVoterServiceId = '.debug.security.voter.'.$voterServiceId;
61+
$container
62+
->register($debugVoterServiceId, TraceableVoter::class)
63+
->setDecoratedService($voterServiceId)
64+
->addArgument(new Reference($debugVoterServiceId.'.inner'))
65+
->addArgument(new Reference('event_dispatcher'));
66+
}
5167
}
5268

5369
$adm = $container->getDefinition('security.access.decision_manager');
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
16+
use Symfony\Component\Security\Core\Event\VoteEvent;
17+
18+
/**
19+
* Listen to vote events from access decision manager.
20+
*
21+
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
class VoteListener implements EventSubscriberInterface
26+
{
27+
private $traceableAccessDecisionManager;
28+
29+
public function __construct(TraceableAccessDecisionManager $traceableAccessDecisionManager)
30+
{
31+
$this->traceableAccessDecisionManager = $traceableAccessDecisionManager;
32+
}
33+
34+
/**
35+
* Event dispatched by a voter during access manager decision.
36+
*
37+
* @param VoteEvent $event event with voter data
38+
*/
39+
public function onVoterVote(VoteEvent $event)
40+
{
41+
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVote());
42+
}
43+
44+
public static function getSubscribedEvents()
45+
{
46+
return array('debug.security.authorization.vote' => 'onVoterVote');
47+
}
48+
}

src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
<argument type="service" id="debug.security.access.decision_manager.inner" />
1212
</service>
1313

14+
<service id="debug.security.voter.vote_listener" class="Symfony\Bundle\SecurityBundle\EventListener\VoteListener">
15+
<tag name="kernel.event_subscriber" />
16+
<argument type="service" id="debug.security.access.decision_manager" />
17+
</service>
18+
1419
<service id="debug.security.firewall" class="Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener">
1520
<tag name="kernel.event_subscriber" />
1621
<argument type="service" id="security.firewall.map" />

src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@
307307

308308
<tbody>
309309
{% for decision in collector.accessDecisionLog %}
310-
<tr>
310+
<tr class="voter_result">
311311
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
312312
<td class="font-normal">
313313
{{ decision.result
@@ -331,6 +331,40 @@
331331
</td>
332332
<td>{{ profiler_dump(decision.seek('object')) }}</td>
333333
</tr>
334+
<tr class="voter_details">
335+
<td></td>
336+
<td colspan="3">
337+
{% if decision.voter_details is not empty %}
338+
{% set voter_details_id = 'voter-details-' ~ loop.index %}
339+
<div id="{{ voter_details_id }}" class="sf-toggle-content sf-toggle-hidden">
340+
<table>
341+
<tbody>
342+
{% for voter_detail in decision.voter_details %}
343+
<tr>
344+
<td class="font-normal">{{ profiler_dump(voter_detail['class']) }}</td>
345+
{% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %}
346+
<td class="font-normal text-small">attribute {{ voter_detail['attributes'][0] }}</td>
347+
{% endif %}
348+
<td class="font-normal text-small">
349+
{% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
350+
ACCESS GRANTED
351+
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
352+
ACCESS ABSTAIN
353+
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
354+
ACCESS DENIED
355+
{% else %}
356+
unknown ({{ voter_detail['vote'] }})
357+
{% endif %}
358+
</td>
359+
</tr>
360+
{% endfor %}
361+
</tbody>
362+
</table>
363+
</div>
364+
<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>
365+
{% endif %}
366+
</td>
367+
</tr>
334368
{% endfor %}
335369
</tbody>
336370
</table>

src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
1818
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
1919
use Symfony\Component\EventDispatcher\EventDispatcher;
20+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2021
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
2122
use Symfony\Component\HttpKernel\HttpKernelInterface;
2223
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
2324
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
25+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
26+
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
27+
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
28+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
2429
use Symfony\Component\Security\Core\Role\Role;
2530
use Symfony\Component\Security\Core\Role\RoleHierarchy;
2631
use Symfony\Component\Security\Core\Role\SwitchUserRole;
@@ -221,6 +226,137 @@ public function testGetListeners()
221226
$this->addToAssertionCount(1);
222227
}
223228

229+
public function providerCollectDecisionLog(): \Generator
230+
{
231+
$voter1 = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
232+
$voter2 = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
233+
234+
$eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass();
235+
$decoratedVoter1 = new TraceableVoter($voter1, $eventDispatcher);
236+
$decoratedVoter2 = new TraceableVoter($voter2, $eventDispatcher);
237+
238+
yield array(
239+
AccessDecisionManager::STRATEGY_AFFIRMATIVE,
240+
array(array(
241+
'attributes' => array('view'),
242+
'object' => new \stdClass(),
243+
'result' => true,
244+
'voterDetails' => array(
245+
array('voter' => $voter1, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
246+
array('voter' => $voter2, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
247+
),
248+
)),
249+
array($decoratedVoter1, $decoratedVoter1),
250+
array(\get_class($voter1), \get_class($voter2)),
251+
array(array(
252+
'attributes' => array('view'),
253+
'object' => new \stdClass(),
254+
'result' => true,
255+
'voter_details' => array(
256+
array('class' => \get_class($voter1), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
257+
array('class' => \get_class($voter2), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
258+
),
259+
)),
260+
);
261+
262+
yield array(
263+
AccessDecisionManager::STRATEGY_UNANIMOUS,
264+
array(
265+
array(
266+
'attributes' => array('view', 'edit'),
267+
'object' => new \stdClass(),
268+
'result' => false,
269+
'voterDetails' => array(
270+
array('voter' => $voter1, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_DENIED),
271+
array('voter' => $voter1, 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_DENIED),
272+
array('voter' => $voter2, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_GRANTED),
273+
array('voter' => $voter2, 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_GRANTED),
274+
),
275+
),
276+
array(
277+
'attributes' => array('update'),
278+
'object' => new \stdClass(),
279+
'result' => true,
280+
'voterDetails' => array(
281+
array('voter' => $voter1, 'attributes' => array(&# F438 39;update'), 'vote' => VoterInterface::ACCESS_GRANTED),
282+
array('voter' => $voter2, 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
283+
),
284+
),
285+
),
286+
array($decoratedVoter1, $decoratedVoter1),
287+
array(\get_class($voter1), \get_class($voter2)),
288+
array(
289+
array(
290+
'attributes' => array('view', 'edit'),
291+
'object' => new \stdClass(),
292+
'result' => false,
293+
'voter_details' => array(
294+
array('class' => \get_class($voter1), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_DENIED),
295+
array('class' => \get_class($voter1), 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_DENIED),
296+
array('class' => \get_class($voter2), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_GRANTED),
297+
array('class' => \get_class($voter2), 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_GRANTED),
298+
),
299+
),
300+
array(
301+
'attributes' => array('update'),
302+
'object' => new \stdClass(),
303+
'result' => true,
304+
'voter_details' => array(
305+
array('class' => \get_class($voter1), 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
306+
array('class' => \get_class($voter2), 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
307+
),
308+
),
309+
),
310+
);
311+
}
312+
313+
/**
314+
* Test the returned data when AccessDecisionManager is a TraceableAccessDecisionManager.
315+
*
316+
* @param string $strategy strategy returned by the AccessDecisionManager
317+
* @param array $voters voters returned by AccessDecisionManager
318+
* @param array $decisionLog log of the votes and final decisions from AccessDecisionManager
319+
* @param array $expectedVoterClasses expected voter classes returned by the collector
320+
* @param array $expectedDecisionLog expected decision log returned by the collector
321+
*
322+
* @dataProvider providerCollectDecisionLog
323+
*/
324+
public function testCollectDecisionLog(string $strategy, array $decisionLog, array $voters, array $expectedVoterClasses, array $expectedDecisionLog): void
325+
{
326+
$accessDecisionManager = $this
327+
->getMockBuilder(TraceableAccessDecisionManager::class)
328+
->disableOriginalConstructor()
329+
->setMethods(array('getStrategy', 'getVoters', 'getDecisionLog'))
330+
->getMock();
331+
332+
$accessDecisionManager
333+
->expects($this->any())
334+
->method('getStrategy')
335+
->willReturn($strategy);
336+
337+
$accessDecisionManager
338+
->expects($this->any())
339+
->method('getVoters')
340+
->willReturn($voters);
341+
342+
$accessDecisionManager
343+
->expects($this->any())
344+
->method('getDecisionLog')
345+
->willReturn($decisionLog);
346+
347+
$dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager);
348+
$dataCollector->collect($this->getRequest(), $this->getResponse());
349+
350+
$this->assertEquals($dataCollector->getAccessDecisionLog(), $expectedDecisionLog, 'Wrong value returned by getAccessDecisionLog');
351+
352+
$this->assertSame(
353+
array_map(function ($classStub) { return (string) $classStub; }, $dataCollector->getVoters()),
354+
$expectedVoterClasses,
355+
'Wrong value returned by getVoters'
356+
);
357+
$this->assertSame($dataCollector->getVoterStrategy(), $strategy, 'Wrong value returned by getVoterStrategy');
358+
}
359+
224360
public function provideRoles()
225361
{
226362
return array(

0 commit comments

Comments
 (0)
0