Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 59761f6

Browse files
committed
Add voter individual decisions to profiler
1 parent 3cfdc9e commit 59761f6

File tree

9 files changed

+648
-68
lines changed

9 files changed

+648
-68
lines changed

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

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
1515
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
16+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1617
use Symfony\Component\Security\Core\Role\Role;
1718
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
1819
use Symfony\Component\HttpFoundation\Request;
@@ -136,16 +137,31 @@ 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();
140-
$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();
141-
142-
foreach ($this->accessDecisionManager->getVoters() as $voter) {
143-
$this->data['voters'][] = $this->hasVarDumper ? new ClassStub(get_class($voter)) : get_class($voter);
140+
$decisionLog = $this->accessDecisionManager->getDecisionLog();
141+
142+
// Voter constants
143+
$reflectionClass = new \ReflectionClass(VoterInterface::class);
144+
// Allows to get the access constant name from the vote log
145+
$voterConstants = array_flip($reflectionClass->getConstants());
146+
147+
$voterDetails = array();
148+
foreach ($decisionLog as $key => $log) {
149+
$voterDetails[$key] = array();
150+
foreach ($log['voterDetails'] as $voterClass => $voterVote) {
151+
$voterDetails[$key][] = array(
152+
'class' => $this->hasVarDumper ? new ClassStub($voterClass) : $voterClass,
153+
'vote' => null === $voterVote ? '-' : ($voterConstants[$voterVote] ?? 'unknown'),
154+
);
155+
}
144156
}
157+
158+
$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();
159+
$this->data['voter_details'] = $voterDetails;
160+
$this->data['access_decision_log'] = $decisionLog;
145161
} else {
146162
$this->data['access_decision_log'] = array();
147163
$this->data['voter_strategy'] = 'unknown';
148-
$this->data['voters'] = array();
164+
$this->data['voter_details'] = array();
149165
}
150166

151167
// collect firewall context information
@@ -305,16 +321,6 @@ public function getLogoutUrl()
305321
return $this->data['logout_url'];
306322
}
307323

