From 4a7bfe2e786335b9546ace72e4396d3785b3da1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Marint=C5=A1enko?= Date: Fri, 20 Jun 2014 14:24:18 +0300 Subject: [PATCH 1/3] add abstract voter implementation, reducing boilerplate required for custom voter --- .../Authorization/Voter/AbstractVoter.php | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php new file mode 100644 index 0000000000000..0c16ca7c6d922 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php @@ -0,0 +1,93 @@ + + */ +abstract class AbstractVoter implements VoterInterface +{ + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return in_array($attribute, $this->getSupportedAttributes()); + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + foreach ($this->getSupportedClasses() as $supportedClass) { + if ($supportedClass === $class || is_subclass_of($class, $supportedClass)) { + return true; + } + } + + return false; + } + + /** + * Iteratively check all given attributes by calling voteOnAttribute + * This method terminates as soon as it is able to return either ACCESS_GRANTED or ACCESS_DENIED vote + * Otherwise it will return ACCESS_ABSTAIN + * + * @param TokenInterface $token A TokenInterface instance + * @param object $object The object to secure + * @param array $attributes An array of attributes associated with the method being invoked + * + * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + if (!$this->supportsClass(get_class($object))) { + return VoterInterface::ACCESS_ABSTAIN; + } + + $user = $token->getUser(); + + foreach ($attributes as $attribute) { + if ($this->supportsAttribute($attribute)) { + $vote = $this->voteOnAttribute($attribute, $object, $user); + if (VoterInterface::ACCESS_ABSTAIN !== $vote) { + return $vote; + } + } + } + + return VoterInterface::ACCESS_ABSTAIN; + } + + /** + * Return an array of supported classes. This will be called by supportsClass + * + * @return array an array of supported classes, i.e. ['\Acme\DemoBundle\Model\Product'] + */ + abstract protected function getSupportedClasses(); + + /** + * Return an array of supported attributes. This will be called by supportsAttribute + * + * @return array an array of supported attributes, i.e. ['CREATE', 'READ'] + */ + abstract protected function getSupportedAttributes(); + + /** + * Perform a single vote operation on a given attribute, object and (optionally) user + * It is safe to assume that $attribute and $object's class pass supportsAttribute/supportsClass + * + * @param string $attribute + * @param object $object + * @param UserInterface $user + * + * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED + */ + abstract protected function voteOnAttribute($attribute, $object, UserInterface $user = null); +} \ No newline at end of file From 8c774e85cff33f534e00a9dd377ea69ab570ba41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Marint=C5=A1enko?= Date: Sat, 23 Aug 2014 09:32:23 +0300 Subject: [PATCH 2/3] update AbstractVoter: vote returns as soon as access is granted; improve/update API based on PR comments --- .../Authorization/Voter/AbstractVoter.php | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php index 0c16ca7c6d922..39c52f4d06929 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php @@ -35,8 +35,10 @@ public function supportsClass($class) } /** - * Iteratively check all given attributes by calling voteOnAttribute - * This method terminates as soon as it is able to return either ACCESS_GRANTED or ACCESS_DENIED vote + * Iteratively check all given attributes by calling isGranted + * + * This method terminates as soon as it is able to return ACCESS_GRANTED + * If at least one attribute is supported, but access not granted, then ACCESS_DENIED is returned * Otherwise it will return ACCESS_ABSTAIN * * @param TokenInterface $token A TokenInterface instance @@ -47,22 +49,28 @@ public function supportsClass($class) */ public function vote(TokenInterface $token, $object, array $attributes) { - if (!$this->supportsClass(get_class($object))) { - return VoterInterface::ACCESS_ABSTAIN; + if (!$object || !$this->supportsClass(get_class($object))) { + return self::ACCESS_ABSTAIN; } - $user = $token->getUser(); + // abstain vote by default in case none of the attributes are supported + $vote = self::ACCESS_ABSTAIN; foreach ($attributes as $attribute) { - if ($this->supportsAttribute($attribute)) { - $vote = $this->voteOnAttribute($attribute, $object, $user); - if (VoterInterface::ACCESS_ABSTAIN !== $vote) { - return $vote; - } + if (!$this->supportsAttribute($attribute)) { + continue; + } + + // as soon as at least one attribute is supported, default is to deny access + $vote = self::ACCESS_DENIED; + + if ($this->isGranted($attribute, $object, $token->getUser())) { + // grant access as soon as at least one voter returns a positive response + return self::ACCESS_GRANTED; } } - return VoterInterface::ACCESS_ABSTAIN; + return $vote; } /** @@ -80,14 +88,18 @@ abstract protected function getSupportedClasses(); abstract protected function getSupportedAttributes(); /** - * Perform a single vote operation on a given attribute, object and (optionally) user + * Perform a single access check operation on a given attribute, object and (optionally) user * It is safe to assume that $attribute and $object's class pass supportsAttribute/supportsClass + * $user can be one of the following: + * a UserInterface object (fully authenticated user) + * a string (anonymously authenticated user) + * null (non-authenticated user) * - * @param string $attribute - * @param object $object - * @param UserInterface $user + * @param string $attribute + * @param object $object + * @param UserInterface|string|null $user * - * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED + * @return bool */ - abstract protected function voteOnAttribute($attribute, $object, UserInterface $user = null); -} \ No newline at end of file + abstract protected function isGranted($attribute, $object, $user = null); +} From 33413fc8e506a2c9385f17c86dee31801d461cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Marint=C5=A1enko?= Date: Sat, 23 Aug 2014 10:04:44 +0300 Subject: [PATCH 3/3] add AbstractVoter tests; add licensing information --- .../Authorization/Voter/AbstractVoter.php | 16 +++- .../Voter/AbstractVoterTest.php | 90 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Security/Tests/Core/Authentication/Voter/AbstractVoterTest.php diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php index 39c52f4d06929..61c928e80c2e1 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php @@ -1,5 +1,14 @@ + * + * 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\Security\Core\User\UserInterface; @@ -93,11 +102,10 @@ abstract protected function getSupportedAttributes(); * $user can be one of the following: * a UserInterface object (fully authenticated user) * a string (anonymously authenticated user) - * null (non-authenticated user) * - * @param string $attribute - * @param object $object - * @param UserInterface|string|null $user + * @param string $attribute + * @param object $object + * @param UserInterface|string $user * * @return bool */ diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Voter/AbstractVoterTest.php b/src/Symfony/Component/Security/Tests/Core/Authentication/Voter/AbstractVoterTest.php new file mode 100644 index 0000000000000..955b610d2f382 --- /dev/null +++ b/src/Symfony/Component/Security/Tests/Core/Authentication/Voter/AbstractVoterTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Tests\Core\Authentication\Voter; + +use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; + +/** + * @author Roman Marintšenko + */ +class AbstractVoterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var AbstractVoter + */ + private $voter; + + private $token; + + public function setUp() + { + $this->voter = new VoterFixture(); + + $tokenMock = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $tokenMock + ->expects($this->any()) + ->method('getUser') + ->will($this->returnValue('user')); + + $this->token = $tokenMock; + } + + /** + * @dataProvider getData + */ + public function testVote($expectedVote, $object, $attributes, $message) + { + $this->assertEquals($expectedVote, $this->voter->vote($this->token, $object, $attributes), $message); + } + + public function getData() + { + return array( + array(AbstractVoter::ACCESS_ABSTAIN, null, array(), 'ACCESS_ABSTAIN for null objects'), + array(AbstractVoter::ACCESS_ABSTAIN, new UnsupportedObjectFixture(), array(), 'ACCESS_ABSTAIN for objects with unsupported class'), + array(AbstractVoter::ACCESS_ABSTAIN, new ObjectFixture(), array(), 'ACCESS_ABSTAIN for no attributes'), + array(AbstractVoter::ACCESS_ABSTAIN, new ObjectFixture(), array('foobar'), 'ACCESS_ABSTAIN for unsupported attributes'), + array(AbstractVoter::ACCESS_GRANTED, new ObjectFixture(), array('foo'), 'ACCESS_GRANTED if attribute grants access'), + array(AbstractVoter::ACCESS_GRANTED, new ObjectFixture(), array('bar', 'foo'), 'ACCESS_GRANTED if *at least one* attribute grants access'), + array(AbstractVoter::ACCESS_GRANTED, new ObjectFixture(), array('foobar', 'foo'), 'ACCESS_GRANTED if *at least one* attribute grants access'), + array(AbstractVoter::ACCESS_DENIED, new ObjectFixture(), array('bar', 'baz'), 'ACCESS_DENIED for if no attribute grants access'), + ); + } +} + +class VoterFixture extends AbstractVoter +{ + protected function getSupportedClasses() + { + return array( + 'Symfony\Component\Security\Tests\Core\Authentication\Voter\ObjectFixture', + ); + } + + protected function getSupportedAttributes() + { + return array( 'foo', 'bar', 'baz'); + } + + protected function isGranted($attribute, $object, $user = null) + { + return $attribute === 'foo'; + } +} + +class ObjectFixture +{ +} + +class UnsupportedObjectFixture +{ +}