Ver código fonte

refactor voters, review FileVoter, update doc

Olivier Massot 2 anos atrás
pai
commit
82a91a3868

+ 3 - 0
doc/internal_requests.md

@@ -46,6 +46,9 @@ Les appels à cette route ne sont autorisés que si :
 
 
 Si ces deux conditions ne sont pas remplies, la requête est rejetée, et ce même si l'utilisateur est authentifié.
 Si ces deux conditions ne sont pas remplies, la requête est rejetée, et ce même si l'utilisateur est authentifié.
 
 
+Les routes internal sont configurées ici : `config/packages/security.yaml`
+
+
 
 
 ### Valider le fonctionnement
 ### Valider le fonctionnement
 
 

+ 85 - 6
doc/security.md

@@ -56,13 +56,12 @@ A chaque requête effectuée, la classe `\App\Security\Voter\ModuleVoter` vérif
 module possédé par l'organisation de l'utilisateur. Si ce n'est pas le cas, une erreur `AccessDeniedHttpException` est levée.
 module possédé par l'organisation de l'utilisateur. Si ce n'est pas le cas, une erreur `AccessDeniedHttpException` est levée.
 
 
 
 
+## Différentes méthodes de sécurisation
 
 
-## Sécurité des ressources Api-Platform
-
-> See : https://api-platform.com/docs/core/security/
+### Les annotations Api-Platform
 
 
 La sécurité des ApiResources peut être définie de manière globale pour la ressource, ou pour chaque opération (Get, 
 La sécurité des ApiResources peut être définie de manière globale pour la ressource, ou pour chaque opération (Get, 
-GetCollection, Put, Post, Delete).
+GetCollection, Put, Post, Delete) via les annotations.
 
 
 Exemple : 
 Exemple : 
 
 
@@ -80,14 +79,94 @@ Exemple :
 Dans certains cas plus complexes (ex: Access), cette configuration peut être déplacée dans un fichier de configuration
 Dans certains cas plus complexes (ex: Access), cette configuration peut être déplacée dans un fichier de configuration
 situé dans le répertoire `~/config/api_platform/` et portant le nom de la ressource.
 situé dans le répertoire `~/config/api_platform/` et portant le nom de la ressource.
 
 
+> Voir plus : https://api-platform.com/docs/core/security/
+
+
+### Voters
+
+Les voters permettent de contrôler l'accès à certaines ressources, selon le type d'opération.
+
+Ils implémentent essentiellement deux méthodes : `supports` et `voteOnAttribute` qui prennent en paramètres la ressource 
+et le type d'opération. `supports` retourne `true` si le voter doit s'appliquer dans ce cas. Si oui, `voteOnAttribute`
+est appellée et retourne `true` si l'opération est autorisée.
+
+> TODO: quand faut-il retourner false et quand faut-il lever une AccessDeniedHttpException ?
+
+Pour qu'un Voter soit appellé, il faut configurer la sécurité de la ressource via son annotation et utiliser la méthode 
+`is_granted` combinée à une des constantes : 
+
+Exemple : 
+
+    #[ApiResource(
+        operations: [
+            new Get(security: "is_granted('READ', object)"),
+            new Put(security: "is_granted('EDIT', object)"),
+            new Post(security: "is_granted('CREATE', object)"),
+            new Delete(security: "is_granted('DELETE', object)"),
+        ]
+    )]
+    class File
+
+    
+
+Les voters custom sont enregistrés dans le répertoire `src/Security/Voter`.
+
+> Voir plus : https://symfony.com/doc/current/security/voters.html
+
+
+### Extensions Doctrine
 
 
-## Extensions Doctrine
+Le framework Api-Platform propose un système d'extensions doctrine pour ajouter des filtres automatiques aux opérations
+réalisées sur des ressources voulues.
+
+Concrètement, l'extension ajoutera une condition au `WHERE` de la requête SQL.
+
+Une extension implémente trois méthodes : 
+
+* `applyToCollection`
+* `applyToItem`
+* `addWhere`
+
+Les deux premières permettent de tester si l'extension doit s'appliquer à la ressource.
+
+La dernière ajoute la condition en question.
+
+> Voir : https://api-platform.com/docs/core/extensions/
+
+
+### Quand utiliser les annotations, un Voter ou une Extension Doctrine ?
+
+Si la sécurité doit pouvoir s'appliquer à une collection et filtrer le résultat de celle-ci (exemple: les notifications 
+d'un utilisateur) : on utilisera une _Extension Doctrine_
+
+Sinon, si les conditions d'accès peuvent s'écrire facilement avec l'expression language des annotations
+api-platform, on utilisera les _annotations Api-platform_.
+
+Si aucune des conditions précédentes n'est remplie, on utilisera un _Voter Symfony_.
 
 
-## Voters
 
 
 ## Internal Requests
 ## Internal Requests
 
 
