AbstractEntityVoter.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. namespace App\Security\Voter\EntityVoter;
  3. use App\Entity\Access\Access;
  4. use App\Service\Access\Utils;
  5. use App\Service\Security\InternalRequestsService;
  6. use App\Service\Security\SwitchUser;
  7. use Doctrine\ORM\EntityManagerInterface;
  8. use Symfony\Bundle\SecurityBundle\Security;
  9. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  10. use Symfony\Component\Security\Core\Authorization\Voter\Voter;
  11. /**
  12. * Base class for custom Voters.
  13. *
  14. * This class also defines a default behavior for entity based voters (ex: FileVoter)
  15. *
  16. * @see doc/security.md
  17. */
  18. abstract class AbstractEntityVoter extends Voter
  19. {
  20. protected const READ = 'READ';
  21. protected const EDIT = 'EDIT';
  22. protected const CREATE = 'CREATE';
  23. protected const DELETE = 'DELETE';
  24. /**
  25. * The current user if any; access it trough isUserLoggedIn or getUser methods
  26. * If the current user is null, it has not been fetched already
  27. * If it is false, there is no user logged in.
  28. */
  29. private Access|false|null $user = null;
  30. /**
  31. * The supported class name. Override it in subclass.
  32. * Ex:
  33. * protected ?string $entityClass = File::class;.
  34. */
  35. protected static ?string $entityClass = null;
  36. /**
  37. * List of supported operations. Override it to restrict.
  38. *
  39. * @var array<string>
  40. */
  41. protected static array $allowedOperations = [
  42. self::READ, self::EDIT, self::CREATE, self::DELETE,
  43. ];
  44. public function __construct(
  45. protected Security $security,
  46. protected Utils $accessUtils,
  47. private InternalRequestsService $internalRequestsService,
  48. private EntityManagerInterface $em,
  49. private SwitchUser $switchUser,
  50. ) {
  51. }
  52. /**
  53. * Default `supports` method, that uses self::entityClass and self::allowedOperations to determine if the voter
  54. * supports the subject and attribute.
  55. */
  56. protected function supports(string $attribute, mixed $subject): bool
  57. {
  58. if (static::$entityClass === null) {
  59. throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
  60. }
  61. return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
  62. }
  63. /**
  64. * Default `voteOnAttribute` method, calling one of the `canXxxx()` method, according to the attribute.
  65. */
  66. protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
  67. {
  68. switch ($attribute) {
  69. case self::READ:
  70. return $this->canView($subject);
  71. case self::EDIT:
  72. return $this->canEdit($subject);
  73. case self::CREATE:
  74. return $this->canCreate($subject);
  75. case self::DELETE:
  76. return $this->canDelete($subject);
  77. }
  78. return false;
  79. }
  80. /**
  81. * Does the client have the right to view this resource?
  82. */
  83. protected function canView(object $subject): bool
  84. {
  85. return false;
  86. }
  87. /**
  88. * Does the client have the right to edit this resource?
  89. */
  90. protected function canEdit(object $subject): bool
  91. {
  92. return false;
  93. }
  94. /**
  95. * Does the client have the right to create this resource?
  96. */
  97. protected function canCreate(object $subject): bool
  98. {
  99. return false;
  100. }
  101. /**
  102. * Does the client have the right to delete this resource?
  103. */
  104. protected function canDelete(object $subject): bool
  105. {
  106. return false;
  107. }
  108. /**
  109. * Returns the current logged in user.
  110. */
  111. protected function getUser(): ?Access
  112. {
  113. if ($this->user === null) {
  114. /** @var Access $user */
  115. $user = $this->security->getUser();
  116. // <-- Special case of impersonated users: the switch user is not setup yet by symfony, we have to do it "manually"
  117. $switchHeaderId = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
  118. if ($switchHeaderId !== null) {
  119. $switchAs = $this->em->find(Access::class, $switchHeaderId);
  120. if (
  121. $switchAs
  122. && (
  123. $this->security->isGranted('ROLE_ALLOWED_TO_SWITCH')
  124. || $this->switchUser->isAllowedToSwitch($user, $switchAs)
  125. )
  126. ) {
  127. $user = $switchAs;
  128. }
  129. }
  130. // -->
  131. // If the user is not anonymous, remember it
  132. $this->user = $user instanceof Access ? $user : false;
  133. }
  134. return $this->user !== false ? $this->user : null;
  135. }
  136. /**
  137. * Is the client an authenticated user ?
  138. */
  139. protected function isUserLoggedIn(): bool
  140. {
  141. return $this->getUser() !== null;
  142. }
  143. /**
  144. * Is the current request a valid internal request?
  145. *
  146. * @see doc/internal_requests.md
  147. * @see doc/security.md
  148. */
  149. protected function isValidInternalRequest(): bool
  150. {
  151. $clientIp = $_SERVER['REMOTE_ADDR'] ?? null;
  152. $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
  153. return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);
  154. }
  155. }