AbstractEntityVoter.php 5.3 KB

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