+Certaines routes de la forme `/internal/...` permettent d'accepter des requêtes sans que le client ne
+soit authentifié (cf `config/packages/security.yaml`), en se basant sur un contrôle de l'IP et un token. 
+
+> Voir: https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/doc/internal_requests.md
+
+
+
 ## Cas particuliers 
 ## Cas particuliers 
 
 
 ### Les Fichiers
 ### Les Fichiers
 
 
+Le client est autorisé à consulter l'enregistrement si l'une de ces conditions est vraie :
+
+* pas de date de disponibilité ou date de disponibilité antérieure à Now
+* visibilité est 'EVERYBODY' 
+* requête interne
+* est connecté et fait partie des AccessPersons du fichier
+* est connecté et fichier a des accessRoles et user a un des roles présents dans accessRoles et fichier appartient à l'organisation à laquelle l'utilisateur appartient
+* est connecté et fichier a des accessRoles et user a un des roles présents dans accessRoles et fichier appartient à l'utilisateur
+=> TODO: trois dernières conditions à revoir
+
+Est autorisé à éditer si :
+

+ 40 - 10
src/Entity/Core/File.php

@@ -5,6 +5,8 @@ namespace App\Entity\Core;
 
 
 use ApiPlatform\Metadata\Put;
 use ApiPlatform\Metadata\Put;
 use ApiPlatform\Metadata\Get;
 use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Delete;
 use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\ApiResource;
 use App\Entity\AccessWish\DocumentWish;
 use App\Entity\AccessWish\DocumentWish;
 use App\Entity\Booking\Event;
 use App\Entity\Booking\Event;
@@ -36,12 +38,10 @@ use App\Enum\Core\FileStatusEnum;
  */
  */
 #[ApiResource(
 #[ApiResource(
     operations: [
     operations: [
-        new Get(
-            security: ''
-        ),
-        new Put(
-            security: ''
-        )
+        new Get(security: "is_granted('READ', object)"),
+        new Put(security: "is_granted('EDIT', object)"),
+        new Post(security: "is_granted('CREATE', object)"),
+        new Delete(security: "is_granted('DELETE', object)")
     ]
     ]
 )]
 )]
 //#[Auditable]
 //#[Auditable]
@@ -182,10 +182,14 @@ class File
 
 
     //    #[ORM\Column]
     //    #[ORM\Column]
     //    private ?int $eventReport_id;
     //    private ?int $eventReport_id;
-    //
-    //    #[ORM\Column]
-    //    private ?\DateTime $availabilityDate;
-    //
+
+    /**
+     * TODO: complete
+     * @var DateTime|null
+     */
+    #[ORM\Column]
+    private ?\DateTime $availabilityDate;
+
     //    #[ORM\Column]
     //    #[ORM\Column]
     //    private ?int $documentWish_id;
     //    private ?int $documentWish_id;
     //
     //
@@ -197,6 +201,7 @@ class File
     //
     //
     //    #[ORM\Column]
     //    #[ORM\Column]
     //    private ?int $work_id;
     //    private ?int $work_id;