308-
/**
309-
* Returns the FQCN of the security voters enabled in the application.
310-
*
311-
* @return string[]
312-
*/
313-
public function getVoters()
314-
{
315-
return $this->data['voters'];
316-
}
317-
318324
/**
319325
* Returns the strategy configured for the security voters.
320326
*
@@ -335,6 +341,16 @@ public function getAccessDecisionLog()
335341
return $this->data['access_decision_log'];
336342
}
337343

344+
/**
345+
* Returns the log of the votes processed in the access decision manager.
346+
*
347+
* @return Data|array
348+
*/
349+
public function getVoterDetails(): iterable
350+
{
351+
return $this->data['voter_details'];
352+
}
353+
338354
/**
339355
* Returns the configuration of the current firewall context.
340356
*

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1717
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
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,16 +43,33 @@ 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->hasParameter('kernel.debug') && $container->getParameter('kernel.debug');
47+
48+
$decisionManagerVoters = array();
4449
foreach ($voters as $voter) {
45-
$definition = $container->getDefinition((string) $voter);
50+
$voterServiceId = (string) $voter;
51+
$definition = $container->getDefinition($voterServiceId);
52+
4653
$class = $container->getParameterBag()->resolveValue($definition->getClass());
4754

4855
if (!is_a($class, VoterInterface::class, true)) {
4956
throw new LogicException(sprintf('%s must implement the %s when used as a voter.', $class, VoterInterface::class));
5057
}
58+
59+
if ($debug) {
60+
// Decorate original voters with TraceableVoter
61+
$debugVoterServiceId = 'debug.'.$voterServiceId;
62+
$container
63+
->register($debugVoterServiceId, TraceableVoter::class)
64+
->setDecoratedService($voterServiceId)
65+
->addArgument(new Reference($debugVoterServiceId.'.inner'))
66+
->setPublic(false);
67+
}
68+
69+
$decisionManagerVoters[] = $voter;
5170
}
5271

5372
$adm = $container->getDefinition('security.access.decision_manager');
54-
$adm->replaceArgument(0, new IteratorArgument($voters));
73+
$adm->replaceArgument(0, new IteratorArgument($decisionManagerVoters));
5574
}
5675
}

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

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@
258258
</div>
259259
{% endif %}
260260

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

264264
<div class="metrics">
265265
<div class="metric">
@@ -268,47 +268,22 @@
268268
</div>
269269
</div>
270270

271-
<table class="voters">
272-
<thead>
273-
<tr>
274-
<th>#</th>
275-
<th>Voter class</th>
276-
</tr>
277-
</thead>
271+
{% for key, decision in collector.accessDecisionLog %}
272+
<table class="decision-log">
273+
<col style="width: 120px">
274+
<col style="width: 25%">
275+
<col style="width: 60%">
278276

279-
<tbody>
280-
{% for voter in collector.voters %}
277+
<thead>
281278
<tr>
282-
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
283-
<td class="font-normal">{{ profiler_dump(voter) }}</td>
279+
<th>Result</th>
280+
<th>Attributes</th>
281+
<th>Object</th>
284282
</tr>
285-
{% endfor %}
286-
</tbody>
287-
</table>
288-
{% endif %}
289-
290-
{% if collector.accessDecisionLog|default([]) is not empty %}
291-
<h2>Access decision log</h2>
283+
</thead>
292284

293-
<table class="decision-log">
294-
<col style="width: 30px">
295-
<col style="width: 120px">
296-
<col style="width: 25%">
297-
<col style="width: 60%">
298-
299-
<thead>
300-
<tr>
301-
<th>#</th>
302-
<th>Result</th>
303-
<th>Attributes</th>
304-
<th>Object</th>
305-
</tr>
306-
</thead>
307-
308-
<tbody>
309-
{% for decision in collector.accessDecisionLog %}
285+
<tbody>
310286
<tr>
311-
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
312287
<td class="font-normal">
313288
{{ decision.result
314289
? '<span class="label status-success same-width">GRANTED</span>'
@@ -331,8 +306,32 @@
331306
</td>
332307
<td>{{ profiler_dump(decision.seek('object')) }}</td>
333308
</tr>
334-
{% endfor %}
335-
</tbody>
336-
</table>
309+
</tbody>
310+
</table>
311+
{% if collector.voterDetails[key] is not empty %}
312+
{% set voter_details_id = 'voter-details-' ~ loop.index %}
313+
<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>
314+
<div id="{{ voter_details_id }}" class="sf-toggle-content sf-toggle-hidden">
315+
<table class="voters">
316+
<thead>
317+
<tr>
318+
<th>#</th>
319+
<th>Voter class</th>
320+
<th>Vote result</th>
321+
</tr>
322+
</thead>
323+
<tbody>
324+
{% for voter_detail in collector.voterDetails[key] %}
325+
<tr>
326+
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
327+
<td class="font-normal">{{ profiler_dump(voter_detail['class']) }}</td>
328+
<td class="font-normal">{{ voter_detail['vote'] }}</td>
329+
</tr>
330+
{% endfor %}
331+
</tbody>
332+
</table>
333+
</div>
334+
{% endif %}
335+
{% endfor %}
337336
{% endif %}
338337
{% endblock %}

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

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
use Symfony\Component\HttpKernel\HttpKernelInterface;
2222
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
2323
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
24+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
25+
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
26+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
2427
use Symfony\Component\Security\Core\Role\Role;
2528
use Symfony\Component\Security\Core\Role\RoleHierarchy;
2629
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
@@ -221,6 +224,148 @@ public function testGetListeners()
221224
$this->addToAssertionCount(1);
222225
}
223226

227+
/**
228+
* Test that no data is returned when AccessDecisionManager is not a TraceableAccessDecisionManager.
229+
*/
230+
public function testCollectWithAccessDecisionManagerNoTraceable(): void
231+
{
232+
$accessDecisionManager = $this->getMockBuilder(AccessDecisionManager::class)->getMock();
233+
$dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager);
234+
$dataCollector->collect($this->getRequest(), $this->getResponse());
235+
236+
$this->assertEmpty($dataCollector->getAccessDecisionLog(), 'getAccessDecisionLog should return an empty value');
237+
$this->assertEmpty($dataCollector->getVoterDetails(), 'getVoterDetails should return emptu value');
238+
$this->assertEquals($dataCollector->getVoterStrategy(), 'unknown', 'Wrong value for getVoterStrategy');
239+
}
240+
241+
/**
242+
* Data provider for testCollectWithTraceableAccessDecisionManager.
243+
*
244+
* @return array
245+
*/
246+
public function providerCollectWithTraceableAccessDecisionManager(): array
247+
{
248+
return array(
249+
array(
250+
AccessDecisionManager::STRATEGY_AFFIRMATIVE,
251+
array(array(
252+
'attributes' => array('view'),
253+
'object' => new \stdClass(),
254+
'result' => true,
255+
'voterDetails' => array(
256+
'Voter1' => VoterInterface::ACCESS_ABSTAIN,
257+
'Voter2' => VoterInterface::ACCESS_GRANTED,
258+
),
259+
)),
260+
array(array(
261+
array('class' => 'Voter1', 'vote' => 'ACCESS_ABSTAIN'),
262+
array('class' => 'Voter2', 'vote' => 'ACCESS_GRANTED'),
263+
)),
264+
),
265+
array(
266+
AccessDecisionManager::STRATEGY_AFFIRMATIVE,
267+
array(array(
268+
'attributes' => array('update'),
269+
'object' => new \stdClass(),
270+
'result' => true,
271+
'voterDetails' => array(
272+
'Voter1' => VoterInterface::ACCESS_GRANTED,
273+
'Voter2' => null,
274+
),
275+
)),
276+
array(array(
277+
array('class' => 'Voter1', 'vote' => 'ACCESS_GRANTED'),
278+
array('class' => 'Voter2', 'vote' => '-'),
279+
)),
280+
),
281+
array(
282+
AccessDecisionManager::STRATEGY_CONSENSUS,
283+
array(array(
284+
'attributes' => array('update', 'delete'),
285+
'object' => new \stdClass(),
286+
'result' => false,
287+
'voterDetails' => array(
288+
'Voter1' => 'dummy',
289+
'Voter2' => VoterInterface::ACCESS_ABSTAIN,
290+
),
291+
)),
292+
array(array(
293+
array('class' => 'Voter1', 'vote' => 'unknown'),
294+
array('class' => 'Voter2', 'vote' => 'ACCESS_ABSTAIN'),
295+
)),
296+
),
297+
array(
298+
AccessDecisionManager::STRATEGY_UNANIMOUS,
299+
array(
300+
array(
301+
'attributes' => array('view', 'edit'),
302+
'object' => new \stdClass(),
303+
'result' => false,
304+
'voterDetails' => array(
305+
'Voter1' => VoterInterface::ACCESS_DENIED,
306+
'Voter2' => VoterInterface::ACCESS_GRANTED,
307+
),
308+
),
309+
array(
310+
'attributes' => array('update'),
311+
'object' => new \stdClass(),
312+
'result' => true,
313+
'voterDetails' => array(
314+
'Voter1' => VoterInterface::ACCESS_GRANTED,
315+
'Voter2' => VoterInterface::ACCESS_GRANTED,
316+
),
317+
),
318+
),
319+
array(
320+
array(
321+
array('class' => 'Voter1', 'vote' => 'ACCESS_DENIED'),
322+
array('class' => 'Voter2', 'vote' => 'ACCESS_GRANTED'),
323+
),
324+
array(
325+
array('class' => 'Voter1', 'vote' => 'ACCESS_GRANTED'),
326+
array('class' => 'Voter2', 'vote' => 'ACCESS_GRANTED'),
327+
),
328+
),
329+
),
330+
);
331+
}
332+
333+
/**
334+
* Test the returned data when AccessDecisionManager is a TraceableAccessDecisionManager.
335+
*
336+
* @param string $strategy strategy returned by the AccessDecisionManager
337+
* @param array $decisionLog log of the votes and final decisions from AccessDecisionManager
338+
* @param array $expectedVoterDetails expected vote details returned by the collector
339+
*
340+
* @dataProvider providerCollectWithTraceableAccessDecisionManager
341+
*/
342+
public function testCollectWithTraceableAccessDecisionManager(string $strategy, array $decisionLog, array $expectedVoterDetails): void
343+
{
344+
$accessDecisionManager = $this
345+
->getMockBuilder(TraceableAccessDecisionManager::class)
346+
->disableOriginalConstructor()
347+
->setMethods(array('getDecisionLog', 'getStrategy'))
348+
->getMock();
349+
350+
$accessDecisionManager
351+
->expects($this->any())
352+
->method('getStrategy')
353+
->willReturn($strategy);
354+
355+
$accessDecisionManager
356+
->expects($this->any())
357+
->method('getDecisionLog')
358+
->willReturn($decisionLog);
359+
360+
$dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager);
361+
$dataCollector->collect($this->getRequest(), $this->getResponse());
362+
363+
$this->assertEquals($dataCollector->getAccessDecisionLog(), $decisionLog, 'Wrong value returned by getAccessDecisionLog');
364+
365+
$this->assertEquals($dataCollector->getVoterDetails(), $expectedVoterDetails, 'Wrong value returned by getVoterDetails');
366+
$this->assertEquals($dataCollector->getVoterStrategy(), $strategy, 'Wrong value returned by getVoterStrategy');
367+
}
368+
224369
public function provideRoles()
225370
{
226371
return array(

0 commit comments

Comments
 (0)