diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CustomAccessDecisionManagerPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CustomAccessDecisionManagerPass.php new file mode 100644 index 0000000000000..62b3f0c9a350f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CustomAccessDecisionManagerPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class CustomAccessDecisionManagerPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $serviceName = $container->getParameter('security.access.manager.service'); + + if ($container->hasDefinition('security.authorization_checker') && $container->hasDefinition($serviceName)) { + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $container->getParameterBag()->resolveValue($container->getDefinition($serviceName)->getClass()); + $refClass = new \ReflectionClass($class); + + if ($refClass->implementsInterface('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')) { + $definition = $container->getDefinition('security.authorization_checker'); + $definition->replaceArgument(2, new Reference($serviceName)); + } else { + throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "AccessDecisionManagerInterface".', $serviceName)); + } + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 7b6ac4186ac03..97bea12944add 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -68,11 +68,19 @@ public function getConfigTreeBuilder() ->booleanNode('hide_user_not_found')->defaultTrue()->end() ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() ->booleanNode('erase_credentials')->defaultTrue()->end() + ->arrayNode('authorization_checker') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('access_decision_manager_service') + ->defaultValue('security.access.decision_manager') + ->end() + ->end() + ->end() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() ->enumNode('strategy') - ->values(array(AccessDecisionManager::STRATEGY_AFFIRMATIVE, AccessDecisionManager::STRATEGY_CONSENSUS, AccessDecisionManager::STRATEGY_UNANIMOUS)) + ->values(array(AccessDecisionManager::STRATEGY_AFFIRMATIVE, AccessDecisionManager::STRATEGY_CONSENSUS, AccessDecisionManager::STRATEGY_UNANIMOUS, AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED)) ->defaultValue(AccessDecisionManager::STRATEGY_AFFIRMATIVE) ->end() ->booleanNode('allow_if_all_abstain')->defaultFalse()->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index e82e3ca8779bc..f5739af476f2c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -75,6 +75,11 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']); + + if(isset($config['authorization_checker']['access_decision_manager_service'])) { + $container->setParameter('security.access.manager.service', $config['authorization_checker']['access_decision_manager_service']); + } + $container ->getDefinition('security.access.decision_manager') ->addArgument($config['access_decision_manager']['strategy']) diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 72f7b68de959d..c41f16354f6f6 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\CustomAccessDecisionManagerPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory; @@ -47,5 +48,6 @@ public function build(ContainerBuilder $container) $extension->addUserProviderFactory(new InMemoryFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); + $container->addCompilerPass(new CustomAccessDecisionManagerPass()); } } diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php index e021cc73547c8..8cafa4b7bda6e 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Security\Core\Authorization; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager\AffirmativeAccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager\ConsensusAccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager\HighestNotAbstainedVoterAccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager\UnanimousAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -25,8 +29,8 @@ class AccessDecisionManager implements AccessDecisionManagerInterface const STRATEGY_AFFIRMATIVE = 'affirmative'; const STRATEGY_CONSENSUS = 'consensus'; const STRATEGY_UNANIMOUS = 'unanimous'; + const STRATEGY_HIGHEST_NOT_ABSTAINED = 'highest'; - private $voters; private $strategy; private $allowIfAllAbstainDecisions; private $allowIfEqualGrantedDeniedDecisions; @@ -43,15 +47,10 @@ class AccessDecisionManager implements AccessDecisionManagerInterface */ public function __construct(array $voters = array(), $strategy = self::STRATEGY_AFFIRMATIVE, $allowIfAllAbstainDecisions = false, $allowIfEqualGrantedDeniedDecisions = true) { - $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 = $strategyMethod; $this->allowIfAllAbstainDecisions = (bool) $allowIfAllAbstainDecisions; $this->allowIfEqualGrantedDeniedDecisions = (bool) $allowIfEqualGrantedDeniedDecisions; + $this->strategy = $this->createStrategy($strategy); + $this->setVoters($voters); } /** @@ -61,7 +60,7 @@ public function __construct(array $voters = array(), $strategy = self::STRATEGY_ */ public function setVoters(array $voters) { - $this->voters = $voters; + $this->strategy->setVoters($voters); } /** @@ -69,21 +68,7 @@ public function setVoters(array $voters) */ public function decide(TokenInterface $token, array $attributes, $object = null) { - return $this->{$this->strategy}($token, $attributes, $object); - } - - /** - * {@inheritdoc} - */ - public function supportsAttribute($attribute) - { - foreach ($this->voters as $voter) { - if ($voter->supportsAttribute($attribute)) { - return true; - } - } - - return false; + return $this->strategy->decide($token, $attributes, $object); } /** @@ -91,129 +76,34 @@ public function supportsAttribute($attribute) */ public function supportsClass($class) { - foreach ($this->voters as $voter) { - if ($voter->supportsClass($class)) { - return true; - } - } - - return false; - } - - /** - * Grants access if any voter returns an affirmative response. - * - * If all voters abstained from voting, the decision will be based on the - * allowIfAllAbstainDecisions property value (defaults to false). - */ - private function decideAffirmative(TokenInterface $token, array $attributes, $object = null) - { - $deny = 0; - foreach ($this->voters as $voter) { - $result = $voter->vote($token, $object, $attributes); - switch ($result) { - case VoterInterface::ACCESS_GRANTED: - return true; - - case VoterInterface::ACCESS_DENIED: - ++$deny; - - break; - - default: - break; - } - } - - if ($deny > 0) { - return false; - } - - return $this->allowIfAllAbstainDecisions; + return $this->strategy->supportsClass($class); } /** - * Grants access if there is consensus of granted against denied responses. - * - * Consensus means majority-rule (ignoring abstains) rather than unanimous - * agreement (ignoring abstains). If you require unanimity, see - * UnanimousBased. + * Checks if the access decision manager supports the given attribute. * - * If there were an equal number of grant and deny votes, the decision will - * be based on the allowIfEqualGrantedDeniedDecisions property value - * (defaults to true). + * @param string $attribute An attribute * - * If all voters abstained from voting, the decision will be based on the - * allowIfAllAbstainDecisions property value (defaults to false). + * @return bool true if this decision manager supports the attribute, false otherwise */ - private function decideConsensus(TokenInterface $token, array $attributes, $object = null) + public function supportsAttribute($attribute) { - $grant = 0; - $deny = 0; - foreach ($this->voters as $voter) { - $result = $voter->vote($token, $object, $attributes); - - switch ($result) { - case VoterInterface::ACCESS_GRANTED: - ++$grant; - - break; - - case VoterInterface::ACCESS_DENIED: - ++$deny; - - break; - } - } - - if ($grant > $deny) { - return true; - } - - if ($deny > $grant) { - return false; - } - - if ($grant > 0) { - return $this->allowIfEqualGrantedDeniedDecisions; - } - - return $this->allowIfAllAbstainDecisions; + return $this->strategy->supportsAttribute($attribute); } - /** - * Grants access if only grant (or abstain) votes were received. - * - * If all voters abstained from voting, the decision will be based on the - * allowIfAllAbstainDecisions property value (defaults to false). - */ - private function decideUnanimous(TokenInterface $token, array $attributes, $object = null) + private function createStrategy($strategyName) { - $grant = 0; - foreach ($attributes as $attribute) { - foreach ($this->voters as $voter) { - $result = $voter->vote($token, $object, array($attribute)); - - switch ($result) { - case VoterInterface::ACCESS_GRANTED: - ++$grant; - - break; - - case VoterInterface::ACCESS_DENIED: - return false; - - default: - break; - } - } + switch ($strategyName) { + case self::STRATEGY_UNANIMOUS: + return new UnanimousAccessDecisionManager($this->allowIfAllAbstainDecisions); + case self::STRATEGY_CONSENSUS: + return new ConsensusAccessDecisionManager($this->allowIfEqualGrantedDeniedDecisions, $this->allowIfAllAbstainDecisions); + case self::STRATEGY_AFFIRMATIVE: + return new AffirmativeAccessDecisionManager($this->allowIfAllAbstainDecisions); + case self::STRATEGY_HIGHEST_NOT_ABSTAINED: + return new HighestNotAbstainedVoterAccessDecisionManager($this->allowIfAllAbstainDecisions); + default: + throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategyName)); } - - // no deny votes - if ($grant > 0) { - return true; - } - - return $this->allowIfAllAbstainDecisions; } } diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AbstractAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AbstractAccessDecisionManager.php new file mode 100644 index 0000000000000..01beb82bcf511 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AbstractAccessDecisionManager.php @@ -0,0 +1,52 @@ +voters = $voters; + } + + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + foreach ($this->voters as $voter) { + if ($voter->supportsAttribute($attribute)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + foreach ($this->voters as $voter) { + if ($voter->supportsClass($class)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + abstract public function decide(TokenInterface $token, array $attributes, $object = null); +} diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AffirmativeAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AffirmativeAccessDecisionManager.php new file mode 100644 index 0000000000000..4ab82dc18c6b7 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/AffirmativeAccessDecisionManager.php @@ -0,0 +1,56 @@ +allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + /** + * {@inheritdoc} + */ + public function decide(TokenInterface $token, array $attributes, $object = null) + { + $deny = 0; + foreach ($this->voters as $voter) { + $result = $voter->vote($token, $object, $attributes); + switch ($result) { + case VoterInterface::ACCESS_GRANTED: + return true; + + case VoterInterface::ACCESS_DENIED: + ++$deny; + + break; + + default: + break; + } + } + + if ($deny > 0) { + return false; + } + + return $this->allowIfAllAbstainDecisions; + } +} diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/ConsensusAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/ConsensusAccessDecisionManager.php new file mode 100644 index 0000000000000..8158b83b10e20 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/ConsensusAccessDecisionManager.php @@ -0,0 +1,76 @@ +allowIfEqualGrantedDeniedDecisions = $allowIfEqualGrantedDeniedDecisions; + $this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + /** + * {@inheritdoc} + */ + public function decide(TokenInterface $token, array $attributes, $object = null) + { + $grant = 0; + $deny = 0; + foreach ($this->voters as $voter) { + $result = $voter->vote($token, $object, $attributes); + + switch ($result) { + case VoterInterface::ACCESS_GRANTED: + ++$grant; + + break; + case VoterInterface::ACCESS_DENIED: + ++$deny; + + break; + } + } + + if ($grant > $deny) { + return true; + } + + if ($deny > $grant) { + return false; + } + + if ($grant > 0) { + return $this->allowIfEqualGrantedDeniedDecisions; + } + + return $this->allowIfAllAbstainDecisions; + } +} diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/HighestNotAbstainedVoterAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/HighestNotAbstainedVoterAccessDecisionManager.php new file mode 100644 index 0000000000000..d2379dafc8e82 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/HighestNotAbstainedVoterAccessDecisionManager.php @@ -0,0 +1,46 @@ +allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + /** + * {@inheritdoc} + */ + public function decide(TokenInterface $token, array $attributes, $object = null) + { + foreach ($this->voters as $voter) { + $result = $voter->vote($token, $object, $attributes); + switch ($result) { + case VoterInterface::ACCESS_GRANTED: + return true; + case VoterInterface::ACCESS_DENIED: + return false; + default: + break; + } + } + + return $this->allowIfAllAbstainDecisions; + } +} diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/UnanimousAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/UnanimousAccessDecisionManager.php new file mode 100644 index 0000000000000..45cb33c4cdcb5 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager/UnanimousAccessDecisionManager.php @@ -0,0 +1,60 @@ +allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; + } + + /** + * {@inheritdoc} + */ + public function decide(TokenInterface $token, array $attributes, $object = null) + { + $grant = 0; + foreach ($attributes as $attribute) { + foreach ($this->voters as $voter) { + $result = $voter->vote($token, $object, array($attribute)); + + switch ($result) { + case VoterInterface::ACCESS_GRANTED: + ++$grant; + + break; + + case VoterInterface::ACCESS_DENIED: + return false; + + default: + break; + } + } + } + + // no deny votes + if ($grant > 0) { + return true; + } + + return $this->allowIfAllAbstainDecisions; + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php index bd876c729f1d8..f0b142bb8e8a7 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php @@ -139,6 +139,13 @@ public function getStrategyTests() array(AccessDecisionManager::STRATEGY_UNANIMOUS, $this->getVoters(0, 0, 2), false, true, false), array(AccessDecisionManager::STRATEGY_UNANIMOUS, $this->getVoters(0, 0, 2), true, true, true), + + //highest not abstained strategy + array(AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED, $this->getVoters(1, 0, 0), false, false, true), + array(AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED, $this->getVoters(0, 1, 0), false, false, false), + array(AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED, $this->getVoters(0, 0, 1), false, false, false), + array(AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED, $this->getVoters(0, 0, 1), true, false, true), + array(AccessDecisionManager::STRATEGY_HIGHEST_NOT_ABSTAINED, $this->getVoters(1, 1, 1), false, false, true), ); }