diff options
Diffstat (limited to 'Core/Authorization')
-rw-r--r-- | Core/Authorization/AccessDecisionManager.php | 13 | ||||
-rw-r--r-- | Core/Authorization/AuthorizationChecker.php | 70 | ||||
-rw-r--r-- | Core/Authorization/AuthorizationCheckerInterface.php | 30 | ||||
-rw-r--r-- | Core/Authorization/ExpressionLanguage.php | 33 | ||||
-rw-r--r-- | Core/Authorization/ExpressionLanguageProvider.php | 58 | ||||
-rw-r--r-- | Core/Authorization/Voter/AbstractVoter.php | 113 | ||||
-rw-r--r-- | Core/Authorization/Voter/ExpressionVoter.php | 118 |
7 files changed, 433 insertions, 2 deletions
diff --git a/Core/Authorization/AccessDecisionManager.php b/Core/Authorization/AccessDecisionManager.php index 6e5effb..b8b6a77 100644 --- a/Core/Authorization/AccessDecisionManager.php +++ b/Core/Authorization/AccessDecisionManager.php @@ -22,6 +22,10 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; */ class AccessDecisionManager implements AccessDecisionManagerInterface { + const STRATEGY_AFFIRMATIVE = 'affirmative'; + const STRATEGY_CONSENSUS = 'consensus'; + const STRATEGY_UNANIMOUS = 'unanimous'; + private $voters; private $strategy; private $allowIfAllAbstainDecisions; @@ -37,14 +41,19 @@ class AccessDecisionManager implements AccessDecisionManagerInterface * * @throws \InvalidArgumentException */ - public function __construct(array $voters, $strategy = 'affirmative', $allowIfAllAbstainDecisions = false, $allowIfEqualGrantedDeniedDecisions = true) + public function __construct(array $voters, $strategy = self::STRATEGY_AFFIRMATIVE, $allowIfAllAbstainDecisions = false, $allowIfEqualGrantedDeniedDecisions = true) { if (!$voters) { throw new \InvalidArgumentException('You must at least add one voter.'); } + $strategyMethod = 'decide'.ucfirst($strategy); + if (!is_callable(array($this, $strategyMethod))) { + throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy)); + } + $this->voters = $voters; - $this->strategy = 'decide'.ucfirst($strategy); + $this->strategy = $strategyMethod; $this->allowIfAllAbstainDecisions = (bool) $allowIfAllAbstainDecisions; $this->allowIfEqualGrantedDeniedDecisions = (bool) $allowIfEqualGrantedDeniedDecisions; } 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 new file mode 100644 index 0000000..c2925af --- /dev/null +++ b/Core/Authorization/ExpressionLanguage.php @@ -0,0 +1,33 @@ +<?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\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 +{ + public function __construct(ParserCacheInterface $cache = null, array $providers = array()) + { + // prepend the default provider to let users override it easily + array_unshift($providers, new ExpressionLanguageProvider()); + + 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 new file mode 100644 index 0000000..98b8f50 --- /dev/null +++ b/Core/Authorization/Voter/ExpressionVoter.php @@ -0,0 +1,118 @@ +<?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\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; + +/** + * ExpressionVoter votes based on the evaluation of an expression. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class ExpressionVoter implements VoterInterface +{ + private $expressionLanguage; + private $trustResolver; + private $roleHierarchy; + + /** + * Constructor. + * + * @param ExpressionLanguage $expressionLanguage + * @param AuthenticationTrustResolverInterface $trustResolver + * @param RoleHierarchyInterface|null $roleHierarchy + */ + public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null) + { + $this->expressionLanguage = $expressionLanguage; + $this->trustResolver = $trustResolver; + $this->roleHierarchy = $roleHierarchy; + } + + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguage->registerProvider($provider); + } + + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return $attribute instanceof Expression; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + $result = VoterInterface::ACCESS_ABSTAIN; + $variables = null; + foreach ($attributes as $attribute) { + if (!$this->supportsAttribute($attribute)) { + continue; + } + + if (null === $variables) { + $variables = $this->getVariables($token, $object); + } + + $result = VoterInterface::ACCESS_DENIED; + if ($this->expressionLanguage->evaluate($attribute, $variables)) { + return VoterInterface::ACCESS_GRANTED; + } + } + + return $result; + } + + private function getVariables(TokenInterface $token, $object) + { + if (null !== $this->roleHierarchy) { + $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); + } else { + $roles = $token->getRoles(); + } + + $variables = array( + 'token' => $token, + 'user' => $token->getUser(), + 'object' => $object, + 'roles' => array_map(function ($role) { return $role->getRole(); }, $roles), + 'trust_resolver' => $this->trustResolver, + ); + + // this is mainly to propose a better experience when the expression is used + // in an access control rule, as the developer does not know that it's going + // to be handled by this voter + if ($object instanceof Request) { + $variables['request'] = $object; + } + + return $variables; + } +} |