vendor/symfony/security-core/Authorization/AccessDecisionManager.php line 114

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Security\Core\Authorization;
  11. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  12. use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
  13. use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
  14. use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy;
  15. use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy;
  16. use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy;
  17. use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
  18. use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
  19. use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
  20. /**
  21. * AccessDecisionManager is the base class for all access decision managers
  22. * that use decision voters.
  23. *
  24. * @author Fabien Potencier <fabien@symfony.com>
  25. *
  26. * @final since Symfony 5.4
  27. */
  28. class AccessDecisionManager implements AccessDecisionManagerInterface
  29. {
  30. /**
  31. * @deprecated use {@see AffirmativeStrategy} instead
  32. */
  33. public const STRATEGY_AFFIRMATIVE = 'affirmative';
  34. /**
  35. * @deprecated use {@see ConsensusStrategy} instead
  36. */
  37. public const STRATEGY_CONSENSUS = 'consensus';
  38. /**
  39. * @deprecated use {@see UnanimousStrategy} instead
  40. */
  41. public const STRATEGY_UNANIMOUS = 'unanimous';
  42. /**
  43. * @deprecated use {@see PriorityStrategy} instead
  44. */
  45. public const STRATEGY_PRIORITY = 'priority';
  46. private const VALID_VOTES = [
  47. VoterInterface::ACCESS_GRANTED => true,
  48. VoterInterface::ACCESS_DENIED => true,
  49. VoterInterface::ACCESS_ABSTAIN => true,
  50. ];
  51. private $voters;
  52. private $votersCacheAttributes;
  53. private $votersCacheObject;
  54. private $strategy;
  55. /**
  56. * @param iterable<mixed, VoterInterface> $voters An array or an iterator of VoterInterface instances
  57. * @param AccessDecisionStrategyInterface|null $strategy The vote strategy
  58. *
  59. * @throws \InvalidArgumentException
  60. */
  61. public function __construct(iterable $voters = [], /* ?AccessDecisionStrategyInterface */ $strategy = null)
  62. {
  63. $this->voters = $voters;
  64. if (\is_string($strategy)) {
  65. trigger_deprecation('symfony/security-core', '5.4', 'Passing the access decision strategy as a string is deprecated, pass an instance of "%s" instead.', AccessDecisionStrategyInterface::class);
  66. $allowIfAllAbstainDecisions = 3 <= \func_num_args() && func_get_arg(2);
  67. $allowIfEqualGrantedDeniedDecisions = 4 > \func_num_args() || func_get_arg(3);
  68. $strategy = $this->createStrategy($strategy, $allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions);
  69. } elseif (null !== $strategy && !$strategy instanceof AccessDecisionStrategyInterface) {
  70. throw new \TypeError(sprintf('"%s": Parameter #2 ($strategy) is expected to be an instance of "%s" or null, "%s" given.', __METHOD__, AccessDecisionStrategyInterface::class, get_debug_type($strategy)));
  71. }
  72. $this->strategy = $strategy ?? new AffirmativeStrategy();
  73. }
  74. /**
  75. * @param bool $allowMultipleAttributes Whether to allow passing multiple values to the $attributes array
  76. *
  77. * {@inheritdoc}
  78. */
  79. public function decide(TokenInterface $token, array $attributes, $object = null/* , bool $allowMultipleAttributes = false */)
  80. {
  81. $allowMultipleAttributes = 3 < \func_num_args() && func_get_arg(3);
  82. // Special case for AccessListener, do not remove the right side of the condition before 6.0
  83. if (\count($attributes) > 1 && !$allowMultipleAttributes) {
  84. throw new InvalidArgumentException(sprintf('Passing more than one Security attribute to "%s()" is not supported.', __METHOD__));
  85. }
  86. return $this->strategy->decide(
  87. $this->collectResults($token, $attributes, $object)
  88. );
  89. }
  90. /**
  91. * @param mixed $object
  92. *
  93. * @return \Traversable<int, int>
  94. */
  95. private function collectResults(TokenInterface $token, array $attributes, $object): \Traversable
  96. {
  97. foreach ($this->getVoters($attributes, $object) as $voter) {
  98. $result = $voter->vote($token, $object, $attributes);
  99. if (!\is_int($result) || !(self::VALID_VOTES[$result] ?? false)) {
  100. trigger_deprecation('symfony/security-core', '5.3', 'Returning "%s" in "%s::vote()" is deprecated, return one of "%s" constants: "ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN".', var_export($result, true), get_debug_type($voter), VoterInterface::class);
  101. }
  102. yield $result;
  103. }
  104. }
  105. /**
  106. * @throws \InvalidArgumentException if the $strategy is invalid
  107. */
  108. private function createStrategy(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): AccessDecisionStrategyInterface
  109. {
  110. switch ($strategy) {
  111. case self::STRATEGY_AFFIRMATIVE:
  112. return new AffirmativeStrategy($allowIfAllAbstainDecisions);
  113. case self::STRATEGY_CONSENSUS:
  114. return new ConsensusStrategy($allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions);
  115. case self::STRATEGY_UNANIMOUS:
  116. return new UnanimousStrategy($allowIfAllAbstainDecisions);
  117. case self::STRATEGY_PRIORITY:
  118. return new PriorityStrategy($allowIfAllAbstainDecisions);
  119. }
  120. throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy));
  121. }
  122. /**
  123. * @return iterable<mixed, VoterInterface>
  124. */
  125. private function getVoters(array $attributes, $object = null): iterable
  126. {
  127. $keyAttributes = [];
  128. foreach ($attributes as $attribute) {
  129. $keyAttributes[] = \is_string($attribute) ? $attribute : null;
  130. }
  131. // use `get_class` to handle anonymous classes
  132. $keyObject = \is_object($object) ? \get_class($object) : get_debug_type($object);
  133. foreach ($this->voters as $key => $voter) {
  134. if (!$voter instanceof CacheableVoterInterface) {
  135. yield $voter;
  136. continue;
  137. }
  138. $supports = true;
  139. // The voter supports the attributes if it supports at least one attribute of the list
  140. foreach ($keyAttributes as $keyAttribute) {
  141. if (null === $keyAttribute) {
  142. $supports = true;
  143. } elseif (!isset($this->votersCacheAttributes[$keyAttribute][$key])) {
  144. $this->votersCacheAttributes[$keyAttribute][$key] = $supports = $voter->supportsAttribute($keyAttribute);
  145. } else {
  146. $supports = $this->votersCacheAttributes[$keyAttribute][$key];
  147. }
  148. if ($supports) {
  149. break;
  150. }
  151. }
  152. if (!$supports) {
  153. continue;
  154. }
  155. if (!isset($this->votersCacheObject[$keyObject][$key])) {
  156. $this->votersCacheObject[$keyObject][$key] = $supports = $voter->supportsType($keyObject);
  157. } else {
  158. $supports = $this->votersCacheObject[$keyObject][$key];
  159. }
  160. if (!$supports) {
  161. continue;
  162. }
  163. yield $voter;
  164. }
  165. }
  166. }