Pārlūkot izejas kodu

refactor voters, review FileVoter, update doc

Olivier Massot 2 gadi atpakaļ
vecāks
revīzija
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é.
 
+Les routes internal sont configurées ici : `config/packages/security.yaml`
+
+
 
 ### 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.
 
 
+## 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, 
-GetCollection, Put, Post, Delete).
+GetCollection, Put, Post, Delete) via les annotations.
 
 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
 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
 
+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 
 
 ### 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\Get;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Delete;
 use ApiPlatform\Metadata\ApiResource;
 use App\Entity\AccessWish\DocumentWish;
 use App\Entity\Booking\Event;
@@ -36,12 +38,10 @@ use App\Enum\Core\FileStatusEnum;
  */
 #[ApiResource(
     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]
@@ -182,10 +182,14 @@ class File
 
     //    #[ORM\Column]
     //    private ?int $eventReport_id;
-    //
-    //    #[ORM\Column]
-    //    private ?\DateTime $availabilityDate;
-    //
+
+    /**
+     * TODO: complete
+     * @var DateTime|null
+     */
+    #[ORM\Column]
+    private ?\DateTime $availabilityDate;
+
     //    #[ORM\Column]
     //    private ?int $documentWish_id;
     //
@@ -197,6 +201,7 @@ class File
     //
     //    #[ORM\Column]
     //    private ?int $work_id;
+
     #[ORM\ManyToMany(targetEntity: Person::class, inversedBy: 'personFiles')]
     #[Groups(['file_person'])]
     private Collection $accessPersons;
@@ -377,6 +382,15 @@ class File
         return $this;
     }
 
+    /**
+     * A priori inutilisé
+     * @deprecated
+     * @return Collection
+     */
+    public function getAccessRoles(): Collection {
+        return new ArrayCollection([]);
+    }
+
     /**
      * @return string
      */
@@ -614,6 +628,22 @@ class File
         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>
      */

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

@@ -8,10 +8,12 @@ use MyCLabs\Enum\Enum;
  * @method static UNKNOWN()
  * @method static NONE()
  * @method static LICENCE_CMF()
+ * @method static BILL()
  */
 class FileTypeEnum extends Enum
 {
     private const UNKNOWN = 'UNKNOWN';
     private const NONE = 'NONE';
     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;
 
 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;
 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
 {
-    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(
-        private Security $security
+        private Security $security,
+        protected Utils $accessUtils,
+        private InternalRequestsService $internalRequestsService,
+        EntityManagerInterface $em,
+        private SwitchUser $switchUser
     ) {
         /** @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 ($user instanceof UserInterface) {
+        if ($user instanceof Access) {
             $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;
         }
 
+        // TODO: on pourrait pas intégrer le isGranted à cette méthode?
         if ($this->switchUser->isAllowedToSwitch($user, $subject)) {
             return true;
         }
 
         return true;
     }
-}
+}

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

@@ -47,7 +47,7 @@ class Utils
      * @return bool
      * @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);
     }
@@ -60,6 +60,7 @@ class Utils
      */
     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);
         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
 {
     /**
+     * TODO: documenter
+     *
      * @param Access $access
      * @param UserInterface $user_to_switch
      * @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)
             ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['hasRoles'])
+            ->setMethodsExcept(['hasRole'])
             ->getMock();
 
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
 
         $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'));
     }
 
     /**