+
     #[ORM\ManyToMany(targetEntity: Person::class, inversedBy: 'personFiles')]
     #[ORM\ManyToMany(targetEntity: Person::class, inversedBy: 'personFiles')]
     #[Groups(['file_person'])]
     #[Groups(['file_person'])]
     private Collection $accessPersons;
     private Collection $accessPersons;
@@ -377,6 +382,15 @@ class File
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * A priori inutilisé
+     * @deprecated
+     * @return Collection
+     */
+    public function getAccessRoles(): Collection {
+        return new ArrayCollection([]);
+    }
+
     /**
     /**
      * @return string
      * @return string
      */
      */
@@ -614,6 +628,22 @@ class File
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * @return DateTime|null
+     */
+    public function getAvailabilityDate(): ?DateTime
+    {
+        return $this->availabilityDate;
+    }
+
+    /**
+     * @param DateTime|null $availabilityDate
+     */
+    public function setAvailabilityDate(?DateTime $availabilityDate): void
+    {
+        $this->availabilityDate = $availabilityDate;
+    }
+
     /**
     /**
      * @return Collection<int, Person>
      * @return Collection<int, Person>
      */
      */

+ 2 - 0
src/Enum/Core/FileTypeEnum.php

@@ -8,10 +8,12 @@ use MyCLabs\Enum\Enum;
  * @method static UNKNOWN()
  * @method static UNKNOWN()
  * @method static NONE()
  * @method static NONE()
  * @method static LICENCE_CMF()
  * @method static LICENCE_CMF()
+ * @method static BILL()
  */
  */
 class FileTypeEnum extends Enum
 class FileTypeEnum extends Enum
 {
 {
     private const UNKNOWN = 'UNKNOWN';
     private const UNKNOWN = 'UNKNOWN';
     private const NONE = 'NONE';
     private const NONE = 'NONE';
     private const LICENCE_CMF ='LICENCE_CMF';
     private const LICENCE_CMF ='LICENCE_CMF';
+    private const BILL ='BILL';
 }
 }

+ 19 - 0
src/Enum/Core/FileVisibilityEnum.php

@@ -0,0 +1,19 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Core;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Statuts des fichiers
+ * @method static EVERYBODY()
+ * @method static NOBODY()
+ * @method static ONLY_ORGANIZATION()
+ */
+class FileVisibilityEnum extends Enum
+{
+    private const EVERYBODY = 'EVERYBODY';
+    private const NOBODY = 'NOBODY';
+    private const ONLY_ORGANIZATION = 'ONLY_ORGANIZATION';
+}

+ 164 - 4
src/Security/Voter/AbstractVoter.php

@@ -3,25 +3,185 @@
 namespace App\Security\Voter;
 namespace App\Security\Voter;
 
 
 use App\Entity\Access\Access;
 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\Bundle\SecurityBundle\Security;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 use Symfony\Component\Security\Core\User\UserInterface;
 use Symfony\Component\Security\Core\User\UserInterface;
 
 
