diff options
Diffstat (limited to 'Core')
23 files changed, 843 insertions, 148 deletions
diff --git a/Core/Authentication/Provider/UserAuthenticationProvider.php b/Core/Authentication/Provider/UserAuthenticationProvider.php index b948135..55ebed4 100644 --- a/Core/Authentication/Provider/UserAuthenticationProvider.php +++ b/Core/Authentication/Provider/UserAuthenticationProvider.php @@ -70,7 +70,7 @@ abstract class UserAuthenticationProvider implements AuthenticationProviderInter $user = $this->retrieveUser($username, $token); } catch (UsernameNotFoundException $notFound) { if ($this->hideUserNotFoundExceptions) { - throw new BadCredentialsException('Bad credentials', 0, $notFound); + throw new BadCredentialsException('Bad credentials.', 0, $notFound); } $notFound->setUsername($username); @@ -87,7 +87,7 @@ abstract class UserAuthenticationProvider implements AuthenticationProviderInter $this->userChecker->checkPostAuth($user); } catch (BadCredentialsException $e) { if ($this->hideUserNotFoundExceptions) { - throw new BadCredentialsException('Bad credentials', 0, $e); + throw new BadCredentialsException('Bad credentials.', 0, $e); } throw $e; diff --git a/Core/Authentication/Token/Storage/TokenStorage.php b/Core/Authentication/Token/Storage/TokenStorage.php new file mode 100644 index 0000000..4b6c11f --- /dev/null +++ b/Core/Authentication/Token/Storage/TokenStorage.php @@ -0,0 +1,43 @@ +<?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\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * TokenStorage contains a TokenInterface + * + * It gives access to the token representing the current user authentication. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class TokenStorage implements TokenStorageInterface +{ + private $token; + + /** + * {@inheritdoc} + */ + public function getToken() + { + return $this->token; + } + + /** + * {@inheritdoc} + */ + public function setToken(TokenInterface $token = null) + { + $this->token = $token; + } +} diff --git a/Core/Authentication/Token/Storage/TokenStorageInterface.php b/Core/Authentication/Token/Storage/TokenStorageInterface.php new file mode 100644 index 0000000..218d750 --- /dev/null +++ b/Core/Authentication/Token/Storage/TokenStorageInterface.php @@ -0,0 +1,36 @@ +<?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\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * The TokenStorageInterface. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +interface TokenStorageInterface +{ + /** + * Returns the current security token. + * + * @return TokenInterface|null A TokenInterface instance or null if no authentication information is available + */ + public function getToken(); + + /** + * Sets the authentication token. + * + * @param TokenInterface $token A TokenInterface token, or null if no further authentication information should be stored + */ + public function setToken(TokenInterface $token = null); +} diff --git a/Core/Authorization/AuthorizationChecker.php b/Core/Authorization/AuthorizationChecker.php new file mode 100644 index 0000000..23c190c --- /dev/null +++ b/Core/Authorization/AuthorizationChecker.php @@ -0,0 +1,70 @@ +<?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; + +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; + +/** + * AuthorizationChecker is the main authorization point of the Security component. + * + * It gives access to the token representing the current user authentication. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class AuthorizationChecker implements AuthorizationCheckerInterface +{ + private $tokenStorage; + private $accessDecisionManager; + private $authenticationManager; + private $alwaysAuthenticate; + + /** + * Constructor. + * + * @param TokenStorageInterface $tokenStorage + * @param AuthenticationManagerInterface $authenticationManager An AuthenticationManager instance + * @param AccessDecisionManagerInterface $accessDecisionManager An AccessDecisionManager instance + * @param bool $alwaysAuthenticate + */ + public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, $alwaysAuthenticate = false) + { + $this->tokenStorage = $tokenStorage; + $this->authenticationManager = $authenticationManager; + $this->accessDecisionManager = $accessDecisionManager; + $this->alwaysAuthenticate = $alwaysAuthenticate; + } + + /** + * {@inheritdoc} + * + * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token. + */ + final public function isGranted($attributes, $object = null) + { + if (null === ($token = $this->tokenStorage->getToken())) { + throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.'); + } + + if ($this->alwaysAuthenticate || !$token->isAuthenticated()) { + $this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token)); + } + + if (!is_array($attributes)) { + $attributes = array($attributes); + } + + return $this->accessDecisionManager->decide($token, $attributes, $object); + } +} diff --git a/Core/Authorization/AuthorizationCheckerInterface.php b/Core/Authorization/AuthorizationCheckerInterface.php new file mode 100644 index 0000000..bd24d6f --- /dev/null +++ b/Core/Authorization/AuthorizationCheckerInterface.php @@ -0,0 +1,30 @@ +<?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; + +/** + * The AuthorizationCheckerInterface. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +interface AuthorizationCheckerInterface +{ + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied object. + * + * @param mixed $attributes + * @param mixed $object + * + * @return bool + */ + public function isGranted($attributes, $object = null); +} diff --git a/Core/Authorization/ExpressionLanguage.php b/Core/Authorization/ExpressionLanguage.php index f9012b7..c2925af 100644 --- a/Core/Authorization/ExpressionLanguage.php +++ b/Core/Authorization/ExpressionLanguage.php @@ -12,46 +12,22 @@ namespace Symfony\Component\Security\Core\Authorization; use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; +use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; /** * Adds some function to the default ExpressionLanguage. * * @author Fabien Potencier <fabien@symfony.com> + * + * @see ExpressionLanguageProvider */ class ExpressionLanguage extends BaseExpressionLanguage { - protected function registerFunctions() + public function __construct(ParserCacheInterface $cache = null, array $providers = array()) { - parent::registerFunctions(); - - $this->register('is_anonymous', function () { - return '$trust_resolver->isAnonymous($token)'; - }, function (array $variables) { - return $variables['trust_resolver']->isAnonymous($variables['token']); - }); - - $this->register('is_authenticated', function () { - return '$token && !$trust_resolver->isAnonymous($token)'; - }, function (array $variables) { - return $variables['token'] && !$variables['trust_resolver']->isAnonymous($variables['token']); - }); - - $this->register('is_fully_authenticated', function () { - return '$trust_resolver->isFullFledged($token)'; - }, function (array $variables) { - return $variables['trust_resolver']->isFullFledged($variables['token']); - }); - - $this->register('is_remember_me', function () { - return '$trust_resolver->isRememberMe($token)'; - }, function (array $variables) { - return $variables['trust_resolver']->isRememberMe($variables['token']); - }); + // prepend the default provider to let users override it easily + array_unshift($providers, new ExpressionLanguageProvider()); - $this->register('has_role', function ($role) { - return sprintf('in_array(%s, $roles)', $role); - }, function (array $variables, $role) { - return in_array($role, $variables['roles']); - }); + parent::__construct($cache, $providers); } } diff --git a/Core/Authorization/ExpressionLanguageProvider.php b/Core/Authorization/ExpressionLanguageProvider.php new file mode 100644 index 0000000..9293ba7 --- /dev/null +++ b/Core/Authorization/ExpressionLanguageProvider.php @@ -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\Authorization; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + +/** + * Define some ExpressionLanguage functions. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + public function getFunctions() + { + return array( + new ExpressionFunction('is_anonymous', function () { + return '$trust_resolver->isAnonymous($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isAnonymous($variables['token']); + }), + + new ExpressionFunction('is_authenticated', function () { + return '$token && !$trust_resolver->isAnonymous($token)'; + }, function (array $variables) { + return $variables['token'] && !$variables['trust_resolver']->isAnonymous($variables['token']); + }), + + new ExpressionFunction('is_fully_authenticated', function () { + return '$trust_resolver->isFullFledged($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isFullFledged($variables['token']); + }), + + new ExpressionFunction('is_remember_me', function () { + return '$trust_resolver->isRememberMe($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isRememberMe($variables['token']); + }), + + new ExpressionFunction('has_role', function ($role) { + return sprintf('in_array(%s, $roles)', $role); + }, function (array $variables, $role) { + return in_array($role, $variables['roles']); + }), + ); + } +} diff --git a/Core/Authorization/Voter/AbstractVoter.php b/Core/Authorization/Voter/AbstractVoter.php new file mode 100644 index 0000000..43ca558 --- /dev/null +++ b/Core/Authorization/Voter/AbstractVoter.php @@ -0,0 +1,113 @@ +<?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\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Abstract Voter implementation that reduces boilerplate code required to create a custom Voter + * + * @author Roman Marintšenko <inoryy@gmail.com> + */ +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 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 + * @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 (!$object || !$this->supportsClass(get_class($object))) { + return self::ACCESS_ABSTAIN; + } + + // abstain vote by default in case none of the attributes are supported + $vote = self::ACCESS_ABSTAIN; + + foreach ($attributes as $attribute) { + 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 $vote; + } + + /** + * Return an array of supported classes. This will be called by supportsClass + * + * @return array an array of supported classes, i.e. array('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. array('CREATE', 'READ') + */ + abstract protected function getSupportedAttributes(); + + /** + * 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) + * + * @param string $attribute + * @param object $object + * @param UserInterface|string $user + * + * @return bool + */ + abstract protected function isGranted($attribute, $object, $user = null); +} diff --git a/Core/Authorization/Voter/ExpressionVoter.php b/Core/Authorization/Voter/ExpressionVoter.php index 3263803..98b8f50 100644 --- a/Core/Authorization/Voter/ExpressionVoter.php +++ b/Core/Authorization/Voter/ExpressionVoter.php @@ -15,6 +15,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Request; @@ -43,6 +44,11 @@ class ExpressionVoter implements VoterInterface $this->roleHierarchy = $roleHierarchy; } + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguage->registerProvider($provider); + } + /** * {@inheritdoc} */ diff --git a/Core/Encoder/UserPasswordEncoder.php b/Core/Encoder/UserPasswordEncoder.php new file mode 100644 index 0000000..13ee835 --- /dev/null +++ b/Core/Encoder/UserPasswordEncoder.php @@ -0,0 +1,55 @@ +<?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\Encoder; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * A generic password encoder + * + * @author Ariel Ferrandini <arielferrandini@gmail.com> + */ +class UserPasswordEncoder implements UserPasswordEncoderInterface +{ + /** + * @var EncoderFactoryInterface + */ + private $encoderFactory; + + /** + * @param EncoderFactoryInterface $encoderFactory The encoder factory + */ + public function __construct(EncoderFactoryInterface $encoderFactory) + { + $this->encoderFactory = $encoderFactory; + } + + /** + * {@inheritdoc} + */ + public function encodePassword(UserInterface $user, $plainPassword) + { + $encoder = $this->encoderFactory->getEncoder($user); + + return $encoder->encodePassword($plainPassword, $user->getSalt()); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid(UserInterface $user, $raw) + { + $encoder = $this->encoderFactory->getEncoder($user); + + return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt()); + } +} diff --git a/Core/Encoder/UserPasswordEncoderInterface.php b/Core/Encoder/UserPasswordEncoderInterface.php new file mode 100644 index 0000000..39e906a --- /dev/null +++ b/Core/Encoder/UserPasswordEncoderInterface.php @@ -0,0 +1,41 @@ +<?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\Encoder; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * UserPasswordEncoderInterface is the interface for the password encoder service. + * + * @author Ariel Ferrandini <arielferrandini@gmail.com> + */ +interface UserPasswordEncoderInterface +{ + /** + * + * Encodes the plain password. + * + * @param UserInterface $user The user + * @param string $plainPassword The password to encode + * + * @return string The encoded password + */ + public function encodePassword(UserInterface $user, $plainPassword); + + /** + * @param UserInterface $user The user + * @param string $raw A raw password + * + * @return bool true if the password is valid, false otherwise + */ + public function isPasswordValid(UserInterface $user, $raw); +} diff --git a/Core/Exception/UsernameNotFoundException.php b/Core/Exception/UsernameNotFoundException.php index 11607d3..6979389 100644 --- a/Core/Exception/UsernameNotFoundException.php +++ b/Core/Exception/UsernameNotFoundException.php @@ -69,4 +69,12 @@ class UsernameNotFoundException extends AuthenticationException parent::unserialize($parentData); } + + /** + * {@inheritdoc} + */ + public function getMessageData() + { + return array('{{ username }}' => $this->username); + } } diff --git a/Core/README.md b/Core/README.md index 4585a5d..66c323e 100644 --- a/Core/README.md +++ b/Core/README.md @@ -11,7 +11,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/Core/Security.php b/Core/Security.php new file mode 100644 index 0000000..14d32f8 --- /dev/null +++ b/Core/Security.php @@ -0,0 +1,24 @@ +<?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; + +/** + * This class holds security information. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +final class Security +{ + const ACCESS_DENIED_ERROR = '_security.403_error'; + const AUTHENTICATION_ERROR = '_security.last_error'; + const LAST_USERNAME = '_security.last_username'; +} diff --git a/Core/SecurityContext.php b/Core/SecurityContext.php index 0326f1d..165c22a 100644 --- a/Core/SecurityContext.php +++ b/Core/SecurityContext.php @@ -11,10 +11,13 @@ namespace Symfony\Component\Security\Core; -use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; /** * SecurityContext is the main entry point of the Security component. @@ -23,48 +26,47 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; * * @author Fabien Potencier <fabien@symfony.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com> + * @deprecated Deprecated since version 2.6, to be removed in 3.0. */ class SecurityContext implements SecurityContextInterface { - private $token; - private $accessDecisionManager; - private $authenticationManager; - private $alwaysAuthenticate; + /** + * @var TokenStorageInterface + */ + private $tokenStorage; /** - * Constructor. - * - * @param AuthenticationManagerInterface $authenticationManager An AuthenticationManager instance - * @param AccessDecisionManagerInterface|null $accessDecisionManager An AccessDecisionManager instance - * @param bool $alwaysAuthenticate + * @var AuthorizationCheckerInterface */ - public function __construct(AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, $alwaysAuthenticate = false) - { - $this->authenticationManager = $authenticationManager; - $this->accessDecisionManager = $accessDecisionManager; - $this->alwaysAuthenticate = $alwaysAuthenticate; - } + private $authorizationChecker; /** - * {@inheritdoc} + * For backwards compatibility, the signature of sf <2.6 still works * - * @throws AuthenticationCredentialsNotFoundException when the security context has no authentication token. + * @param TokenStorageInterface|AuthenticationManagerInterface $tokenStorage + * @param AuthorizationCheckerInterface|AccessDecisionManagerInterface $authorizationChecker + * @param bool $alwaysAuthenticate only applicable with old signature */ - final public function isGranted($attributes, $object = null) + public function __construct($tokenStorage, $authorizationChecker, $alwaysAuthenticate = false) { - if (null === $this->token) { - throw new AuthenticationCredentialsNotFoundException('The security context contains no authentication token. One possible reason may be that there is no firewall configured for this URL.'); - } + $oldSignature = $tokenStorage instanceof AuthenticationManagerInterface && $authorizationChecker instanceof AccessDecisionManagerInterface; + $newSignature = $tokenStorage instanceof TokenStorageInterface && $authorizationChecker instanceof AuthorizationCheckerInterface; - if ($this->alwaysAuthenticate || !$this->token->isAuthenticated()) { - $this->token = $this->authenticationManager->authenticate($this->token); + // confirm possible signatures + if (!$oldSignature && !$newSignature) { + throw new \BadMethodCallException('Unable to construct SecurityContext, please provide the correct arguments'); } - if (!is_array($attributes)) { - $attributes = array($attributes); + if ($oldSignature) { + // renamed for clearity + $authenticationManager = $tokenStorage; + $accessDecisionManager = $authorizationChecker; + $tokenStorage = new TokenStorage(); + $authorizationChecker = new AuthorizationChecker($tokenStorage, $authenticationManager, $accessDecisionManager, $alwaysAuthenticate); } - return $this->accessDecisionManager->decide($this->token, $attributes, $object); + $this->tokenStorage = $tokenStorage; + $this->authorizationChecker = $authorizationChecker; } /** @@ -72,7 +74,7 @@ class SecurityContext implements SecurityContextInterface */ public function getToken() { - return $this->token; + return $this->tokenStorage->getToken(); } /** @@ -80,6 +82,14 @@ class SecurityContext implements SecurityContextInterface */ public function setToken(TokenInterface $token = null) { - $this->token = $token; + return $this->tokenStorage->setToken($token); + } + + /** + * {@inheritdoc} + */ + public function isGranted($attributes, $object = null) + { + return $this->authorizationChecker->isGranted($attributes, $object); } } diff --git a/Core/SecurityContextInterface.php b/Core/SecurityContextInterface.php index 50c30bb..bceb506 100644 --- a/Core/SecurityContextInterface.php +++ b/Core/SecurityContextInterface.php @@ -11,40 +11,18 @@ namespace Symfony\Component\Security\Core; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; /** * The SecurityContextInterface. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> + * @deprecated Deprecated since version 2.6, to be removed in 3.0. */ -interface SecurityContextInterface +interface SecurityContextInterface extends TokenStorageInterface, AuthorizationCheckerInterface { - const ACCESS_DENIED_ERROR = '_security.403_error'; - const AUTHENTICATION_ERROR = '_security.last_error'; - const LAST_USERNAME = '_security.last_username'; - - /** - * Returns the current security token. - * - * @return TokenInterface|null A TokenInterface instance or null if no authentication information is available - */ - public function getToken(); - - /** - * Sets the authentication token. - * - * @param TokenInterface $token A TokenInterface token, or null if no further authentication information should be stored - */ - public function setToken(TokenInterface $token = null); - - /** - * Checks if the attributes are granted against the current authentication token and optionally supplied object. - * - * @param mixed $attributes - * @param mixed $object - * - * @return bool - */ - public function isGranted($attributes, $object = null); + const ACCESS_DENIED_ERROR = Security::ACCESS_DENIED_ERROR; + const AUTHENTICATION_ERROR = Security::AUTHENTICATION_ERROR; + const LAST_USERNAME = Security::LAST_USERNAME; } diff --git a/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php b/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php new file mode 100644 index 0000000..d06e3f0 --- /dev/null +++ b/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php @@ -0,0 +1,26 @@ +<?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\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; + +class TokenStorageTest extends \PHPUnit_Framework_TestCase +{ + public function testGetSetToken() + { + $tokenStorage = new TokenStorage(); + $this->assertNull($tokenStorage->getToken()); + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $tokenStorage->setToken($token); + $this->assertSame($token, $tokenStorage->getToken()); + } +} diff --git a/Core/Tests/Authorization/AuthorizationCheckerTest.php b/Core/Tests/Authorization/AuthorizationCheckerTest.php new file mode 100644 index 0000000..64de6ef --- /dev/null +++ b/Core/Tests/Authorization/AuthorizationCheckerTest.php @@ -0,0 +1,99 @@ +<?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; + +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; + +class AuthorizationCheckerTest extends \PHPUnit_Framework_TestCase +{ + private $authenticationManager; + private $accessDecisionManager; + private $authorizationChecker; + private $tokenStorage; + + public function setUp() + { + $this->authenticationManager = $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'); + $this->accessDecisionManager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); + $this->tokenStorage = new TokenStorage(); + + $this->authorizationChecker = new AuthorizationChecker( + $this->tokenStorage, + $this->authenticationManager, + $this->accessDecisionManager + ); + } + + public function testVoteAuthenticatesTokenIfNecessary() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $this->tokenStorage->setToken($token); + + $newToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $this->authenticationManager + ->expects($this->once()) + ->method('authenticate') + ->with($this->equalTo($token)) + ->will($this->returnValue($newToken)); + + // default with() isn't a strict check + $tokenComparison = function ($value) use ($newToken) { + // make sure that the new token is used in "decide()" and not the old one + return $value === $newToken; + }; + + $this->accessDecisionManager + ->expects($this->once()) + ->method('decide') + ->with($this->callback($tokenComparison)) + ->will($this->returnValue(true)); + + // first run the token has not been re-authenticated yet, after isGranted is called, it should be equal + $this->assertFalse($newToken === $this->tokenStorage->getToken()); + $this->assertTrue($this->authorizationChecker->isGranted('foo')); + $this->assertTrue($newToken === $this->tokenStorage->getToken()); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException + */ + public function testVoteWithoutAuthenticationToken() + { + $this->authorizationChecker->isGranted('ROLE_FOO'); + } + + /** + * @dataProvider isGrantedProvider + */ + public function testIsGranted($decide) + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->once()) + ->method('isAuthenticated') + ->will($this->returnValue(true)); + + $this->accessDecisionManager + ->expects($this->once()) + ->method('decide') + ->will($this->returnValue($decide)); + $this->tokenStorage->setToken($token); + $this->assertTrue($decide === $this->authorizationChecker->isGranted('ROLE_FOO')); + } + + public function isGrantedProvider() + { + return array(array(true), array(false)); + } +} diff --git a/Core/Tests/Encoder/UserPasswordEncoderTest.php b/Core/Tests/Encoder/UserPasswordEncoderTest.php new file mode 100644 index 0000000..590652d --- /dev/null +++ b/Core/Tests/Encoder/UserPasswordEncoderTest.php @@ -0,0 +1,70 @@ +<?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\Encoder; + +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder; + +class UserPasswordEncoderTest extends \PHPUnit_Framework_TestCase +{ + public function testEncodePassword() + { + $userMock = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); + $userMock->expects($this->any()) + ->method('getSalt') + ->will($this->returnValue('userSalt')); + + $mockEncoder = $this->getMock('Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface'); + $mockEncoder->expects($this->any()) + ->method('encodePassword') + ->with($this->equalTo('plainPassword'), $this->equalTo('userSalt')) + ->will($this->returnValue('encodedPassword')); + + $mockEncoderFactory = $this->getMock('Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface'); + $mockEncoderFactory->expects($this->any()) + ->method('getEncoder') + ->with($this->equalTo($userMock)) + ->will($this->returnValue($mockEncoder)); + + $passwordEncoder = new UserPasswordEncoder($mockEncoderFactory); + + $encoded = $passwordEncoder->encodePassword($userMock, 'plainPassword'); + $this->assertEquals('encodedPassword', $encoded); + } + + public function testIsPasswordValid() + { + $userMock = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); + $userMock->expects($this->any()) + ->method('getSalt') + ->will($this->returnValue('userSalt')); + $userMock->expects($this->any()) + ->method('getPassword') + ->will($this->returnValue('encodedPassword')); + + $mockEncoder = $this->getMock('Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface'); + $mockEncoder->expects($this->any()) + ->method('isPasswordValid') + ->with($this->equalTo('encodedPassword'), $this->equalTo('plainPassword'), $this->equalTo('userSalt')) + ->will($this->returnValue(true)); + + $mockEncoderFactory = $this->getMock('Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface'); + $mockEncoderFactory->expects($this->any()) + ->method('getEncoder') + ->with($this->equalTo($userMock)) + ->will($this->returnValue($mockEncoder)); + + $passwordEncoder = new UserPasswordEncoder($mockEncoderFactory); + + $isValid = $passwordEncoder->isPasswordValid($userMock, 'plainPassword'); + $this->assertTrue($isValid); + } +} diff --git a/Core/Tests/Exception/UsernameNotFoundExceptionTest.php b/Core/Tests/Exception/UsernameNotFoundExceptionTest.php new file mode 100644 index 0000000..98ea374 --- /dev/null +++ b/Core/Tests/Exception/UsernameNotFoundExceptionTest.php @@ -0,0 +1,25 @@ +<?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\Exception; + +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + +class UsernameNotFoundExceptionTest extends \PHPUnit_Framework_TestCase +{ + public function testGetMessageData() + { + $exception = new UsernameNotFoundException('Username could not be found.'); + $this->assertEquals(array('{{ username }}' => null), $exception->getMessageData()); + $exception->setUsername('username'); + $this->assertEquals(array('{{ username }}' => 'username'), $exception->getMessageData()); + } +} diff --git a/Core/Tests/SecurityContextTest.php b/Core/Tests/SecurityContextTest.php index dd0e2e3..886c596 100644 --- a/Core/Tests/SecurityContextTest.php +++ b/Core/Tests/SecurityContextTest.php @@ -11,82 +11,109 @@ namespace Symfony\Component\Security\Core\Tests; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\Security\Core\SecurityContext; class SecurityContextTest extends \PHPUnit_Framework_TestCase { - public function testVoteAuthenticatesTokenIfNecessary() + private $tokenStorage; + private $authorizationChecker; + private $securityContext; + + public function setUp() { - $authManager = $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'); - $decisionManager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); + $this->tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $this->authorizationChecker = $this->getMock('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'); + $this->securityContext = new SecurityContext($this->tokenStorage, $this->authorizationChecker); + } - $context = new SecurityContext($authManager, $decisionManager); - $context->setToken($token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); + public function testGetTokenDelegation() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); - $authManager + $this->tokenStorage ->expects($this->once()) - ->method('authenticate') - ->with($this->equalTo($token)) - ->will($this->returnValue($newToken = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'))) - ; + ->method('getToken') + ->will($this->returnValue($token)); - $decisionManager + $this->assertTrue($token === $this->securityContext->getToken()); + } + + public function testSetTokenDelegation() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $this->tokenStorage ->expects($this->once()) - ->method('decide') - ->will($this->returnValue(true)) - ; + ->method('setToken') + ->with($token); - $this->assertTrue($context->isGranted('foo')); - $this->assertSame($newToken, $context->getToken()); + $this->securityContext->setToken($token); } /** - * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException + * @dataProvider isGrantedDelegationProvider */ - public function testVoteWithoutAuthenticationToken() + public function testIsGrantedDelegation($attributes, $object, $return) + { + $this->authorizationChecker + ->expects($this->once()) + ->method('isGranted') + ->with($attributes, $object) + ->will($this->returnValue($return)); + + $this->assertEquals($return, $this->securityContext->isGranted($attributes, $object)); + } + + public function isGrantedDelegationProvider() { - $context = new SecurityContext( - $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'), - $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface') + return array( + array(array(), new \stdClass(), true), + array(array('henk'), new \stdClass(), false), + array(null, new \stdClass(), false), + array('henk', null, true), + array(array(1), 'henk', true), ); + } - $context->isGranted('ROLE_FOO'); + /** + * Test dedicated to check if the backwards compatibility is still working + */ + public function testOldConstructorSignature() + { + $authenticationManager = $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'); + $accessDecisionManager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); + new SecurityContext($authenticationManager, $accessDecisionManager); } - public function testIsGranted() + /** + * @dataProvider oldConstructorSignatureFailuresProvider + * @expectedException \BadMethodCallException + */ + public function testOldConstructorSignatureFailures($first, $second) { - $manager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); - $manager->expects($this->once())->method('decide')->will($this->returnValue(false)); - $context = new SecurityContext($this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'), $manager); - $context->setToken($token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); - $token - ->expects($this->once()) - ->method('isAuthenticated') - ->will($this->returnValue(true)) - ; - $this->assertFalse($context->isGranted('ROLE_FOO')); - - $manager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); - $manager->expects($this->once())->method('decide')->will($this->returnValue(true)); - $context = new SecurityContext($this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'), $manager); - $context->setToken($token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); - $token - ->expects($this->once()) - ->method('isAuthenticated') - ->will($this->returnValue(true)) - ; - $this->assertTrue($context->isGranted('ROLE_FOO')); + new SecurityContext($first, $second); } - public function testGetSetToken() + public function oldConstructorSignatureFailuresProvider() { - $context = new SecurityContext( - $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'), - $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface') - ); - $this->assertNull($context->getToken()); + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $authorizationChecker = $this->getMock('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'); + $authenticationManager = $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'); + $accessDecisionManager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); - $context->setToken($token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); - $this->assertSame($token, $context->getToken()); + return array( + array(new \stdClass(), new \stdClass()), + array($tokenStorage, $accessDecisionManager), + array($accessDecisionManager, $tokenStorage), + array($authorizationChecker, $accessDecisionManager), + array($accessDecisionManager, $authorizationChecker), + array($tokenStorage, $accessDecisionManager), + array($authenticationManager, $authorizationChecker), + array('henk', 'hans'), + array(null, false), + array(true, null), + ); } } diff --git a/Core/Validator/Constraints/UserPassword.php b/Core/Validator/Constraints/UserPassword.php index aee4cda..35537b3 100644 --- a/Core/Validator/Constraints/UserPassword.php +++ b/Core/Validator/Constraints/UserPassword.php @@ -19,7 +19,7 @@ use Symfony\Component\Validator\Constraint; */ class UserPassword extends Constraint { - public $message = 'This value should be the user current password.'; + public $message = 'This value should be the user\'s current password.'; public $service = 'security.validator.user_password'; /** diff --git a/Core/composer.json b/Core/composer.json index 16288c8..c6923bf 100644 --- a/Core/composer.json +++ b/Core/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "symfony/event-dispatcher": "~2.1", - "symfony/expression-language": "~2.4", + "symfony/expression-language": "~2.6", "symfony/http-foundation": "~2.4", "symfony/translation": "~2.0,>=2.0.5", "symfony/validator": "~2.5,>=2.5.5", @@ -41,7 +41,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } |