-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Make AccessDecisionStrategy extensible. And add highestnotabstainedvoter strategy #15295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
38b8cf5
66457b0
5894731
78c18ab
f78b3df
ffb411b
5d5f4ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
HighestNotAbstainedVoterStrategy has been added. Also this commit makes access decision strategy extensible.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
|
||
class AddAccessDecisionStrategyPass implements CompilerPassInterface | ||
{ | ||
/** | ||
* You can modify the container here before it is dumped to PHP code. | ||
* | ||
* @param ContainerBuilder $container | ||
* | ||
* @api | ||
*/ | ||
public function process(ContainerBuilder $container) | ||
{ | ||
if (!$container->hasDefinition('security.access.decision_manager')) { | ||
return; | ||
} | ||
|
||
$strategies = array(); | ||
foreach ($container->findTaggedServiceIds('security.access_strategy') as $id => $attributes) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrong naming. And you should support having several |
||
$strategyName = isset($attributes[0]['strategy']) ? $attributes[0]['strategy'] : 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
$strategies[$strategyName] = new Reference($id); | ||
} | ||
|
||
if (!$strategies) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use |
||
throw new LogicException('No access decision strategies found. You need to tag at least one with "security.access_strategy"'); | ||
} | ||
|
||
$container->getDefinition('security.access.decision_manager')->addMethodCall('setStrategies', $strategies); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,10 @@ | |
|
||
namespace Symfony\Component\Security\Core\Authorization; | ||
|
||
use Symfony\Component\Security\Core\Authorization\Strategy\DecideAffirmativeStrategy; | ||
use Symfony\Component\Security\Core\Authorization\Strategy\DecideConsensusStrategy; | ||
use Symfony\Component\Security\Core\Authorization\Strategy\DecideHighestNotAbstainedVoterStrategy; | ||
use Symfony\Component\Security\Core\Authorization\Strategy\DecideUnanimousStrategy; | ||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
|
||
|
@@ -25,8 +29,10 @@ 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 $strategies; | ||
private $strategy; | ||
private $allowIfAllAbstainDecisions; | ||
private $allowIfEqualGrantedDeniedDecisions; | ||
|
@@ -43,13 +49,9 @@ 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->strategies = array(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. injecting an array of strategy objects does not make sense IMO: you only need a single strategy, and changing it after instantiation is forbidden. |
||
$this->voters = $voters; | ||
$this->strategy = $strategyMethod; | ||
$this->strategy = $strategy; | ||
$this->allowIfAllAbstainDecisions = (bool) $allowIfAllAbstainDecisions; | ||
$this->allowIfEqualGrantedDeniedDecisions = (bool) $allowIfEqualGrantedDeniedDecisions; | ||
} | ||
|
@@ -65,155 +67,74 @@ public function setVoters(array $voters) | |
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
* @param mixed $strategies | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrong phpdoc |
||
*/ | ||
public function decide(TokenInterface $token, array $attributes, $object = null) | ||
public function addStrategy($name,$strategy) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing typehint |
||
{ | ||
return $this->{$this->strategy}($token, $attributes, $object); | ||
$this->strategies[$name] = $strategy; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supportsAttribute($attribute) | ||
private function getStrategy($strategyName) | ||
{ | ||
foreach ($this->voters as $voter) { | ||
if ($voter->supportsAttribute($attribute)) { | ||
return true; | ||
if(!array_key_exists($strategyName,$this->strategies)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Space missing before |
||
{ | ||
switch($strategyName){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Space missing before |
||
case self::STRATEGY_UNANIMOUS: | ||
return new DecideUnanimousStrategy(); | ||
case self::STRATEGY_CONSENSUS: | ||
return new DecideConsensusStrategy(); | ||
case self::STRATEGY_AFFIRMATIVE: | ||
return new DecideAffirmativeStrategy(); | ||
case self::STRATEGY_HIGHEST_NOT_ABSTAINED: | ||
return new DecideHighestNotAbstainedVoterStrategy(); | ||
default: | ||
break; | ||
} | ||
} elseif($this->strategies[$strategyName] instanceof AccessDecisionStrategyInterface) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Space missing before |
||
return $this->strategies[$strategyName]; | ||
} | ||
|
||
return false; | ||
throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategyName)); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supportsClass($class) | ||
public function decide(TokenInterface $token, array $attributes, $object = null) | ||
{ | ||
foreach ($this->voters as $voter) { | ||
if ($voter->supportsClass($class)) { | ||
return true; | ||
} | ||
} | ||
$strategy = $this->getStrategy($this->strategy); | ||
$strategy->setVoters($this->voters); | ||
$strategy->setAllowIfAllAbstainDecisions($this->allowIfAllAbstainDecisions); | ||
$strategy->setAllowIfEqualGrantedDeniedDecisions($this->allowIfEqualGrantedDeniedDecisions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is the desired way, why set it every decision? Why not just do it when building the container? |
||
|
||
return false; | ||
return $strategy->decide($token, $attributes, $object); | ||
} | ||
|
||
/** | ||
* 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). | ||
* {@inheritdoc} | ||
*/ | ||
private function decideAffirmative(TokenInterface $token, array $attributes, $object = null) | ||
public function supportsAttribute($attribute) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please keep these methods before |
||
{ | ||
$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 ($voter->supportsAttribute($attribute)) { | ||
return true; | ||
} | ||
} | ||
|
||
if ($deny > 0) { | ||
return false; | ||
} | ||
|
||
return $this->allowIfAllAbstainDecisions; | ||
return false; | ||
} | ||
|
||
/** | ||
* 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. | ||
* | ||
* If there were an equal number of grant and deny votes, the decision will | ||
* be based on the allowIfEqualGrantedDeniedDecisions property value | ||
* (defaults to true). | ||
* | ||
* If all voters abstained from voting, the decision will be based on the | ||
* allowIfAllAbstainDecisions property value (defaults to false). | ||
* {@inheritdoc} | ||
*/ | ||
private function decideConsensus(TokenInterface $token, array $attributes, $object = null) | ||
public function supportsClass($class) | ||
{ | ||
$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; | ||
} | ||
|
||
/** | ||
* 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) | ||
{ | ||
$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; | ||
} | ||
if ($voter->supportsClass($class)) { | ||
return true; | ||
} | ||
} | ||
|
||
// no deny votes | ||
if ($grant > 0) { | ||
return true; | ||
} | ||
|
||
return $this->allowIfAllAbstainDecisions; | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Security\Core\Authorization; | ||
|
||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
|
||
/** | ||
* AccessDecisionStrategyInterface contains the strategy to make access decissions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* | ||
* @author Fabien Potencier <fabien@symfony.com> | ||
*/ | ||
interface AccessDecisionStrategyInterface | ||
{ | ||
/** | ||
* Configures the voters. | ||
* | ||
* @param VoterInterface[] $voters An array of VoterInterface instances | ||
*/ | ||
public function setVoters(array $voters); | ||
|
||
/** | ||
* Set whether to grant access if all voters abstained or not. | ||
* | ||
* @param bool $allowIfAllAbstainDecisions | ||
*/ | ||
public function setAllowIfAllAbstainDecisions($allowIfAllAbstainDecisions); | ||
|
||
/** | ||
* Set whether to grant access if result are equals. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* | ||
* @param bool $allowIfEqualGrantedDeniedDecisions | ||
*/ | ||
public function setAllowIfEqualGrantedDeniedDecisions($allowIfEqualGrantedDeniedDecisions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these setters don't make sense for all strategies. They only make sense for the 3 existing strategies |
||
|
||
/** | ||
* Decides whether the access is possible or not. | ||
* | ||
* @param TokenInterface $token | ||
* @param array $attributes | ||
* @param null $object | ||
* | ||
* @return true if this decision strategy decides that the access can be made | ||
*/ | ||
public function decide(TokenInterface $token, array $attributes, $object = null); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Security\Core\Authorization\Strategy; | ||
|
||
abstract class AbstractDecideStrategy | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As each "stategy" is an implementation of |
||
{ | ||
protected $voters; | ||
|
||
protected $allowIfAllAbstainDecisions; | ||
|
||
protected $allowIfEqualGrantedDeniedDecisions; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setVoters(array $voters) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there should be not setter here. this should be a constructor argument There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah no sorry, we have a setter currently |
||
{ | ||
$this->voters = $voters; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setAllowIfAllAbstainDecisions($allowIfAllAbstainDecisions) | ||
{ | ||
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setAllowIfEqualGrantedDeniedDecisions($allowIfEqualGrantedDeniedDecisions) | ||
{ | ||
$this->allowIfEqualGrantedDeniedDecisions = $allowIfEqualGrantedDeniedDecisions; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please don't duplicate the interface phpdoc here (especially as it does not really make sense)