+/**
+ * Base class for custom Voters.
+ *
+ * This class also defines a default behavior for entity based voters (ex: FileVoter)
+ *
+ * @see doc/security.md
+ */
 abstract class AbstractVoter extends Voter
 abstract class AbstractVoter extends Voter
 {
 {
-    protected ?UserInterface $user = null;
+    protected const READ = 'READ';
+    protected const EDIT = 'EDIT';
+    protected const CREATE = 'CREATE';
+    protected const DELETE = 'DELETE';
+
+    /**
+     * The current user if any, else null; access it trough isUserLoggedIn or getUser methods
+     * @var Access|null
+     */
+    private ?Access $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(
     public function __construct(
-        private Security $security
+        private Security $security,
+        protected Utils $accessUtils,
+        private InternalRequestsService $internalRequestsService,
+        EntityManagerInterface $em,
+        private SwitchUser $switchUser
     ) {
     ) {
         /** @var Access $user */
         /** @var Access $user */
-        $user = $token->getUser();
+        $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 = $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
         // If the user is not anonymous, remember it
-        if ($user instanceof UserInterface) {
+        if ($user instanceof Access) {
             $this->user = $user;
             $this->user = $user;
         }
         }
     }
     }
 
 
+    /**
+     * 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::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
+     */
+    abstract protected function canView(object $subject): bool;
+
+    /**
+     * Does the client have the right to edit this resource?
+     * @param object $subject
+     * @return bool
+     */
+    abstract protected function canEdit(object $subject): bool;
+
+    /**
+     * Does the client have the right to create this resource?
+     *
+     * @param object $subject
+     * @return bool
+     */
+    abstract protected function canCreate(object $subject): bool;
+
+    /**
+     * Does the client have the right to delete this resource?
+     *
+     * @param object $subject
+     * @return bool
+     */
+    abstract protected function canDelete(object $subject): bool;
+
+    /**
+     * Is the client an authenticated user ?
+     *
+     * @return bool
+     */
+    protected function isUserLoggedIn(): bool {
+        return $this->user !== null;
+    }
+
+    /**
+     * Returns the current logged in user
+     * @return Access
+     */
+    protected function getUser(): ?Access {
+        return $this->user;
+    }
+
+    /**
+     * 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'];
+        $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
+
+        return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);
+    }
 
 
 }
 }

+ 206 - 0
src/Security/Voter/Core/FileVoter.php

@@ -0,0 +1,206 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Security\Voter\Core;
+
+use App\Entity\Access\Access;
+use App\Entity\Core\File;
+use App\Enum\Core\FileVisibilityEnum;
+use App\Enum\Core\FileTypeEnum;
+use App\Security\Voter\AbstractVoter;
+use App\Service\Utils\DatesUtils;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+
+/**
+ * Contrôle l'accès à l'entité File
+ */
+class FileVoter extends AbstractVoter
+{
+    /**
+     * @inheritdoc
+     * @var string|null
+     */
+    protected static ?string $entityClass = File::class;
+
+    /**
+     * @inheritdoc
+     *
+     * @param $subject File
+     * @return boolean
+     */
+    public function canView(object $subject): bool
+    {
+        // Le fichier n'est pas encore disponible
+        if(!$this->isAvailable($subject, $this->getUser())) {
+            // TODO: à revoir
+            return false;
+        }
+
+        // File has public visibility
+        if ($subject->getVisibility() === FileVisibilityEnum::EVERYBODY()->getValue()) {
+            return true;
+        }
+
+        // Is this an internal request?
+        if ($this->isValidInternalRequest()) {
+            return true;
+        }
+
+        // If the user is not logged in, the file is not available
+        if ($this->isUserLoggedIn()) {
+
+            // If the logged user is in the accessPersons of the File
+            if (
+                $subject->getAccessPersons()->contains($this->getUser()->getPerson())
+            ) {
+                return true;
+            }
+
+            // User is the owner of the file
+            if ($subject->getPerson() && $subject->getPerson()->getId() === $this->getUser()->getPerson()->getId()) {
+                return true;
+            }
+
+            // User has ROLE_FILE and owns the file or belongs to the organization who does
+            if (
+                $this->accessUtils->hasRole($this->getUser(), 'ROLE_FILE') &&
+                $this->isInFileOwningGroup($subject, $this->getUser())
+            ) {
+                return true;
+            }
+
+            // User has the requested role and owns the file or belongs to the organization who does
+            if (
+                $this->isInFileOwningGroupWithRole($subject, $this->getUser())
+            ) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @inheritdoc
+     *
+     * @param $subject File
+     * @return boolean
+     */
+    public function canEdit(object $subject): bool
+    {
+        // Le fichier n'est pas encore disponible
+        if(!$this->isAvailable($subject, $this->getUser())) {
+            return false;
+        }
+
+        if ($this->isUserLoggedIn()) {
+
+            // User has ROLE_FILE and has access to the file
+            if (
+                $this->accessUtils->hasRole($this->getUser(), 'ROLE_FILE') &&
+                $this->isInFileOwningGroup($subject, $this->getUser())
+            ) {
+                return true;
+            }
+
+            // User is the owner of the file
+            if ($subject->getPerson() && $subject->getPerson()->getId() === $this->getUser()->getPerson()->getId()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @inheritdoc
+     *
+     * @param $subject File
+     * @return boolean
+     */
+    public function canCreate(object $subject): bool
+    {
+        // An authenticated user can create a file
+        if ($this->isUserLoggedIn()) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @inheritdoc
+     *
+     * @param $subject File
+     * @return boolean
+     */
+    public function canDelete(object $subject): bool {
+        return $this->canEdit($subject);
+    }
+
+    /**
+     * Is the given user the owner of the file, or does he belong to the organization that owns it, and does the user
+     * have any of the required role to access this file?
+     *
+     * @param File $file
+     * @param Access $user
+     * @return bool
+     */
+    private function isInFileOwningGroup(File $file, Access $user): bool {
+        $isOrganizationOwned = $file->getOrganization() && $file->getOrganization()->getId() === $user->getOrganization()->getId();
+        $isAccessOwned = $file->getPerson() && $file->getPerson()->getId() === $user->getPerson()->getId();
+
+        return $isOrganizationOwned || $isAccessOwned;
+    }
+
+    /**
+     * Is the given user the owner of the file, or does he belong to the organization that owns it, and does the user
+     * have any of the required role to access this file?
+     *
+     * @deprecated
+     * @param File $file
+     * @param Access $user
+     * @return bool
+     */
+    private function isInFileOwningGroupWithRole(File $file, Access $user): bool {
+
+        $requiredRoles = $file->getAccessRoles();
+        if ($requiredRoles->count() === 0) {
+            return false;
+        }
+
+        $userHasRequiredRole = $file->getAccessRoles()->exists(
+            // TODO: implémenter une méthode hasAnyRoleAmong() côté accessUtils
+            fn($aR) => $this->accessUtils->hasRole($user, $aR)
+        );
+
+        return $this->isInFileOwningGroup($file, $user) && $userHasRequiredRole;
+    }
+
+    /**
+     * Is the file available now?
+     * Depends on the availabilityDate, the type of the file and the user
+     *
+     * @param $file File
+     * @return bool
+     * @throws \Exception
+     */
+    private function isAvailable(File $file, Access $user): bool {
+
+        $today = DatesUtils::new();
+
+        if ($file->getAvailabilityDate() !== null && $file->getAvailabilityDate() > $today) {
+
+            //  Cas particulier de la liste des factures
+            if (
+                $file->getType() === FileTypeEnum::BILL()->getValue() &&
+                $this->accessUtils->hasRole($user, 'ROLE_BILLACCOUNTING')
+            ) {
+                return true;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+}

+ 0 - 199
src/Security/Voter/FileVoter.php

@@ -1,199 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Security\Voter;
-
-use App\Entity\Access\Access;
-use App\Entity\Core\BankAccount;
-use App\Entity\Core\File;
-use AppBundle\Entity\Organization\Organization;
-use AppBundle\Entity\Person\Person;
-use AppBundle\Enum\Core\FileTypeEnum;
-use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
-use Symfony\Component\Security\Core\Authorization\Voter\Voter;
-use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\Security\Core\User\UserInterface;
-
-/**
- * Contrôle l'accès à l'entité File
- */
-class FileVoter extends AbstractVoter
-{
-    protected const FILE_READ = 'FILE_READ';
-    protected const FILE_EDIT = 'FILE_EDIT';
-    protected const FILE_CREATE = 'FILE_CREATE';
-    protected const FILE_DELETE = 'FILE_DELETE';
-
-    public function __construct(
-        private Security $security
-    ) {}
-
-    protected function supports(string $attribute, $subject): bool
-    {
-        return $subject instanceof File && in_array($attribute, [self::FILE_READ, self::FILE_EDIT, self::FILE_DELETE]);
-    }
-
-    /**
-     * Retourne True si l'utilisateur a le droit d'effectuer l'opération demandée
-     *
-     * @param string $attribute
-     * @param mixed $subject
-     * @param TokenInterface $token
-     * @return bool
-     */
-    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
-    {
-        if(!$this->isAvailabilityDatePassed($subject)) {
-            // Le fichier n'est pas encore disponible TODO: à revoir
-            return false;
-        }
-
-        switch ($attribute) {
-            case self::FILE_READ:
-                return $this->canView($subject);
-            case self::FILE_EDIT:
-                return $this->canEdit($subject, $user);
-            case self::FILE_CREATE:
-                return $this->canCreate($subject, $user);
-            case self::FILE_DELETE:
-                return $this->canDelete($subject, $user);
-        }
-
-        return false;
-    }
-
-    /**
-     * Returns True if the current user has the right to GET this record
-     *
-     * @param $subject \AppBundle\Entity\Core\File
-     * @param $user
-     * @return boolean
-     */
-    public function canView(File $subject): bool
-    {
-        // File has public visibility
-        if ($subject->getVisibility() === FileVisibilityEnum::EVERYBODY) {
-            return true;
-        }
-
-        // Is this an internal request? (@see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/doc/internal_requests.md)
-        $clientIp = $_SERVER['REMOTE_ADDR'];
-        $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';
-        if ($this->internalRequestsService->isAllowed($clientIp, $internalRequestToken)) {
-            return true;
-        }
-
-        // If the user has not logged in, the file is not available
-        if (!$this->user) {
-            return false;
-        }
-
-        // If the logged user is in accessUser of File
-        if ($subject->getAccessPersons()->count() !== 0) {
-            foreach ($subject->getAccessPersons() as $accessPerson) {
-                if ($user->getId() == $accessPerson->getId()) {
-                    return true;
-                }
-            }
-        }
-
-        /**
-         * If the logged user is in accessRole of File
-         */
-        if(0 !== count($subject->getAccessRoles())
-            &&(
-                ($subject->getOrganization() instanceof Organization && $subject->getOrganization()->getId() === $this->accessService->getAccess()->getOrganization()->getId())
-                || ($subject->getPerson() instanceof Person && $subject->getPerson()->getId() === $this->accessService->getAccess()->getPerson()->getId())
-            )
-        ){
-            foreach ($subject->getAccessRoles() as $accessRole){
-                if($this->accessService->hasRole($accessRole)){
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-
-    /**
-     * @param $subject \AppBundle\Entity\Core\File
-     * @param $user
-     * @return boolean
-     */
-    public function canEdit($subject, $user)
-    {
-        /**
-         * If user is not logged, the file is not available
-         */
-        if (!$user instanceof Person) {
-            return false;
-        } /**
-         * If user has ROLE_FILE
-         */
-        elseif ($this->accessService->hasRole('ROLE_FILE')
-            && (
-                ($subject->getOrganization() instanceof Organization && $subject->getOrganization()->getId() === $this->accessService->getAccess()->getOrganization()->getId())
-                || ($subject->getPerson() instanceof Person && $subject->getPerson()->getId() === $this->accessService->getAccess()->getPerson()->getId())
-            )
-        ) {
-            return true;
-        } /**
-         * If proprietary person of file is same of logged user
-         */
-        elseif ($subject->getPerson() instanceof Person && $subject->getPerson()->getId() === $user->getId()) {
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * @param $subject File
-     * @param $user
-     * @return boolean
-     */
-    public function canCreate($subject, $user)
-    {
-        /**
-         * If user is not logged, the file is not available
-         */
-        if (!$user instanceof Person) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * @param $subject File
-     * @param $user
-     * @return boolean
-     */
-    public function canDelete($subject, $user){
-        return $this->canEdit($subject, $user);
-    }
-
-    /**
-     * @param $subject
-     * @return bool
-     * @throws \Exception
-     */
-    private function isAvailabilityDatePassed($subject){
-        $isAvailabilityDatePassed = true;
-
-        $today = new \DateTime();
-        if (!empty($subject->getAvailabilityDate()) && $subject->getAvailabilityDate() > $today) {
-            $userHasRole = false;
-            //  Si l'utilisateur a les droits suffisant pour voir le fichier meme si la date n'est pas passé: exemple: liste des factures
-            if($subject->getType() === FileTypeEnum::BILL && $this->roleServiceUtils->checkIfUserHasRoles($this->accessService->getRoles(), ['ROLE_BILLACCOUNTING'])){
-                $userHasRole = true;
-            }
-            if(!$userHasRole)
-                $isAvailabilityDatePassed = false;
-        }
-
-        return $isAvailabilityDatePassed;
-    }
-
-}

+ 2 - 1
src/Security/Voter/SwitchUserVoter.php

@@ -42,10 +42,11 @@ class SwitchUserVoter extends Voter
             return true;
             return true;
         }
         }
 
 
+        // TODO: on pourrait pas intégrer le isGranted à cette méthode?
         if ($this->switchUser->isAllowedToSwitch($user, $subject)) {
         if ($this->switchUser->isAllowedToSwitch($user, $subject)) {
             return true;
             return true;
         }
         }
 
 
         return true;
         return true;
     }
     }
-}
+}

+ 2 - 1
src/Service/Access/Utils.php

@@ -47,7 +47,7 @@ class Utils
      * @return bool
      * @return bool
      * @see UtilsTest::testHasRoles()
      * @see UtilsTest::testHasRoles()
      */
      */
-    public function hasRoles(Access $access, string $roleToHave): bool
+    public function hasRole(Access $access, string $roleToHave): bool
     {
     {
         return in_array($roleToHave, $this->getAllRoles($access), true);
         return in_array($roleToHave, $this->getAllRoles($access), true);
     }
     }
@@ -60,6 +60,7 @@ class Utils
      */
      */
     public function getAllRoles(Access $access): array
     public function getAllRoles(Access $access): array
     {
     {
+        // TODO: est-ce qu'on ne pourrait pas mettre ça en cache d'une façon ou d'une autre?
         $roles = $this->optionalsRolesIterator->getOptionalsRoles($access);
         $roles = $this->optionalsRolesIterator->getOptionalsRoles($access);
         return $this->roleHierarchy->getReachableRoleNames(array_merge($access->getRoles(), $roles));
         return $this->roleHierarchy->getReachableRoleNames(array_merge($access->getRoles(), $roles));
     }
     }

+ 2 - 0
src/Service/Security/SwitchUser.php

@@ -14,6 +14,8 @@ use Symfony\Component\Security\Core\User\UserInterface;
 class SwitchUser
 class SwitchUser
 {
 {
     /**
     /**
+     * TODO: documenter
+     *
      * @param Access $access
      * @param Access $access
      * @param UserInterface $user_to_switch
      * @param UserInterface $user_to_switch
      * @return bool
      * @return bool

+ 5 - 5
tests/Unit/Service/Access/UtilsTest.php

@@ -54,21 +54,21 @@ class UtilsTest extends TestCase
     }
     }
 
 
     /**
     /**
-     * @see Utils::hasRoles()
+     * @see Utils::hasRole()
      */
      */
-    public function testHasRoles(): void
+    public function testHasRole(): void
     {
     {
         $accessUtils = $this->getMockBuilder(AccessUtils::class)
         $accessUtils = $this->getMockBuilder(AccessUtils::class)
             ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
             ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['hasRoles'])
+            ->setMethodsExcept(['hasRole'])
             ->getMock();
             ->getMock();
 
 
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
 
 
         $accessUtils->method('getAllRoles')->with($access)->willReturn(['ROLE_A']);
         $accessUtils->method('getAllRoles')->with($access)->willReturn(['ROLE_A']);
 
 
-        $this->assertTrue($accessUtils->hasRoles($access, 'ROLE_A'));
-        $this->assertFalse($accessUtils->hasRoles($access, 'ROLE_B'));
+        $this->assertTrue($accessUtils->hasRole($access, 'ROLE_A'));
+        $this->assertFalse($accessUtils->hasRole($access, 'ROLE_B'));
     }
     }
 
 
     /**
     /**