|
|
@@ -0,0 +1,195 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Security\Voter\EntityVoter;
|
|
|
+
|
|
|
+use App\Entity\Access\Access;
|
|
|
+use App\Service\Access\Utils;
|
|
|
+use App\Service\Security\InternalRequestsService;
|
|
|
+use App\Service\Security\SwitchUser;
|
|
|
+use Doctrine\ORM\EntityManagerInterface;
|
|
|
+use Symfony\Bundle\SecurityBundle\Security;
|
|
|
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
|
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Base class for custom Voters
|
|
|
+ *
|
|
|
+ * This class also defines a default behavior for entity based voters (ex: FileVoter)
|
|
|
+ *
|
|
|
+ * @see doc/security.md
|
|
|
+ */
|
|
|
+abstract class AbstractEntityVoter extends Voter
|
|
|
+{
|
|
|
+ protected const READ = 'READ';
|
|
|
+ protected const EDIT = 'EDIT';
|
|
|
+ protected const CREATE = 'CREATE';
|
|
|
+ protected const DELETE = 'DELETE';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The current user if any; access it trough isUserLoggedIn or getUser methods
|
|
|
+ * If the current user is null, it has not been fetched already
|
|
|
+ * If it is false, there is no user logged in
|
|
|
+ * @var Access|null|false
|
|
|
+ */
|
|
|
+ private Access|null|false $user = null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The supported class name. Override it in subclass.
|
|
|
+ * Ex:
|
|
|
+ * protected ?string $entityClass = File::class;
|
|
|
+ *
|
|
|
+ * @var string|null
|
|
|
+ */
|
|
|
+ protected static ?string $entityClass = null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * List of supported operations. Override it to restrict.
|
|
|
+ * @var array<string>
|
|
|
+ */
|
|
|
+ protected static array $allowedOperations = [
|
|
|
+ self::READ, self::EDIT, self::CREATE, self::DELETE
|
|
|
+ ];
|
|
|
+
|
|
|
+ public function __construct(
|
|
|
+ protected Security $security,
|
|
|
+ protected Utils $accessUtils,
|
|
|
+ private InternalRequestsService $internalRequestsService,
|
|
|
+ private EntityManagerInterface $em,
|
|
|
+ private SwitchUser $switchUser
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Default `supports` method, that uses self::entityClass and self::allowedOperations to determine if the voter
|
|
|
+ * supports the subject and attribute.
|
|
|
+ *
|
|
|
+ * @param string $attribute
|
|
|
+ * @param mixed $subject
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function supports(string $attribute, mixed $subject): bool
|
|
|
+ {
|
|
|
+ if (static::$entityClass === null) {
|
|
|
+ throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
|
|
|
+ }
|
|
|
+
|
|
|
+ return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Default `voteOnAttribute` method, calling one of the `canXxxx()` method, according to the attribute.
|
|
|
+ *
|
|
|
+ * @param string $attribute
|
|
|
+ * @param mixed $subject
|
|
|
+ * @param TokenInterface $token
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
|
|
+ {
|
|
|
+ switch ($attribute) {
|
|
|
+ case self::READ:
|
|
|
+ return $this->canView($subject);
|
|
|
+ case self::EDIT:
|
|
|
+ return $this->canEdit($subject);
|
|
|
+ case self::CREATE:
|
|
|
+ return $this->canCreate($subject);
|
|
|
+ case self::DELETE:
|
|
|
+ return $this->canDelete($subject);
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Does the client have the right to view this resource?
|
|
|
+ * @param object $subject
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function canView(object $subject): bool {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Does the client have the right to edit this resource?
|
|
|
+ * @param object $subject
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function canEdit(object $subject): bool {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Does the client have the right to create this resource?
|
|
|
+ *
|
|
|
+ * @param object $subject
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function canCreate(object $subject): bool {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Does the client have the right to delete this resource?
|
|
|
+ *
|
|
|
+ * @param object $subject
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function canDelete(object $subject): bool {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the current logged in user
|
|
|
+ * @return Access
|
|
|
+ */
|
|
|
+ protected function getUser(): ?Access {
|
|
|
+ if ($this->user === null) {
|
|
|
+ /** @var Access $user */
|
|
|
+ $user = $this->security->getUser();
|
|
|
+
|
|
|
+ // <-- Special case of impersonated users: the switch user is not setup yet by symfony, we have to do it "manually"
|
|
|
+ $switchHeaderId = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
|
|
|
+ if ($switchHeaderId !== null) {
|
|
|
+ $switchAs = $this->em->find(Access::class, $switchHeaderId);
|
|
|
+ if (
|
|
|
+ $switchAs &&
|
|
|
+ (
|
|
|
+ $this->security->isGranted('ROLE_ALLOWED_TO_SWITCH') ||
|
|
|
+ $this->switchUser->isAllowedToSwitch($user, $switchAs)
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ $user = $switchAs;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // -->
|
|
|
+
|
|
|
+ // If the user is not anonymous, remember it
|
|
|
+ $this->user = $user instanceof Access ? $user : false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->user !== false ? $this->user : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the client an authenticated user ?
|
|
|
+ *
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function isUserLoggedIn(): bool {
|
|
|
+ return $this->getUser() !== null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the current request a valid internal request?
|
|
|
+ *
|
|
|
+ * @see doc/internal_requests.md
|
|
|
+ * @see doc/security.md
|
|
|
+ *
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function isValidInternalRequest(): bool {
|
|
|
+ $clientIp = $_SERVER['REMOTE_ADDR'] ?? null;
|
|
|
+ $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
|
|
|
+
|
|
|
+ return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);
|
|
|
+ }
|
|
|
+}
|