src/Security/Voter/InsightsVoter.php line 14

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Security\Voter;
  3. use MedBrief\MSR\Entity\Account;
  4. use MedBrief\MSR\Entity\Project;
  5. use MedBrief\MSR\Entity\User;
  6. use Override;
  7. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  8. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  9. use Symfony\Component\Security\Core\Authorization\Voter\Voter;
  10. use Symfony\Component\Security\Core\User\UserInterface;
  11. class InsightsVoter extends Voter
  12. {
  13. public const VIEW_RECORDS_INSIGHTS = 'VIEW_RECORDS_INSIGHTS';
  14. public function __construct(private readonly AuthorizationCheckerInterface $auth)
  15. {
  16. }
  17. #[Override]
  18. protected function supports(string $attribute, mixed $subject): bool
  19. {
  20. return $attribute === self::VIEW_RECORDS_INSIGHTS && $subject instanceof Project;
  21. }
  22. #[Override]
  23. protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
  24. {
  25. $user = $token->getUser();
  26. if (!$user instanceof UserInterface) {
  27. return false;
  28. }
  29. /** @var Project $project */
  30. $project = $subject;
  31. return match ($attribute) {
  32. self::VIEW_RECORDS_INSIGHTS => $this->canViewRecordsInsights($user, $project),
  33. default => false,
  34. };
  35. }
  36. /**
  37. * Determines if the user can view the insights button and panel.
  38. * User roles allowed:
  39. * - MB super admins (ROLE_SUPER_ADMIN)
  40. * - MB admins (ROLE_ADMIN)
  41. * - Client super admins (ROLE_ACCOUNT_{accountId}_SUPERADMINISTRATOR)
  42. * - Client admins (ROLE_ACCOUNT_{accountId}_ADMINISTRATOR)
  43. * - Matter/Project managers:
  44. * - Role-based: ROLE_PROJECT_{projectId}_PROJECTMANAGER
  45. * - Entity-based: $project->getManager() === $user
  46. *
  47. * @param UserInterface $user
  48. * @param Project $project
  49. *
  50. * @return bool Returns true if the user can view records insights, false otherwise
  51. */
  52. private function canViewRecordsInsights(UserInterface $user, Project $project): bool
  53. {
  54. $account = $project->getAccount();
  55. // Only consider Client matters (no Disclosures or other non-matter projects)
  56. if ($project->getType() !== Project::TYPE_MATTER_FIRM) {
  57. return false;
  58. }
  59. // MB admins always have access to insights, regardless of account settings or user opt-in
  60. if ($this->auth->isGranted('ROLE_SUPER_ADMIN') || $this->auth->isGranted('ROLE_ADMIN')) {
  61. return true;
  62. }
  63. // If insights is not enabled at the account level, then only MB admins can access insights
  64. if (!$account instanceof Account || !$account->isInsightsEnabled()) {
  65. return false;
  66. }
  67. // For client users, check if they have insights access based on account settings
  68. if (!$this->hasUserInsightsAccess($account, $user)) {
  69. return false;
  70. }
  71. // For client users, they must have an eligible role and insights must be enabled for the account, then we check opt-in settings
  72. if (!$this->hasEligibleInsightsRole($user, $project, $account)) {
  73. return false;
  74. }
  75. // At this point, the project is eligible for Insights and the client user has
  76. // both an allowed role and the required opt-in access, so access is granted.
  77. return true;
  78. }
  79. /**
  80. * Checks if the user has an eligible role for insights access based on their relationship to the project and account.
  81. *
  82. * @param UserInterface $user The user for whom we are checking roles
  83. * @param Project $project The project associated with the insights access
  84. * @param Account $account The account associated with the project
  85. *
  86. * @return bool Returns true if the user has an eligible role for insights access, false otherwise
  87. */
  88. private function hasEligibleInsightsRole(UserInterface $user, Project $project, Account $account): bool
  89. {
  90. // Client-level admins
  91. if ($this->auth->isGranted('ROLE_ACCOUNT_' . $account->getId() . '_SUPERADMINISTRATOR')) {
  92. return true;
  93. }
  94. if ($this->auth->isGranted('ROLE_ACCOUNT_' . $account->getId() . '_ADMINISTRATOR')) {
  95. return true;
  96. }
  97. // Matter/Project manager (role-based)
  98. if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getId() . '_PROJECTMANAGER')) {
  99. return true;
  100. }
  101. // Matter/Project manager (entity relationship fallback)
  102. if ($project->getManager() instanceof User && $project->getManager() === $user) {
  103. return true;
  104. }
  105. return false;
  106. }
  107. /**
  108. * Checks if the user has access to insights based on account level and user level opt-in settings.
  109. *
  110. * @param UserInterface $user The user for whom we are checking access
  111. * @param Account $account The account associated with the project for which we are checking access
  112. *
  113. * @return bool Returns true if the user has access to insights, false otherwise
  114. */
  115. private function hasUserInsightsAccess(Account $account, UserInterface $user): bool
  116. {
  117. // If insights is enabled for all client users, then any user with an eligible role
  118. // can access,otherwise we check the user's individual opt-in setting
  119. if ($account->isInsightsOptInAllClientUsers()) {
  120. return true;
  121. }
  122. // If insights is only enabled for specific users, then we check the user's opt-in setting
  123. if ($account->isInsightsOptInSpecificUsers() && $user instanceof User) {
  124. return $user->getInsightsOptIn();
  125. }
  126. return false;
  127. }
  128. }