src/Security/Voter/DocumentVoter.php line 16

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Security\Voter;
  3. use Doctrine\ORM\EntityManagerInterface;
  4. use InvalidArgumentException;
  5. use MedBrief\MSR\Entity\Document;
  6. use MedBrief\MSR\Entity\ProjectUser;
  7. use MedBrief\MSR\Service\EntityHelper\UserHelper;
  8. use Override;
  9. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  10. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  11. use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
  12. use Symfony\Component\Security\Core\User\UserInterface;
  13. class DocumentVoter implements VoterInterface
  14. {
  15. public const CREATE = 'CREATE';
  16. public const READ = 'READ';
  17. public const UPDATE = 'UPDATE';
  18. public const DELETE = 'DELETE';
  19. public const ADMINISTRATION = 'ADMINISTRATION';
  20. public const DOWNLOAD_NATIVE = 'DOWNLOAD_NATIVE';
  21. public const STREAM_NATIVE = 'STREAM_NATIVE';
  22. // Controls if the user can update the annotation visibility of a Document's annotations.
  23. public const UPDATE_ANNOTATION_VISIBILITY = 'UPDATE_ANNOTATION_VISIBILITY';
  24. public const ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS = 'ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS';
  25. public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly EntityManagerInterface $entityManager, private readonly UserHelper $userHelper)
  26. {
  27. }
  28. public function supportsAttribute($attribute): bool
  29. {
  30. return in_array($attribute, [
  31. self::CREATE,
  32. self::READ,
  33. self::UPDATE,
  34. self::DELETE,
  35. self::ADMINISTRATION,
  36. self::DOWNLOAD_NATIVE,
  37. self::STREAM_NATIVE,
  38. self::UPDATE_ANNOTATION_VISIBILITY,
  39. self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS,
  40. ]);
  41. }
  42. public function supportsClass($class): bool
  43. {
  44. $supportedClass = Document::class;
  45. return $supportedClass === $class || is_subclass_of($class, $supportedClass);
  46. }
  47. /**
  48. *
  49. * @param mixed $entity
  50. */
  51. #[Override]
  52. public function vote(TokenInterface $token, $entity, array $attributes)
  53. {
  54. /**
  55. * START: This is common code for all Voter::vote() methods
  56. */
  57. // check if class of this object is supported by this voter
  58. if (!$this->supportsClass($entity && !is_array($entity) ? $entity::class : '')) {
  59. return VoterInterface::ACCESS_ABSTAIN;
  60. }
  61. // check if the voter is used correct, only allow one attribute
  62. // this isn't a requirement, it's just one easy way for you to
  63. // design your voter
  64. if (1 !== count($attributes)) {
  65. throw new InvalidArgumentException(
  66. 'Only one attribute is allowed for Medbrief Voters.'
  67. );
  68. }
  69. // set the attribute to check against
  70. $attribute = $attributes[0];
  71. // check if the given attribute is covered by this voter
  72. if (!$this->supportsAttribute($attribute)) {
  73. return VoterInterface::ACCESS_ABSTAIN;
  74. }
  75. // get current logged in user
  76. $user = $token->getUser();
  77. // make sure there is a user object (i.e. that the user is logged in)
  78. if (!$user instanceof UserInterface) {
  79. return VoterInterface::ACCESS_DENIED;
  80. }
  81. // Admin users can do everything
  82. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  83. return VoterInterface::ACCESS_GRANTED;
  84. }
  85. /**
  86. * END: Common code for all Voter:vote() methods. Put custom logic below.
  87. */
  88. $this->userHelper->setUser($user);
  89. // if this user has ADMINISTRATION rights for the project to which this
  90. // document belongs
  91. if ($this->authorizationChecker->isGranted('ADMINISTRATION', $entity->getProject())) {
  92. // then they have access to do anything
  93. return VoterInterface::ACCESS_GRANTED;
  94. }
  95. // Check if the user can update annotation visibility for a Project's documents.
  96. // Project Managers can do this.
  97. if (($attribute === self::UPDATE_ANNOTATION_VISIBILITY || $attribute === self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS) && $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getProject()->getId() . '_PROJECTMANAGER')) {
  98. return VoterInterface::ACCESS_GRANTED;
  99. }
  100. // if this user has Medical Records administration or Radiology Administration - this was added to allow client
  101. // level sorters to have the access they need
  102. // @todo We should probably do an additiona check here to see what kind of document we have here (a medical record
  103. // or a radiology file). But at the moment I don't think it is a problem because I user that has one of the below
  104. // access levels always has the other one as well
  105. // For everything, except updating the visibility of annotations.
  106. if (($this->authorizationChecker->isGranted('MEDICAL_RECORDS_ADMINISTRATION', $entity->getProject()) || $this->authorizationChecker->isGranted('RADIOLOGY_ADMINISTRATION', $entity->getProject())) && ($attribute !== self::UPDATE_ANNOTATION_VISIBILITY && $attribute !== self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS)) {
  107. // then they have access to do anything
  108. return VoterInterface::ACCESS_GRANTED;
  109. }
  110. // if the user is wanting to stream the native (I.E view) or read this document of this document
  111. if ($attribute == self::STREAM_NATIVE || $attribute == self::READ) {
  112. // if the user is an expert agency administrator let them pass.
  113. // this should probably be done in a better way, tying permissions
  114. // to an entity in one place somehow....
  115. if ($user->isExpertAgencyAdministrator()) {
  116. return VoterInterface::ACCESS_GRANTED;
  117. }
  118. $project = $entity->getProject();
  119. // if the user has a role on the project that gives them
  120. // administrative access to it
  121. if ($this->userHelper->hasProjectAdministrativeRole($project)) {
  122. // then they have access
  123. return VoterInterface::ACCESS_GRANTED;
  124. // else if they have any other role on the project (currently just EXPERT or EXPERTVIEWER)
  125. // Or, if the user has disclosure project read access (Project needs to then be type disclosure)
  126. } elseif ($this->userHelper->hasProjectRole($project) || $this->userHelper->hasDisclosureProjectReadAccess($project)) {
  127. // then grab their project user record (if they have one)
  128. $projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
  129. // if they have a project user record for the project
  130. if ($projectUser) {
  131. // and if the document is in the project's public collection or
  132. // is in this user's private/PrivateSandbox collection or
  133. // in the project's Unsorted Records collection
  134. if ($project->getMedicalRecordsCollection()->containsDocumentRecursive($entity)
  135. || ($projectUser->getPrivateCollection() && $projectUser->getPrivateCollection()->containsDocumentRecursive($entity))
  136. || ($projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity))
  137. || ($project->getUnsortedRecordsCollection() && $project->getUnsortedRecordsCollection()->containsDocumentRecursive($entity))
  138. ) {
  139. // then they have access
  140. return VoterInterface::ACCESS_GRANTED;
  141. }
  142. // if this is a radiology file
  143. // @todo We need a better way to determine if this is a radiology file
  144. if ($entity->getMimeType() == 'application/dicom') {
  145. // then they have access
  146. return VoterInterface::ACCESS_GRANTED;
  147. }
  148. }
  149. }
  150. }
  151. // if the user wants to delete or update this document
  152. if ($attribute == self::DELETE || $attribute == self::UPDATE) {
  153. // if it is a dicom file
  154. // @todo we should actually do a DB check to see if this document
  155. // is connected toe a Disc
  156. //then if this user may manage radiology for the documents project
  157. if ($entity->getMimeType() == 'application/dicom' && $this->authorizationChecker->isGranted('RADIOLOGY_ADMINISTRATION', $entity->getProject())) {
  158. // then they have access
  159. return VoterInterface::ACCESS_GRANTED;
  160. }
  161. //else we assume that this is a medical record (since there are no other kinds of document) So then if they have
  162. // medical records administration access
  163. if ($this->authorizationChecker->isGranted('MEDICAL_RECORDS_ADMINISTRATION', $entity->getProject())) {
  164. // then they have access
  165. return VoterInterface::ACCESS_GRANTED;
  166. }
  167. // Grab their project user record (if they have one)
  168. $projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
  169. // If the document is in the PrivateSandbox collection of the user, and they created it, allow them to update/delete it.
  170. if ($projectUser && $projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity) && $entity->getCreator() && $entity->getCreator()->getId() == $user->getId()) {
  171. return VoterInterface::ACCESS_GRANTED;
  172. }
  173. }
  174. // finally if the user is trying to download the document
  175. if ($attribute == self::DOWNLOAD_NATIVE) {
  176. // If the document is in the public medical records collection, or the public unsorted records collection, a user with MEDICAL_RECORD_DOWNLOAD can download it.
  177. if (
  178. ($entity->getProject()->getMedicalRecordsCollection()->containsDocumentRecursive($entity) || $entity->getProject()->getUnsortedRecordsCollection() && $entity->getProject()->getUnsortedRecordsCollection()->containsDocumentRecursive($entity)) && $this->authorizationChecker->isGranted('MEDICAL_RECORD_DOWNLOAD', $entity->getProject())
  179. ) {
  180. return VoterInterface::ACCESS_GRANTED;
  181. }
  182. // Grab the projectUser, so we can do some checks against the associated collections.
  183. $projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
  184. // If the document is in their own Private collection, a user with MEDICAL_RECORD_DOWNLOAD can download it.
  185. if ($projectUser && $projectUser->getPrivateCollection() && $projectUser->getPrivateCollection()->containsDocumentRecursive($entity) && $this->authorizationChecker->isGranted('MEDICAL_RECORD_DOWNLOAD', $entity->getProject())) {
  186. return VoterInterface::ACCESS_GRANTED;
  187. }
  188. // If the document is in the user's PrivateSandbox collection, they can download it.
  189. if ($projectUser && $projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity)) {
  190. return VoterInterface::ACCESS_GRANTED;
  191. }
  192. }
  193. // If we get to the end of this function, then no decisions have been
  194. // made so we deny access
  195. return VoterInterface::ACCESS_DENIED;
  196. }
  197. }