From abe804726279148cdebae017550308c7fc21114b Mon Sep 17 00:00:00 2001 From: Johannes Schmitt Date: Sun, 12 Dec 2010 09:41:47 +0100 Subject: [PATCH] added authentication trust resolver --- .../DependencyInjection/SecurityExtension.php | 2 +- .../Resources/config/security.xml | 10 +++ .../Resources/config/security_templates.xml | 1 + .../Security/Firewall/ExceptionListener.php | 11 ++-- .../AuthenticationTrustResolver.php | 66 +++++++++++++++++++ .../AuthenticationTrustResolverInterface.php | 53 +++++++++++++++ .../Authentication/Token/RememberMeToken.php | 56 ++++++++++++++++ .../Voter/AuthenticatedVoter.php | 40 +++++++++-- .../AuthenticationTrustResolverTest.php | 63 ++++++++++++++++++ .../Voter/AuthenticatedVoterTest.php | 41 ++++++++---- 10 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 src/Symfony/Component/Security/Authentication/AuthenticationTrustResolver.php create mode 100644 src/Symfony/Component/Security/Authentication/AuthenticationTrustResolverInterface.php create mode 100644 src/Symfony/Component/Security/Authentication/Token/RememberMeToken.php create mode 100644 tests/Symfony/Tests/Component/Security/Authentication/AuthenticationTrustResolverTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php index 9cb97e867826..45835d143b17 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/SecurityExtension.php @@ -450,7 +450,7 @@ protected function createExceptionListener($container, $id, $defaultEntryPoint) $exceptionListenerId = 'security.exception_listener.'.$id; $listener = $container->setDefinition($exceptionListenerId, clone $container->getDefinition('security.exception_listener')); $arguments = $listener->getArguments(); - $arguments[1] = null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint); + $arguments[2] = null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint); $listener->setArguments($arguments); return $exceptionListenerId; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml index 18d63a375080..35f8ab0ed28d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml @@ -16,6 +16,10 @@ Symfony\Component\Security\User\InMemoryUserProvider + Symfony\Component\Security\Authentication\AuthenticationTrustResolver + Symfony\Component\Security\Authentication\Token\AnonymousToken + Symfony\Component\Security\Authentication\Token\RememberMeToken + Symfony\Component\Security\Authentication\Provider\DaoAuthenticationProvider Symfony\Component\Security\Authentication\Provider\PreAuthenticatedAuthenticationProvider @@ -101,6 +105,11 @@ + + %security.authentication.trust_resolver.anonymous_class% + %security.authentication.trust_resolver.rememberme_class% + + @@ -132,6 +141,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml index 602dc0523f86..866645d0bbbb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_templates.xml @@ -62,6 +62,7 @@ + %security.access_denied.url% diff --git a/src/Symfony/Component/HttpKernel/Security/Firewall/ExceptionListener.php b/src/Symfony/Component/HttpKernel/Security/Firewall/ExceptionListener.php index f4c734bdc048..2d72f19dc135 100644 --- a/src/Symfony/Component/HttpKernel/Security/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/Security/Firewall/ExceptionListener.php @@ -3,6 +3,7 @@ namespace Symfony\Component\HttpKernel\Security\Firewall; use Symfony\Component\Security\SecurityContext; +use Symfony\Component\Security\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Authentication\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -33,13 +34,15 @@ class ExceptionListener implements ListenerInterface { protected $context; protected $authenticationEntryPoint; + protected $authenticationTrustResolver; protected $errorPage; protected $logger; - public function __construct(SecurityContext $context, AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, LoggerInterface $logger = null) + public function __construct(SecurityContext $context, AuthenticationTrustResolverInterface $trustResolver, AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, LoggerInterface $logger = null) { $this->context = $context; $this->authenticationEntryPoint = $authenticationEntryPoint; + $this->authenticationTrustResolver = $trustResolver; $this->errorPage = $errorPage; $this->logger = $logger; } @@ -87,9 +90,9 @@ public function handleException(Event $event) } } elseif ($exception instanceof AccessDeniedException) { $token = $this->context->getToken(); - if (null === $token || $token instanceof AnonymousToken) { + if (!$this->authenticationTrustResolver->isFullFledged($token)) { if (null !== $this->logger) { - $this->logger->info('Access denied (user is anonymous); redirecting to authentication entry point'); + $this->logger->info('Access denied (user is not fully authenticated); redirecting to authentication entry point'); } try { @@ -101,7 +104,7 @@ public function handleException(Event $event) } } else { if (null !== $this->logger) { - $this->logger->info('Access is denied (and user is not anonymous)'); + $this->logger->info('Access is denied (and user is neither anonymous, nor remember-me)'); } if (null === $this->errorPage) { diff --git a/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolver.php b/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolver.php new file mode 100644 index 000000000000..010fa9117288 --- /dev/null +++ b/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolver.php @@ -0,0 +1,66 @@ + + */ +class AuthenticationTrustResolver implements AuthenticationTrustResolverInterface +{ + protected $anonymousClass; + protected $rememberMeClass; + + /** + * Constructor + * + * @param string $anonymousClass + * @param string $rememberMeClass + * + * @return void + */ + public function __construct($anonymousClass, $rememberMeClass) + { + $this->anonymousClass = $anonymousClass; + $this->rememberMeClass = $rememberMeClass; + } + + /** + * {@inheritDoc} + */ + public function isAnonymous(TokenInterface $token = null) + { + if (null === $token) { + return false; + } + + return $token instanceof $this->anonymousClass; + } + + /** + * {@inheritDoc} + */ + public function isRememberMe(TokenInterface $token = null) + { + if (null === $token) { + return false; + } + + return $token instanceof $this->rememberMeClass; + } + + /** + * {@inheritDoc} + */ + public function isFullFledged(TokenInterface $token = null) + { + if (null === $token) { + return false; + } + + return !$this->isAnonymous($token) && !$this->isRememberMe($token); + } +} diff --git a/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolverInterface.php b/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolverInterface.php new file mode 100644 index 000000000000..e57bb6542b6e --- /dev/null +++ b/src/Symfony/Component/Security/Authentication/AuthenticationTrustResolverInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface for resolving the authentication status of a given token. + * + * @author Johannes M. Schmitt + */ +interface AuthenticationTrustResolverInterface +{ + /** + * Resolves whether the passed token implementation is authenticated + * anonymously. + * + * If null is passed, the method must return false. + * + * @param TokenInterface $token + * + * @return Boolean + */ + function isAnonymous(TokenInterface $token = null); + + /** + * Resolves whether the passed token implementation is authenticated + * using remember-me capabilities. + * + * @param TokenInterface $token + * + * @return Boolean + */ + function isRememberMe(TokenInterface $token = null); + + /** + * Resolves whether the passed token implementation is fully authenticated. + * + * @param TokenInterface $token + * + * @return Boolean + */ + function isFullFledged(TokenInterface $token = null); +} diff --git a/src/Symfony/Component/Security/Authentication/Token/RememberMeToken.php b/src/Symfony/Component/Security/Authentication/Token/RememberMeToken.php new file mode 100644 index 000000000000..587222d9b065 --- /dev/null +++ b/src/Symfony/Component/Security/Authentication/Token/RememberMeToken.php @@ -0,0 +1,56 @@ + + */ +class RememberMeToken extends Token +{ + protected $key; + + /** + * The persistent token which resulted in this authentication token. + * + * @var PersistentTokenInterface + */ + protected $persistentToken; + + /** + * Constructor. + * + * @param string $username + * @param string $key + */ + public function __construct(AccountInterface $user, $key) { + parent::__construct($user->getRoles()); + + if (0 === strlen($key)) { + throw new \InvalidArgumentException('$key cannot be empty.'); + } + + $this->user = $user; + $this->key = $key; + $this->setAuthenticated(true); + } + + public function getKey() + { + return $this->key; + } + + public function setPersistentToken(PersistentTokenInterface $persistentToken) + { + $this->persistentToken = $persistentToken; + } + + public function getPersistentToken() + { + return $this->persistentToken; + } +} diff --git a/src/Symfony/Component/Security/Authorization/Voter/AuthenticatedVoter.php b/src/Symfony/Component/Security/Authorization/Voter/AuthenticatedVoter.php index 4b330f908c95..bf78626e6f97 100644 --- a/src/Symfony/Component/Security/Authorization/Voter/AuthenticatedVoter.php +++ b/src/Symfony/Component/Security/Authorization/Voter/AuthenticatedVoter.php @@ -2,8 +2,8 @@ namespace Symfony\Component\Security\Authorization\Voter; +use Symfony\Component\Security\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Authentication\Token\AnonymousToken; /* * This file is part of the Symfony package. @@ -15,22 +15,40 @@ */ /** - * AuthenticatedVoter votes if an attribute like IS_AUTHENTICATED_FULLY or - * IS_AUTHENTICATED_ANONYMOUSLY is present. + * AuthenticatedVoter votes if an attribute like IS_AUTHENTICATED_FULLY, + * IS_AUTHENTICATED_REMEMBERED, or IS_AUTHENTICATED_ANONYMOUSLY is present. + * + * This list is most restrictive to least restrictive checking. * * @author Fabien Potencier + * @author Johannes M. Schmitt */ class AuthenticatedVoter implements VoterInterface { const IS_AUTHENTICATED_FULLY = 'IS_AUTHENTICATED_FULLY'; + const IS_AUTHENTICATED_REMEMBERED = 'IS_AUTHENTICATED_REMEMBERED'; const IS_AUTHENTICATED_ANONYMOUSLY = 'IS_AUTHENTICATED_ANONYMOUSLY'; + protected $authenticationTrustResolver; + + /** + * Constructor. + * + * @param AuthenticationTrustResolverInterface $authenticationTrustResolver + * + * @return void + */ + public function __construct(AuthenticationTrustResolverInterface $authenticationTrustResolver) + { + $this->authenticationTrustResolver = $authenticationTrustResolver; + } + /** * {@inheritdoc} */ public function supportsAttribute($attribute) { - return null !== $attribute && (self::IS_AUTHENTICATED_FULLY === $attribute || self::IS_AUTHENTICATED_ANONYMOUSLY === $attribute); + return null !== $attribute && (self::IS_AUTHENTICATED_FULLY === $attribute || self::IS_AUTHENTICATED_REMEMBERED === $attribute || self::IS_AUTHENTICATED_ANONYMOUSLY === $attribute); } /** @@ -54,11 +72,21 @@ public function vote(TokenInterface $token, $object, array $attributes) $result = VoterInterface::ACCESS_DENIED; - if (self::IS_AUTHENTICATED_FULLY === $attribute && !$token instanceof AnonymousToken) { + if (self::IS_AUTHENTICATED_FULLY === $attribute + && $this->authenticationTrustResolver->isFullFledged($token)) { + return VoterInterface::ACCESS_GRANTED; + } + + if (self::IS_AUTHENTICATED_REMEMBERED === $attribute + && ($this->authenticationTrustResolver->isRememberMe($token) + || $this->authenticationTrustResolver->isFullFledged($token))) { return VoterInterface::ACCESS_GRANTED; } - if (self::IS_AUTHENTICATED_ANONYMOUSLY === $attribute) { + if (self::IS_AUTHENTICATED_ANONYMOUSLY === $attribute + && ($this->authenticationTrustResolver->isAnonymous($token) + || $this->authenticationTrustResolver->isRememberMe($token) + || $this->authenticationTrustResolver->isFullFledged($token))) { return VoterInterface::ACCESS_GRANTED; } } diff --git a/tests/Symfony/Tests/Component/Security/Authentication/AuthenticationTrustResolverTest.php b/tests/Symfony/Tests/Component/Security/Authentication/AuthenticationTrustResolverTest.php new file mode 100644 index 000000000000..4cab497ce4e1 --- /dev/null +++ b/tests/Symfony/Tests/Component/Security/Authentication/AuthenticationTrustResolverTest.php @@ -0,0 +1,63 @@ +getResolver(); + + $this->assertFalse($resolver->isAnonymous(null)); + $this->assertFalse($resolver->isAnonymous($this->getToken())); + $this->assertFalse($resolver->isAnonymous($this->getRememberMeToken())); + $this->assertTrue($resolver->isAnonymous($this->getAnonymousToken())); + } + + public function testIsRememberMe() + { + $resolver = $this->getResolver(); + + $this->assertFalse($resolver->isRememberMe(null)); + $this->assertFalse($resolver->isRememberMe($this->getToken())); + $this->assertFalse($resolver->isRememberMe($this->getAnonymousToken())); + $this->assertTrue($resolver->isRememberMe($this->getRememberMeToken())); + } + + public function testisFullFledged() + { + $resolver = $this->getResolver(); + + $this->assertFalse($resolver->isFullFledged(null)); + $this->assertFalse($resolver->isFullFledged($this->getAnonymousToken())); + $this->assertFalse($resolver->isFullFledged($this->getRememberMeToken())); + $this->assertTrue($resolver->isFullFledged($this->getToken())); + } + + protected function getToken() + { + return $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + } + + protected function getAnonymousToken() + { + return $this->getMock('Symfony\Component\Security\Authentication\Token\AnonymousToken', null, array('', '')); + } + + protected function getRememberMeToken() + { + return $this->getMock('Symfony\Component\Security\Authentication\Token\RememberMeToken', array('setPersistent'), array(), '', false); + } + + protected function getResolver() + { + return new AuthenticationTrustResolver( + 'Symfony\\Component\\Security\\Authentication\\Token\\AnonymousToken', + 'Symfony\\Component\\Security\\Authentication\\Token\\RememberMeToken' + ); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Security/Authorization/Voter/AuthenticatedVoterTest.php b/tests/Symfony/Tests/Component/Security/Authorization/Voter/AuthenticatedVoterTest.php index 81b659ea8f2e..03563e660831 100644 --- a/tests/Symfony/Tests/Component/Security/Authorization/Voter/AuthenticatedVoterTest.php +++ b/tests/Symfony/Tests/Component/Security/Authorization/Voter/AuthenticatedVoterTest.php @@ -10,6 +10,7 @@ namespace Symfony\Tests\Component\Security\Authorization\Voter; +use Symfony\Component\Security\Authentication\AuthenticationTrustResolver; use Symfony\Component\Security\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Role\Role; @@ -18,7 +19,7 @@ class AuthenticatedVoterTest extends \PHPUnit_Framework_TestCase { public function testSupportsClass() { - $voter = new AuthenticatedVoter(); + $voter = new AuthenticatedVoter($this->getResolver()); $this->assertTrue($voter->supportsClass('stdClass')); } @@ -27,7 +28,7 @@ public function testSupportsClass() */ public function testVote($authenticated, $attributes, $expected) { - $voter = new AuthenticatedVoter(); + $voter = new AuthenticatedVoter($this->getResolver()); $this->assertSame($expected, $voter->vote($this->getToken($authenticated), null, $attributes)); } @@ -35,23 +36,41 @@ public function testVote($authenticated, $attributes, $expected) public function getVoteTests() { return array( - array(true, array(), VoterInterface::ACCESS_ABSTAIN), - array(true, array('FOO'), VoterInterface::ACCESS_ABSTAIN), - array(false, array(), VoterInterface::ACCESS_ABSTAIN), - array(false, array('FOO'), VoterInterface::ACCESS_ABSTAIN), + array('fully', array(), VoterInterface::ACCESS_ABSTAIN), + array('fully', array('FOO'), VoterInterface::ACCESS_ABSTAIN), + array('remembered', array(), VoterInterface::ACCESS_ABSTAIN), + array('remembered', array('FOO'), VoterInterface::ACCESS_ABSTAIN), + array('anonymously', array(), VoterInterface::ACCESS_ABSTAIN), + array('anonymously', array('FOO'), VoterInterface::ACCESS_ABSTAIN), - array(true, array('IS_AUTHENTICATED_ANONYMOUSLY'), VoterInterface::ACCESS_GRANTED), - array(false, array('IS_AUTHENTICATED_ANONYMOUSLY'), VoterInterface::ACCESS_GRANTED), + array('fully', array('IS_AUTHENTICATED_ANONYMOUSLY'), VoterInterface::ACCESS_GRANTED), + array('remembered', array('IS_AUTHENTICATED_ANONYMOUSLY'), VoterInterface::ACCESS_GRANTED), + array('anonymously', array('IS_AUTHENTICATED_ANONYMOUSLY'), VoterInterface::ACCESS_GRANTED), - array(true, array('IS_AUTHENTICATED_FULLY'), VoterInterface::ACCESS_GRANTED), - array(false, array('IS_AUTHENTICATED_FULLY'), VoterInterface::ACCESS_DENIED), + array('fully', array('IS_AUTHENTICATED_REMEMBERED'), VoterInterface::ACCESS_GRANTED), + array('remembered', array('IS_AUTHENTICATED_REMEMBERED'), VoterInterface::ACCESS_GRANTED), + array('anonymously', array('IS_AUTHENTICATED_REMEMBERED'), VoterInterface::ACCESS_DENIED), + + array('fully', array('IS_AUTHENTICATED_FULLY'), VoterInterface::ACCESS_GRANTED), + array('remembered', array('IS_AUTHENTICATED_FULLY'), VoterInterface::ACCESS_DENIED), + array('anonymously', array('IS_AUTHENTICATED_FULLY'), VoterInterface::ACCESS_DENIED), + ); + } + + protected function getResolver() + { + return new AuthenticationTrustResolver( + 'Symfony\\Component\\Security\\Authentication\\Token\\AnonymousToken', + 'Symfony\\Component\\Security\\Authentication\\Token\\RememberMeToken' ); } protected function getToken($authenticated) { - if ($authenticated) { + if ('fully' === $authenticated) { return $this->getMock('Symfony\Component\Security\Authentication\Token\TokenInterface'); + } else if ('remembered' === $authenticated) { + return $this->getMock('Symfony\Component\Security\Authentication\Token\RememberMeToken', array('setPersistent'), array(), '', false); } else { return $this->getMock('Symfony\Component\Security\Authentication\Token\AnonymousToken', null, array('', '')); }