src/Security/Voter/CollectionVoter.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\Collection;
  6. use MedBrief\MSR\Entity\ProjectUser;
  7. use Override;
  8. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  9. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  10. use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
  11. use Symfony\Component\Security\Core\User\UserInterface;
  12. class CollectionVoter implements VoterInterface
  13. {
  14. public const CREATE = 'CREATE';
  15. public const READ = 'READ';
  16. public const UPDATE = 'UPDATE';
  17. public const DELETE = 'DELETE';
  18. public const ADMINISTRATION = 'ADMINISTRATION';
  19. public const DOWNLOAD = 'DOWNLOAD';
  20. public const ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS = 'ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS';
  21. public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly EntityManagerInterface $entityManager)
  22. {
  23. }
  24. public function supportsAttribute($attribute): bool
  25. {
  26. return in_array($attribute, [
  27. self::CREATE,
  28. self::READ,
  29. self::UPDATE,
  30. self::DELETE,
  31. self::ADMINISTRATION,
  32. self::DOWNLOAD,
  33. self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS,
  34. ]);
  35. }
  36. public function supportsClass($class): bool
  37. {
  38. $supportedClass = Collection::class;
  39. return $supportedClass === $class || is_subclass_of($class, $supportedClass);
  40. }
  41. /**
  42. *
  43. * @param mixed $entity
  44. */
  45. #[Override]
  46. public function vote(TokenInterface $token, $entity, array $attributes)
  47. {
  48. /**
  49. * START: This is common code for all Voter::vote() methods
  50. */
  51. // check if class of this object is supported by this voter
  52. if (!$this->supportsClass($entity && !is_array($entity) ? $entity::class : '')) {
  53. return VoterInterface::ACCESS_ABSTAIN;
  54. }
  55. // check if the voter is used correct, only allow one attribute
  56. // this isn't a requirement, it's just one easy way for you to
  57. // design your voter
  58. if (1 !== count($attributes)) {
  59. throw new InvalidArgumentException(
  60. 'Only one attribute is allowed for Medbrief Voters.'
  61. );
  62. }
  63. // set the attribute to check against
  64. $attribute = $attributes[0];
  65. // check if the given attribute is covered by this voter
  66. if (!$this->supportsAttribute($attribute)) {
  67. return VoterInterface::ACCESS_ABSTAIN;
  68. }
  69. // get current logged in user
  70. $user = $token->getUser();
  71. // make sure there is a user object (i.e. that the user is logged in)
  72. if (!$user instanceof UserInterface) {
  73. return VoterInterface::ACCESS_DENIED;
  74. }
  75. // Admin users can do everything
  76. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  77. return VoterInterface::ACCESS_GRANTED;
  78. }
  79. /**
  80. * END: Common code for all Voter:vote() methods. Put custom logic below.
  81. */
  82. // If this user has ADMINISTRATION rights for the project to which this
  83. // collection belongs
  84. if ($this->authorizationChecker->isGranted('ADMINISTRATION', $entity->getProject())) {
  85. // then they have access to do anything
  86. return VoterInterface::ACCESS_GRANTED;
  87. }
  88. // Decide if we allow the advanced annotation download options to display in the form for this collection.
  89. if ($attribute === self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS && ($entity->getProject() && $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getProject()->getId() . '_PROJECTMANAGER'))) {
  90. return VoterInterface::ACCESS_GRANTED;
  91. }
  92. if ($attribute == self::UPDATE || $attribute == self::DELETE) {
  93. // Retrieve the collection's root collection
  94. $rootCollection = $this->entityManager->getRepository(Collection::class)->find($entity->getRoot());
  95. // Get the project from the collection's root collection.
  96. $project = $rootCollection->getProject();
  97. // MEDICAL_RECORDS_ADMINISTRATION can update and delete any medical records Collection.
  98. if ($this->authorizationChecker->isGranted('MEDICAL_RECORDS_ADMINISTRATION', $project)) {
  99. $isGranted
  100. // Is the Collection in the public medical records Collection?
  101. = ($project->getMedicalRecordsCollection() && $project->getMedicalRecordsCollection()->containsCollectionRecursive($entity))
  102. // Or Private Collections?
  103. || ($project->getPrivateCollection() && $project->getPrivateCollection()->containsCollectionRecursive($entity))
  104. // Or PrivateSandbox Collections?
  105. || ($project->getPrivateSandboxCollection() && $project->getPrivateSandboxCollection()->containsCollectionRecursive($entity))
  106. // Or Unsorted Records collection?
  107. || ($project->getUnsortedRecordsCollection() && $project->getUnsortedRecordsCollection()->containsCollectionRecursive($entity));
  108. if ($isGranted) {
  109. return VoterInterface::ACCESS_GRANTED;
  110. }
  111. }
  112. // Grab the ProjectUser, so we can do some checks against the user's private collections.
  113. $projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $project]);
  114. // A user can update and delete collections that they created, contained in their PrivateSandbox collection.
  115. if ($projectUser) {
  116. $isGranted
  117. // Is the collection in the user's PrivateSandbox Collection?
  118. = $projectUser->getPrivateSandboxCollection()
  119. && $projectUser->getPrivateSandboxCollection()->containsCollectionRecursive($entity)
  120. // And did they create the Collection?
  121. && $entity->getCreator()
  122. && $entity->getCreator()->getId() == $user->getId();
  123. if ($isGranted) {
  124. // If we're updating, no problem...
  125. if ($attribute == self::UPDATE) {
  126. return VoterInterface::ACCESS_GRANTED;
  127. }
  128. // If we're deleting, then we need to check there are no Collections/Documents in the Collection that the user
  129. // does not have permission to delete.
  130. if ($attribute == self::DELETE) {
  131. // Check the user has access to delete the Documents contained directly in the collection.
  132. foreach ($entity->getDocuments() as $document) {
  133. if (!$this->authorizationChecker->isGranted('DELETE', $document)) {
  134. return VoterInterface::ACCESS_DENIED;
  135. }
  136. }
  137. // Grab all child Collections of the Collection
  138. $children = $this->entityManager->getRepository(Collection::class)->getChildren($entity);
  139. foreach ($children as $childCollection) {
  140. // Check the user has access to delete each of the child Collections, if one fails, we deny access.
  141. if (!$this->authorizationChecker->isGranted('DELETE', $childCollection)) {
  142. return VoterInterface::ACCESS_DENIED;
  143. }
  144. // Check the user has access to delete the Documents in the child Collection, If one fails, we deny access.
  145. foreach ($childCollection->getDocuments() as $document) {
  146. if (!$this->authorizationChecker->isGranted('DELETE', $document)) {
  147. return VoterInterface::ACCESS_DENIED;
  148. }
  149. }
  150. }
  151. // All clear for DELETE, grant access
  152. return VoterInterface::ACCESS_GRANTED;
  153. }
  154. }
  155. }
  156. }
  157. if ($attribute == self::DOWNLOAD) {
  158. // Get the Project from the Collection's root collection.
  159. $rootCollection = $this->entityManager->getRepository(Collection::class)->find($entity->getRoot());
  160. $project = $rootCollection->getProject();
  161. // MEDICAL_RECORDS_ADMINISTRATION can download all medical record Collections.
  162. if ($this->authorizationChecker->isGranted('MEDICAL_RECORDS_ADMINISTRATION', $project)) {
  163. $isGranted
  164. // Is the Collection in the Public Medical Record Collection?
  165. = ($project->getMedicalRecordsCollection() && $project->getMedicalRecordsCollection()->containsCollectionRecursive($entity))
  166. // Or, Private Medical Record collection?
  167. || ($project->getPrivateCollection() && $project->getPrivateCollection()->containsCollectionRecursive($entity))
  168. // Or, PrivateSandbox Medical Record collection?
  169. || ($project->getPrivateSandboxCollection() && $project->getPrivateSandboxCollection()->containsCollectionRecursive($entity))
  170. // Or, Unsorted Records collection?
  171. || ($project->getUnsortedRecordsCollection() && $project->getUnsortedRecordsCollection()->containsCollectionRecursive($entity));
  172. if ($isGranted) {
  173. return VoterInterface::ACCESS_GRANTED;
  174. }
  175. }
  176. // Grab the ProjectUser, so we can do some checks against the user's private Collections.
  177. $projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $project]);
  178. // Users with MEDICAL_RECORD_DOWNLOAD can download collections in the public medical records folder, as well as collections in their
  179. // private collection. They can also download records from the Unsorted Records folder.
  180. if ($projectUser && $this->authorizationChecker->isGranted('MEDICAL_RECORD_DOWNLOAD', $project)) {
  181. $isGranted
  182. // Is the Collection in the Public Medical Record Collection?
  183. = ($project->getMedicalRecordsCollection() && $project->getMedicalRecordsCollection()->containsCollectionRecursive($entity))
  184. // Or, is it in user's own Private Medical Record Collection?
  185. || ($projectUser->getPrivateCollection() && $projectUser->getPrivateCollection()->containsCollectionRecursive($entity))
  186. // Or, is it in the Unsorted Records collection?
  187. || ($project->getUnsortedRecordsCollection() && $project->getUnsortedRecordsCollection()->containsCollectionRecursive($entity));
  188. if ($isGranted) {
  189. return VoterInterface::ACCESS_GRANTED;
  190. }
  191. }
  192. // All users can download collections in their PrivateSandbox collections.
  193. if ($projectUser) {
  194. $isGranted
  195. // Is in user's PrivateSandbox Medical Record Collection?
  196. = $projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsCollectionRecursive($entity);
  197. if ($isGranted) {
  198. return VoterInterface::ACCESS_GRANTED;
  199. }
  200. }
  201. }
  202. // If we get to the end of this function, then no decisions have been
  203. // made so we deny access
  204. return VoterInterface::ACCESS_DENIED;
  205. }
  206. }