src/Security/Voter/MatchVoter.php line 19

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Security\Voter;
  3. use MedBrief\MSR\Entity\Expert;
  4. use MedBrief\MSR\Entity\Project;
  5. use MedBrief\MSR\Entity\User;
  6. use MedBrief\MSR\Repository\AccountRepository;
  7. use MedBrief\MSR\Repository\ExpertRepository;
  8. use MedBrief\MSR\Repository\MatchLetterResponseRepository;
  9. use MedBrief\MSR\Repository\MessageThreadParticipantRepository;
  10. use MedBrief\MSR\Repository\ProjectRepository;
  11. use Override;
  12. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  13. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  14. use Symfony\Component\Security\Core\Authorization\Voter\Voter;
  15. use Symfony\Component\Security\Core\User\UserInterface;
  16. class MatchVoter extends Voter
  17. {
  18. public const VIEW_FIRM = 'MATCH_VIEW_FIRM';
  19. public const VIEW_EXPERT = 'MATCH_VIEW_EXPERT';
  20. public const VIEW_EXPERT_TERMS_ACCEPTED = 'MATCH_VIEW_EXPERT_TERMS_ACCEPTED';
  21. public const VIEW_MESSAGES_NAV = 'MATCH_VIEW_MESSAGES_NAV';
  22. public function __construct(
  23. private readonly AuthorizationCheckerInterface $auth,
  24. private readonly AccountRepository $accountRepository,
  25. private readonly ExpertRepository $expertRepository,
  26. private readonly MessageThreadParticipantRepository $messageThreadParticipantRepository,
  27. private readonly ProjectRepository $projectRepository,
  28. private readonly MatchLetterResponseRepository $matchLetterResponseRepository
  29. ) {
  30. }
  31. #[Override]
  32. protected function supports(string $attribute, mixed $subject): bool
  33. {
  34. if ($attribute === self::VIEW_MESSAGES_NAV || $attribute === self::VIEW_EXPERT_TERMS_ACCEPTED) {
  35. return $subject === null;
  36. }
  37. return \in_array($attribute, [self::VIEW_FIRM, self::VIEW_EXPERT], true)
  38. && $subject instanceof Project;
  39. }
  40. #[Override]
  41. protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
  42. {
  43. $user = $token->getUser();
  44. if (!$user instanceof UserInterface) {
  45. return false;
  46. }
  47. /** @var Project $project */
  48. $project = $subject;
  49. return match ($attribute) {
  50. self::VIEW_FIRM => $this->canViewAsFirm($user, $project),
  51. self::VIEW_EXPERT => $this->canViewAsExpert($user, $project),
  52. self::VIEW_EXPERT_TERMS_ACCEPTED => $this->canViewExpertTermsAccepted($user),
  53. self::VIEW_MESSAGES_NAV => $this->canViewMessagesNavigation($user),
  54. default => false,
  55. };
  56. }
  57. /**
  58. * Determines if the user can view the match tab.
  59. * User roles allowed:
  60. * - MB super admins (ROLE_SUPER_ADMIN)
  61. * - MB admins (ROLE_ADMIN)
  62. * - Client super admins (ROLE_ACCOUNT_{accountId}_SUPERADMINISTRATOR)
  63. * - Client admins (ROLE_ACCOUNT_{accountId}_ADMINISTRATOR)
  64. * - Matter/Project managers:
  65. * - Role-based: ROLE_PROJECT_{projectId}_PROJECTMANAGER
  66. * - Entity-based: $project->getManager() === $user
  67. *
  68. * @param UserInterface $user
  69. * @param Project $project
  70. *
  71. * @return bool
  72. */
  73. private function canViewAsFirm(UserInterface $user, Project $project): bool
  74. {
  75. // Check if client has opted in to match and if the matter is of type Clinical Negligence and is a client matter, if classic matter, don't allow viewing of match
  76. if (!$project->getAccount()->getMatchOptIn()
  77. || $project->getMatterType() !== Project::MATTER_TYPE_CLINICAL_NEGLIGENCE
  78. || $project->getType() !== Project::TYPE_MATTER_FIRM
  79. || $project->getMatterRequest() === null
  80. ) {
  81. return false;
  82. }
  83. // MB admins
  84. if ($this->auth->isGranted('ROLE_SUPER_ADMIN') || $this->auth->isGranted('ROLE_ADMIN')) {
  85. return true;
  86. }
  87. // Client-level admins
  88. if ($this->auth->isGranted('ROLE_ACCOUNT_' . $project->getAccount()->getId() . '_SUPERADMINISTRATOR')) {
  89. return true;
  90. }
  91. if ($this->auth->isGranted('ROLE_ACCOUNT_' . $project->getAccount()->getId() . '_ADMINISTRATOR')) {
  92. return true;
  93. }
  94. // Matter/Project manager (role-based)
  95. if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getId() . '_PROJECTMANAGER')) {
  96. return true;
  97. }
  98. // Matter/Project manager (entity relationship fallback)
  99. if ($project->getManager() && $project->getManager() === $user) {
  100. return true;
  101. }
  102. return false;
  103. }
  104. /**
  105. * Determines if a user can view as an Expert.
  106. *
  107. * @param UserInterface $user
  108. * @param Project $project
  109. *
  110. * @return bool
  111. */
  112. private function canViewAsExpert(UserInterface $user, Project $project): bool
  113. {
  114. // Invited Experts
  115. if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getId() . '_EXPERT')) {
  116. return true;
  117. }
  118. if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getId() . '_EXPERTVIEWER')) {
  119. return true;
  120. }
  121. // Uninvited Experts can only view if they are participants in the message thread for the project
  122. if (!$user instanceof User) {
  123. return false;
  124. }
  125. return $this->messageThreadParticipantRepository->isUserParticipantInProject($project, $user);
  126. }
  127. /**
  128. * Determines if a user can see the messages navigation tab
  129. *
  130. * @param UserInterface $user
  131. *
  132. * @return bool
  133. */
  134. private function canViewMessagesNavigation(UserInterface $user): bool
  135. {
  136. // MB admins
  137. if ($this->auth->isGranted('ROLE_SUPER_ADMIN') || $this->auth->isGranted('ROLE_ADMIN')) {
  138. return true;
  139. }
  140. // If the user has the expert or expert viewer role, allow access
  141. if ($user instanceof User && ($user->isExpertOnly() || $user->isExpertViewerOnly())) {
  142. // Only experts with an expert profile
  143. if ($this->expertRepository->findOneBy(['user' => $user]) instanceof Expert) {
  144. return true;
  145. }
  146. }
  147. // Extract account IDs and project IDs from user roles
  148. $userRoles = $user->getRoles();
  149. $accountIds = [];
  150. $projectIds = [];
  151. foreach ($userRoles as $role) {
  152. if (preg_match('/^ROLE_ACCOUNT_(\d+)_/', $role, $matches)) {
  153. $accountIds[] = (int) $matches[1];
  154. }
  155. if (preg_match('/^ROLE_PROJECT_(\d+)_/', $role, $matches)) {
  156. $projectIds[] = (int) $matches[1];
  157. }
  158. }
  159. // Remove duplicates
  160. $accountIds = array_unique($accountIds);
  161. $projectIds = array_unique($projectIds);
  162. // Check if any accounts have match enabled
  163. if (!empty($accountIds)) {
  164. $accounts = $this->accountRepository->findBy(['id' => $accountIds]);
  165. foreach ($accounts as $account) {
  166. if ($account->getMatchOptIn()) {
  167. return true;
  168. }
  169. }
  170. }
  171. // Check if any projects with accounts have match enabled
  172. if (!empty($projectIds)) {
  173. $projects = $this->projectRepository->findBy(['id' => $projectIds]);
  174. foreach ($projects as $project) {
  175. $account = $project->getAccount();
  176. if ($account && $account->getMatchOptIn()) {
  177. return true;
  178. }
  179. }
  180. }
  181. return false;
  182. }
  183. /**
  184. * Determines if an expert user has accepted the expert terms.
  185. *
  186. * @param UserInterface $user
  187. *
  188. * @return bool
  189. */
  190. private function canViewExpertTermsAccepted(UserInterface $user): bool
  191. {
  192. // If the user has the expert or expert viewer role, allow access
  193. if ($user instanceof User && ($user->isExpertOnly() || $user->isExpertViewerOnly())) {
  194. // Only experts with an expert profile
  195. if ($this->expertRepository->findOneBy(['user' => $user]) instanceof Expert) {
  196. // Check if the expert has accepted the terms
  197. if ($this->matchLetterResponseRepository->hasExpertAcceptedTerms($user)) {
  198. return true;
  199. }
  200. }
  201. }
  202. // Default deny
  203. return false;
  204. }
  205. }