[Security][SecurityBundle] Add voter individual decisions to profiler
This commit is contained in:
parent
edcd627230
commit
8abb05607b
@ -16,6 +16,7 @@ CHANGELOG
|
||||
* Deprecated the `simple_form` and `simple_preauth` authentication listeners, use Guard instead.
|
||||
* Deprecated the `SimpleFormFactory` and `SimplePreAuthenticationFactory` classes, use Guard instead.
|
||||
* Added `port` in access_control
|
||||
* Added individual voter decisions to the profiler
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||
use Symfony\Component\Security\Core\Role\SwitchUserRole;
|
||||
@ -136,12 +137,33 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
|
||||
|
||||
// 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) {
|
||||
if ($voter instanceof TraceableVoter) {
|
||||
$voter = $voter->getDecoratedVoter();
|
||||
}
|
||||
|
||||
$this->data['voters'][] = $this->hasVarDumper ? new ClassStub(\get_class($voter)) : \get_class($voter);
|
||||
}
|
||||
|
||||
// collect voter details
|
||||
$decisionLog = $this->accessDecisionManager->getDecisionLog();
|
||||
foreach ($decisionLog as $key => $log) {
|
||||
$decisionLog[$key]['voter_details'] = array();
|
||||
foreach ($log['voterDetails'] as $voterDetail) {
|
||||
$voterClass = \get_class($voterDetail['voter']);
|
||||
$classData = $this->hasVarDumper ? new ClassStub($voterClass) : $voterClass;
|
||||
$decisionLog[$key]['voter_details'][] = array(
|
||||
'class' => $classData,
|
||||
'attributes' => $voterDetail['attributes'], // Only displayed for unanimous strategy
|
||||
'vote' => $voterDetail['vote'],
|
||||
);
|
||||
}
|
||||
unset($decisionLog[$key]['voterDetails']);
|
||||
}
|
||||
|
||||
$this->data['access_decision_log'] = $decisionLog;
|
||||
} else {
|
||||
$this->data['access_decision_log'] = array();
|
||||
$this->data['voter_strategy'] = 'unknown';
|
||||
|
@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -41,13 +43,27 @@ class AddSecurityVotersPass implements CompilerPassInterface
|
||||
throw new LogicException('No security voters found. You need to tag at least one with "security.voter".');
|
||||
}
|
||||
|
||||
$debug = $container->getParameter('kernel.debug');
|
||||
|
||||
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.security.voter.'.$voterServiceId;
|
||||
$container
|
||||
->register($debugVoterServiceId, TraceableVoter::class)
|
||||
->setDecoratedService($voterServiceId)
|
||||
->addArgument(new Reference($debugVoterServiceId.'.inner'))
|
||||
->addArgument(new Reference('event_dispatcher'));
|
||||
}
|
||||
}
|
||||
|
||||
$adm = $container->getDefinition('security.access.decision_manager');
|
||||
|
@ -0,0 +1,48 @@
|
||||
<?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\Bundle\SecurityBundle\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Event\VoteEvent;
|
||||
|
||||
/**
|
||||
* Listen to vote events from traceable voters.
|
||||
*
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class VoteListener implements EventSubscriberInterface
|
||||
{
|
||||
private $traceableAccessDecisionManager;
|
||||
|
||||
public function __construct(TraceableAccessDecisionManager $traceableAccessDecisionManager)
|
||||
{
|
||||
$this->traceableAccessDecisionManager = $traceableAccessDecisionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event dispatched by a voter during access manager decision.
|
||||
*
|
||||
* @param VoteEvent $event event with voter data
|
||||
*/
|
||||
public function onVoterVote(VoteEvent $event)
|
||||
{
|
||||
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVote());
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array('debug.security.authorization.vote' => 'onVoterVote');
|
||||
}
|
||||
}
|
@ -11,6 +11,11 @@
|
||||
<argument type="service" id="debug.security.access.decision_manager.inner" />
|
||||
</service>
|
||||
|
||||
<service id="debug.security.voter.vote_listener" class="Symfony\Bundle\SecurityBundle\EventListener\VoteListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="debug.security.access.decision_manager" />
|
||||
</service>
|
||||
|
||||
<service id="debug.security.firewall" class="Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="security.firewall.map" />
|
||||
|
@ -307,7 +307,7 @@
|
||||
|
||||
<tbody>
|
||||
{% for decision in collector.accessDecisionLog %}
|
||||
<tr>
|
||||
<tr class="voter_result">
|
||||
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
|
||||
<td class="font-normal">
|
||||
{{ decision.result
|
||||
@ -331,6 +331,40 @@
|
||||
</td>
|
||||
<td>{{ profiler_dump(decision.seek('object')) }}</td>
|
||||
</tr>
|
||||
<tr class="voter_details">
|
||||
<td></td>
|
||||
<td colspan="3">
|
||||
{% if decision.voter_details is not empty %}
|
||||
{% set voter_details_id = 'voter-details-' ~ loop.index %}
|
||||
<div id="{{ voter_details_id }}" class="sf-toggle-content sf-toggle-hidden">
|
||||
<table>
|
||||
<tbody>
|
||||
{% for voter_detail in decision.voter_details %}
|
||||
<tr>
|
||||
<td class="font-normal">{{ profiler_dump(voter_detail['class']) }}</td>
|
||||
{% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %}
|
||||
<td class="font-normal text-small">attribute {{ voter_detail['attributes'][0] }}</td>
|
||||
{% endif %}
|
||||
<td class="font-normal text-small">
|
||||
{% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
|
||||
ACCESS GRANTED
|
||||
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
|
||||
ACCESS ABSTAIN
|
||||
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
|
||||
ACCESS DENIED
|
||||
{% else %}
|
||||
unknown ({{ voter_detail['vote'] }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -17,10 +17,15 @@ use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Security\Core\Role\RoleHierarchy;
|
||||
use Symfony\Component\Security\Core\Role\SwitchUserRole;
|
||||
@ -221,6 +226,137 @@ class SecurityDataCollectorTest extends TestCase
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
public function providerCollectDecisionLog(): \Generator
|
||||
{
|
||||
$voter1 = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
|
||||
$voter2 = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
|
||||
|
||||
$eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass();
|
||||
$decoratedVoter1 = new TraceableVoter($voter1, $eventDispatcher);
|
||||
$decoratedVoter2 = new TraceableVoter($voter2, $eventDispatcher);
|
||||
|
||||
yield array(
|
||||
AccessDecisionManager::STRATEGY_AFFIRMATIVE,
|
||||
array(array(
|
||||
'attributes' => array('view'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => true,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
),
|
||||
)),
|
||||
array($decoratedVoter1, $decoratedVoter1),
|
||||
array(\get_class($voter1), \get_class($voter2)),
|
||||
array(array(
|
||||
'attributes' => array('view'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => true,
|
||||
'voter_details' => array(
|
||||
array('class' => \get_class($voter1), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('class' => \get_class($voter2), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
yield array(
|
||||
AccessDecisionManager::STRATEGY_UNANIMOUS,
|
||||
array(
|
||||
array(
|
||||
'attributes' => array('view', 'edit'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => false,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('voter' => $voter1, 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('voter' => $voter2, 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
array('voter' => $voter2, 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'attributes' => array('update'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => true,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
array('voter' => $voter2, 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
),
|
||||
),
|
||||
array($decoratedVoter1, $decoratedVoter1),
|
||||
array(\get_class($voter1), \get_class($voter2)),
|
||||
array(
|
||||
array(
|
||||
'attributes' => array('view', 'edit'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => false,
|
||||
'voter_details' => array(
|
||||
array('class' => \get_class($voter1), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('class' => \get_class($voter1), 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('class' => \get_class($voter2), 'attributes' => array('view'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
array('class' => \get_class($voter2), 'attributes' => array('edit'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'attributes' => array('update'),
|
||||
'object' => new \stdClass(),
|
||||
'result' => true,
|
||||
'voter_details' => array(
|
||||
array('class' => \get_class($voter1), 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
array('class' => \get_class($voter2), 'attributes' => array('update'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the returned data when AccessDecisionManager is a TraceableAccessDecisionManager.
|
||||
*
|
||||
* @param string $strategy strategy returned by the AccessDecisionManager
|
||||
* @param array $voters voters returned by AccessDecisionManager
|
||||
* @param array $decisionLog log of the votes and final decisions from AccessDecisionManager
|
||||
* @param array $expectedVoterClasses expected voter classes returned by the collector
|
||||
* @param array $expectedDecisionLog expected decision log returned by the collector
|
||||
*
|
||||
* @dataProvider providerCollectDecisionLog
|
||||
*/
|
||||
public function testCollectDecisionLog(string $strategy, array $decisionLog, array $voters, array $expectedVoterClasses, array $expectedDecisionLog): void
|
||||
{
|
||||
$accessDecisionManager = $this
|
||||
->getMockBuilder(TraceableAccessDecisionManager::class)
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(array('getStrategy', 'getVoters', 'getDecisionLog'))
|
||||
->getMock();
|
||||
|
||||
$accessDecisionManager
|
||||
->expects($this->any())
|
||||
->method('getStrategy')
|
||||
->willReturn($strategy);
|
||||
|
||||
$accessDecisionManager
|
||||
->expects($this->any())
|
||||
->method('getVoters')
|
||||
->willReturn($voters);
|
||||
|
||||
$accessDecisionManager
|
||||
->expects($this->any())
|
||||
->method('getDecisionLog')
|
||||
->willReturn($decisionLog);
|
||||
|
||||
$dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager);
|
||||
$dataCollector->collect($this->getRequest(), $this->getResponse());
|
||||
|
||||
$this->assertEquals($dataCollector->getAccessDecisionLog(), $expectedDecisionLog, 'Wrong value returned by getAccessDecisionLog');
|
||||
|
||||
$this->assertSame(
|
||||
array_map(function ($classStub) { return (string) $classStub; }, $dataCollector->getVoters()),
|
||||
$expectedVoterClasses,
|
||||
'Wrong value returned by getVoters'
|
||||
);
|
||||
$this->assertSame($dataCollector->getVoterStrategy(), $strategy, 'Wrong value returned by getVoterStrategy');
|
||||
}
|
||||
|
||||
public function provideRoles()
|
||||
{
|
||||
return array(
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
@ -39,6 +40,8 @@ class AddSecurityVotersPassTest extends TestCase
|
||||
public function testThatSecurityVotersAreProcessedInPriorityOrder()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', false);
|
||||
|
||||
$container
|
||||
->register('security.access.decision_manager', AccessDecisionManager::class)
|
||||
->addArgument(array())
|
||||
@ -69,6 +72,68 @@ class AddSecurityVotersPassTest extends TestCase
|
||||
$this->assertCount(4, $refs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that in debug mode, voters are correctly decorated.
|
||||
*/
|
||||
public function testThatVotersAreDecoratedInDebugMode(): void
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$voterDef1 = new Definition(Voter::class);
|
||||
$voterDef1->addTag('security.voter');
|
||||
$container->setDefinition('voter1', $voterDef1);
|
||||
|
||||
$voterDef2 = new Definition(Voter::class);
|
||||
$voterDef2->addTag('security.voter');
|
||||
$container->setDefinition('voter2', $voterDef2);
|
||||
|
||||
$container
|
||||
->register('security.access.decision_manager', AccessDecisionManager::class)
|
||||
->addArgument(array($voterDef1, $voterDef2));
|
||||
$container->setParameter('kernel.debug', true);
|
||||
|
||||
$compilerPass = new AddSecurityVotersPass();
|
||||
$compilerPass->process($container);
|
||||
|
||||
$def1 = $container->getDefinition('.debug.security.voter.voter1');
|
||||
$this->assertEquals(array('voter1', null, 0), $def1->getDecoratedService(), 'voter1: wrong return from getDecoratedService');
|
||||
$this->assertEquals(new Reference('.debug.security.voter.voter1.inner'), $def1->getArgument(0), 'voter1: wrong decorator argument');
|
||||
|
||||
$def2 = $container->getDefinition('.debug.security.voter.voter2');
|
||||
$this->assertEquals(array('voter2', null, 0), $def2->getDecoratedService(), 'voter2: wrong return from getDecoratedService');
|
||||
$this->assertEquals(new Reference('.debug.security.voter.voter2.inner'), $def2->getArgument(0), 'voter2: wrong decorator argument');
|
||||
|
||||
$voters = $container->findTaggedServiceIds('security.voter');
|
||||
$this->assertCount(2, $voters, 'Incorrect count of voters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that voters are not decorated if the application is not in debug mode.
|
||||
*/
|
||||
public function testThatVotersAreNotDecoratedWithoutDebugMode(): void
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', false);
|
||||
|
||||
$voterDef1 = new Definition(Voter::class);
|
||||
$voterDef1->addTag('security.voter');
|
||||
$container->setDefinition('voter1', $voterDef1);
|
||||
|
||||
$voterDef2 = new Definition(Voter::class);
|
||||
$voterDef2->addTag('security.voter');
|
||||
$container->setDefinition('voter2', $voterDef2);
|
||||
|
||||
$container
|
||||
->register('security.access.decision_manager', AccessDecisionManager::class)
|
||||
->addArgument(array($voterDef1, $voterDef2));
|
||||
|
||||
$compilerPass = new AddSecurityVotersPass();
|
||||
$compilerPass->process($container);
|
||||
|
||||
$this->assertFalse($container->has('debug.security.voter.voter1'), 'voter1 should not be decorated');
|
||||
$this->assertFalse($container->has('debug.security.voter.voter2'), 'voter2 should not be decorated');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
|
||||
* @expectedExceptionMessage stdClass must implement the Symfony\Component\Security\Core\Authorization\Voter\VoterInterface when used as a voter.
|
||||
@ -76,6 +141,7 @@ class AddSecurityVotersPassTest extends TestCase
|
||||
public function testVoterMissingInterface()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', false);
|
||||
$container
|
||||
->register('security.access.decision_manager', AccessDecisionManager::class)
|
||||
->addArgument(array())
|
||||
|
@ -511,6 +511,8 @@ abstract class CompleteConfigurationTest extends TestCase
|
||||
$file .= '.'.$this->getFileExtension();
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', false);
|
||||
|
||||
$security = new SecurityExtension();
|
||||
$container->registerExtension($security);
|
||||
|
||||
|
@ -346,6 +346,8 @@ class SecurityExtensionTest extends TestCase
|
||||
protected function getRawContainer()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', false);
|
||||
|
||||
$security = new SecurityExtension();
|
||||
$container->registerExtension($security);
|
||||
|
||||
|
@ -0,0 +1,40 @@
|
||||
<?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\Bundle\SecurityBundle\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\EventListener\VoteListener;
|
||||
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
use Symfony\Component\Security\Core\Event\VoteEvent;
|
||||
|
||||
class VoteListenerTest extends TestCase
|
||||
{
|
||||
public function testOnVoterVote()
|
||||
{
|
||||
$voter = $this->createMock(VoterInterface::class);
|
||||
|
||||
$traceableAccessDecisionManager = $this
|
||||
->getMockBuilder(TraceableAccessDecisionManager::class)
|
||||
->disableOriginalConstructor()
|
||||
->setMethods(array('addVoterVote'))
|
||||
->getMock();
|
||||
|
||||
$traceableAccessDecisionManager
|
||||
->expects($this->once())
|
||||
->method('addVoterVote')
|
||||
->with($voter, array('myattr1', 'myattr2'), VoterInterface::ACCESS_GRANTED);
|
||||
|
||||
$sut = new VoteListener($traceableAccessDecisionManager);
|
||||
$sut->onVoterVote(new VoteEvent($voter, 'mysubject', array('myattr1', 'myattr2'), VoterInterface::ACCESS_GRANTED));
|
||||
}
|
||||
}
|
@ -1027,6 +1027,31 @@ table.logs .metadata {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
{# Security panel
|
||||
========================================================================= #}
|
||||
#collector-content .decision-log .voter_result td {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#collector-content .decision-log .voter_details td {
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#collector-content .decision-log .voter_details table {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
#collector-content .decision-log .voter_details table td {
|
||||
border: 0;
|
||||
padding: 0 0 8px 0;
|
||||
}
|
||||
|
||||
{# Validator panel
|
||||
========================================================================= #}
|
||||
|
||||
|
@ -27,7 +27,8 @@ class TraceableAccessDecisionManager implements AccessDecisionManagerInterface
|
||||
private $manager;
|
||||
private $strategy;
|
||||
private $voters = array();
|
||||
private $decisionLog = array();
|
||||
private $decisionLog = array(); // All decision logs
|
||||
private $currentLog = array(); // Logs being filled in
|
||||
|
||||
public function __construct(AccessDecisionManagerInterface $manager)
|
||||
{
|
||||
@ -49,17 +50,40 @@ class TraceableAccessDecisionManager implements AccessDecisionManagerInterface
|
||||
*/
|
||||
public function decide(TokenInterface $token, array $attributes, $object = null)
|
||||
{
|
||||
$result = $this->manager->decide($token, $attributes, $object);
|
||||
|
||||
$this->decisionLog[] = array(
|
||||
$currentDecisionLog = array(
|
||||
'attributes' => $attributes,
|
||||
'object' => $object,
|
||||
'result' => $result,
|
||||
'voterDetails' => array(),
|
||||
);
|
||||
|
||||
$this->currentLog[] = &$currentDecisionLog;
|
||||
|
||||
$result = $this->manager->decide($token, $attributes, $object);
|
||||
|
||||
$currentDecisionLog['result'] = $result;
|
||||
|
||||
$this->decisionLog[] = array_pop($this->currentLog); // Using a stack since decide can be called by voters
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds voter vote and class to the voter details.
|
||||
*
|
||||
* @param VoterInterface $voter voter
|
||||
* @param array $attributes attributes used for the vote
|
||||
* @param int $vote vote of the voter
|
||||
*/
|
||||
public function addVoterVote(VoterInterface $voter, array $attributes, int $vote)
|
||||
{
|
||||
$currentLogIndex = \count($this->currentLog) - 1;
|
||||
$this->currentLog[$currentLogIndex]['voterDetails'][] = array(
|
||||
'voter' => $voter,
|
||||
'attributes' => $attributes,
|
||||
'vote' => $vote,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?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\Voter;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Event\VoteEvent;
|
||||
|
||||
/**
|
||||
* Decorates voter classes to send result events.
|
||||
*
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TraceableVoter implements VoterInterface
|
||||
{
|
||||
private $voter;
|
||||
private $eventDispatcher;
|
||||
|
||||
public function __construct(VoterInterface $voter, EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->voter = $voter;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function vote(TokenInterface $token, $subject, array $attributes)
|
||||
{
|
||||
$result = $this->voter->vote($token, $subject, $attributes);
|
||||
|
||||
$this->eventDispatcher->dispatch('debug.security.authorization.vote', new VoteEvent($this->voter, $subject, $attributes, $result));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getDecoratedVoter(): VoterInterface
|
||||
{
|
||||
return $this->voter;
|
||||
}
|
||||
}
|
58
src/Symfony/Component/Security/Core/Event/VoteEvent.php
Normal file
58
src/Symfony/Component/Security/Core/Event/VoteEvent.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* This event is dispatched on voter vote.
|
||||
*
|
||||
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class VoteEvent extends Event
|
||||
{
|
||||
private $voter;
|
||||
private $subject;
|
||||
private $attributes;
|
||||
private $vote;
|
||||
|
||||
public function __construct(VoterInterface $voter, $subject, array $attributes, int $vote)
|
||||
{
|
||||
$this->voter = $voter;
|
||||
$this->subject = $subject;
|
||||
$this->attributes = $attributes;
|
||||
$this->vote = $vote;
|
||||
}
|
||||
|
||||
public function getVoter(): VoterInterface
|
||||
{
|
||||
return $this->voter;
|
||||
}
|
||||
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getVote(): int
|
||||
{
|
||||
return $this->vote;
|
||||
}
|
||||
}
|
@ -16,31 +16,162 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
class TraceableAccessDecisionManagerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideObjectsAndLogs
|
||||
*/
|
||||
public function testDecideLog($expectedLog, $object)
|
||||
public function testDecideLog(array $expectedLog, array $attributes, $object, array $voterVotes, bool $result)
|
||||
{
|
||||
$adm = new TraceableAccessDecisionManager(new AccessDecisionManager());
|
||||
$adm->decide($this->getMockBuilder(TokenInterface::class)->getMock(), array('ATTRIBUTE_1'), $object);
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
|
||||
$this->assertSame($expectedLog, $adm->getDecisionLog());
|
||||
$admMock = $this
|
||||
->getMockBuilder(AccessDecisionManager::class)
|
||||
->setMethods(array('decide'))
|
||||
->getMock();
|
||||
|
||||
$adm = new TraceableAccessDecisionManager($admMock);
|
||||
|
||||
$admMock
|
||||
->expects($this->once())
|
||||
->method('decide')
|
||||
->with($token, $attributes, $object)
|
||||
->willReturnCallback(function ($token, $attributes, $object) use ($voterVotes, $adm, $result) {
|
||||
foreach ($voterVotes as $voterVote) {
|
||||
list($voter, $vote) = $voterVote;
|
||||
$adm->addVoterVote($voter, $attributes, $vote);
|
||||
}
|
||||
|
||||
return $result;
|
||||
})
|
||||
;
|
||||
|
||||
$adm->decide($token, $attributes, $object);
|
||||
|
||||
$this->assertEquals($expectedLog, $adm->getDecisionLog());
|
||||
}
|
||||
|
||||
public function provideObjectsAndLogs()
|
||||
public function provideObjectsAndLogs(): \Generator
|
||||
{
|
||||
$object = new \stdClass();
|
||||
$voter1 = $this->getMockForAbstractClass(VoterInterface::class);
|
||||
$voter2 = $this->getMockForAbstractClass(VoterInterface::class);
|
||||
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => null, 'result' => false)), null);
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => true, 'result' => false)), true);
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => 'jolie string', 'result' => false)), 'jolie string');
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => 12345, 'result' => false)), 12345);
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => $x = fopen(__FILE__, 'r'), 'result' => false)), $x);
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => $x = array(), 'result' => false)), $x);
|
||||
yield array(array(array('attributes' => array('ATTRIBUTE_1'), 'object' => $object, 'result' => false)), $object);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array('ATTRIBUTE_1'),
|
||||
'object' => null,
|
||||
'result' => true,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('ATTRIBUTE_1'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
array('voter' => $voter2, 'attributes' => array('ATTRIBUTE_1'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
)),
|
||||
array('ATTRIBUTE_1'),
|
||||
null,
|
||||
array(
|
||||
array($voter1, VoterInterface::ACCESS_GRANTED),
|
||||
array($voter2, VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
true,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array('ATTRIBUTE_1', 'ATTRIBUTE_2'),
|
||||
'object' => true,
|
||||
'result' => false,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('ATTRIBUTE_1', 'ATTRIBUTE_2'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array('ATTRIBUTE_1', 'ATTRIBUTE_2'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
)),
|
||||
array('ATTRIBUTE_1', 'ATTRIBUTE_2'),
|
||||
true,
|
||||
array(
|
||||
array($voter1, VoterInterface::ACCESS_ABSTAIN),
|
||||
array($voter2, VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
false,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array(null),
|
||||
'object' => 'jolie string',
|
||||
'result' => false,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array(null), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array(null), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
),
|
||||
)),
|
||||
array(null),
|
||||
'jolie string',
|
||||
array(
|
||||
array($voter1, VoterInterface::ACCESS_ABSTAIN),
|
||||
array($voter2, VoterInterface::ACCESS_DENIED),
|
||||
),
|
||||
false,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array(12),
|
||||
'object' => 12345,
|
||||
'result' => true,
|
||||
'voterDetails' => array(),
|
||||
)),
|
||||
'attributes' => array(12),
|
||||
12345,
|
||||
array(),
|
||||
true,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array(new \stdClass()),
|
||||
'object' => $x = fopen(__FILE__, 'rb'),
|
||||
'result' => true,
|
||||
'voterDetails' => array(),
|
||||
)),
|
||||
array(new \stdClass()),
|
||||
$x,
|
||||
array(),
|
||||
true,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array('ATTRIBUTE_2'),
|
||||
'object' => $x = array(),
|
||||
'result' => false,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('ATTRIBUTE_2'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array('ATTRIBUTE_2'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
),
|
||||
)),
|
||||
array('ATTRIBUTE_2'),
|
||||
$x,
|
||||
array(
|
||||
array($voter1, VoterInterface::ACCESS_ABSTAIN),
|
||||
array($voter2, VoterInterface::ACCESS_ABSTAIN),
|
||||
),
|
||||
false,
|
||||
);
|
||||
yield array(
|
||||
array(array(
|
||||
'attributes' => array(12.13),
|
||||
'object' => new \stdClass(),
|
||||
'result' => false,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array(12.13), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('voter' => $voter2, 'attributes' => array(12.13), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
),
|
||||
)),
|
||||
array(12.13),
|
||||
new \stdClass(),
|
||||
array(
|
||||
array($voter1, VoterInterface::ACCESS_DENIED),
|
||||
array($voter2, VoterInterface::ACCESS_DENIED),
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public function testDebugAccessDecisionManagerAliasExistsForBC()
|
||||
@ -49,4 +180,101 @@ class TraceableAccessDecisionManagerTest extends TestCase
|
||||
|
||||
$this->assertInstanceOf(DebugAccessDecisionManager::class, $adm, 'For BC, TraceableAccessDecisionManager must be an instance of DebugAccessDecisionManager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests decision log returned when a voter call decide method of AccessDecisionManager.
|
||||
*/
|
||||
public function testAccessDecisionManagerCalledByVoter()
|
||||
{
|
||||
$voter1 = $this
|
||||
->getMockBuilder(VoterInterface::class)
|
||||
->setMethods(array('vote'))
|
||||
->getMock();
|
||||
|
||||
$voter2 = $this
|
||||
->getMockBuilder(VoterInterface::class)
|
||||
->setMethods(array('vote'))
|
||||
->getMock();
|
||||
|
||||
$voter3 = $this
|
||||
->getMockBuilder(VoterInterface::class)
|
||||
->setMethods(array('vote'))
|
||||
->getMock();
|
||||
|
||||
$sut = new TraceableAccessDecisionManager(new AccessDecisionManager(array($voter1, $voter2, $voter3)));
|
||||
|
||||
$voter1
|
||||
->expects($this->any())
|
||||
->method('vote')
|
||||
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter1) {
|
||||
$vote = \in_array('attr1', $attributes) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_ABSTAIN;
|
||||
$sut->addVoterVote($voter1, $attributes, $vote);
|
||||
|
||||
return $vote;
|
||||
});
|
||||
|
||||
$voter2
|
||||
->expects($this->any())
|
||||
->method('vote')
|
||||
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter2) {
|
||||
if (\in_array('attr2', $attributes)) {
|
||||
$vote = null == $subject ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
|
||||
} else {
|
||||
$vote = VoterInterface::ACCESS_ABSTAIN;
|
||||
}
|
||||
|
||||
$sut->addVoterVote($voter2, $attributes, $vote);
|
||||
|
||||
return $vote;
|
||||
});
|
||||
|
||||
$voter3
|
||||
->expects($this->any())
|
||||
->method('vote')
|
||||
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter3) {
|
||||
if (\in_array('attr2', $attributes) && $subject) {
|
||||
$vote = $sut->decide($token, $attributes);
|
||||
} else {
|
||||
$vote = VoterInterface::ACCESS_ABSTAIN;
|
||||
}
|
||||
|
||||
$sut->addVoterVote($voter3, $attributes, $vote);
|
||||
|
||||
return $vote;
|
||||
});
|
||||
|
||||
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
||||
$sut->decide($token, array('attr1'), null);
|
||||
$sut->decide($token, array('attr2'), $obj = new \stdClass());
|
||||
|
||||
$this->assertEquals(array(
|
||||
array(
|
||||
'attributes' => array('attr1'),
|
||||
'object' => null,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('attr1'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
'result' => true,
|
||||
),
|
||||
array(
|
||||
'attributes' => array('attr2'),
|
||||
'object' => null,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('attr2'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array('attr2'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
'result' => true,
|
||||
),
|
||||
array(
|
||||
'attributes' => array('attr2'),
|
||||
'object' => $obj,
|
||||
'voterDetails' => array(
|
||||
array('voter' => $voter1, 'attributes' => array('attr2'), 'vote' => VoterInterface::ACCESS_ABSTAIN),
|
||||
array('voter' => $voter2, 'attributes' => array('attr2'), 'vote' => VoterInterface::ACCESS_DENIED),
|
||||
array('voter' => $voter3, 'attributes' => array('attr2'), 'vote' => VoterInterface::ACCESS_GRANTED),
|
||||
),
|
||||
'result' => true,
|
||||
),
|
||||
), $sut->getDecisionLog());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
<?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\Tests\Authorization\Voter;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
use Symfony\Component\Security\Core\Event\VoteEvent;
|
||||
|
||||
class TraceableVoterTest extends TestCase
|
||||
{
|
||||
public function testGetDecoratedVoterClass()
|
||||
{
|
||||
$voter = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
|
||||
|
||||
$sut = new TraceableVoter($voter, $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass());
|
||||
$this->assertSame($voter, $sut->getDecoratedVoter());
|
||||
}
|
||||
|
||||
public function testVote()
|
||||
{
|
||||
$voter = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
|
||||
|
||||
$eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass();
|
||||
$token = $this->getMockBuilder(TokenInterface::class)->getMockForAbstractClass();
|
||||
|
||||
$voter
|
||||
->expects($this->once())
|
||||
->method('vote')
|
||||
->with($token, 'anysubject', array('attr1'))
|
||||
->willReturn(VoterInterface::ACCESS_DENIED);
|
||||
|
||||
$eventDispatcher
|
||||
->expects($this->once())
|
||||
->method('dispatch')
|
||||
->with('debug.security.authorization.vote', new VoteEvent($voter, 'anysubject', array('attr1'), VoterInterface::ACCESS_DENIED));
|
||||
|
||||
$sut = new TraceableVoter($voter, $eventDispatcher);
|
||||
$result = $sut->vote($token, 'anysubject', array('attr1'));
|
||||
|
||||
$this->assertSame(VoterInterface::ACCESS_DENIED, $result);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user