<?php
namespace MedBrief\MSR\Security\Voter;
use MedBrief\MSR\Entity\MessageThread;
use MedBrief\MSR\Entity\MessageThreadParticipant;
use MedBrief\MSR\Entity\Project;
use MedBrief\MSR\Repository\MessageThreadParticipantRepository;
use MedBrief\MSR\Repository\MessageThreadRepository;
use Override;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class MatchVoter extends Voter
{
public const VIEW_FIRM = 'MATCH_VIEW_FIRM';
public const VIEW_EXPERT = 'MATCH_VIEW_EXPERT';
public function __construct(
private readonly AuthorizationCheckerInterface $auth,
private readonly MessageThreadRepository $messageThreadRepository,
private readonly MessageThreadParticipantRepository $messageThreadParticipantRepository,
) {
}
#[Override]
protected function supports(string $attribute, mixed $subject): bool
{
return \in_array($attribute, [self::VIEW_FIRM, self::VIEW_EXPERT], true)
&& $subject instanceof Project;
}
#[Override]
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
/** @var Project $project */
$project = $subject;
return match ($attribute) {
self::VIEW_FIRM => $this->canViewAsFirm($user, $project),
self::VIEW_EXPERT => $this->canViewAsExpert($user, $project),
default => false,
};
}
/**
* Determines if the user can view the match tab.
* User roles allowed:
* - MB super admins (ROLE_SUPER_ADMIN)
* - MB admins (ROLE_ADMIN)
* - Client super admins (ROLE_ACCOUNT_{accountId}_SUPERADMINISTRATOR)
* - Client admins (ROLE_ACCOUNT_{accountId}_ADMINISTRATOR)
* - Matter/Project managers:
* - Role-based: ROLE_PROJECT_{projectId}_PROJECTMANAGER
* - Entity-based: $project->getManager() === $user
*
* @param UserInterface $user
* @param Project $project
*
* @return bool
*/
private function canViewAsFirm(UserInterface $user, Project $project): bool
{
// 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
if (!$project->getAccount()->getMatchOptIn()
|| $project->getMatterType() !== Project::MATTER_TYPE_CLINICAL_NEGLIGENCE
|| $project->getType() !== Project::TYPE_MATTER_FIRM
|| $project->getMatterRequest() === null
) {
return false;
}
// MB admins
if ($this->auth->isGranted('ROLE_SUPER_ADMIN') || $this->auth->isGranted('ROLE_ADMIN')) {
return true;
}
// Client-level admins
if ($this->auth->isGranted('ROLE_ACCOUNT_' . $project->getAccount()->getId() . '_SUPERADMINISTRATOR')) {
return true;
}
if ($this->auth->isGranted('ROLE_ACCOUNT_' . $project->getAccount()->getId() . '_ADMINISTRATOR')) {
return true;
}
// Matter/Project manager (role-based)
if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getId() . '_PROJECTMANAGER')) {
return true;
}
// Matter/Project manager (entity relationship fallback)
if ($project->getManager() && $project->getManager() === $user) {
return true;
}
return false;
}
/**
* Scaffolding in case we want to limit which users can message experts.
*
* @param UserInterface $user
* @param Project $project
*
* @return bool
*/
private function canViewAsExpert(UserInterface $user, Project $project): bool
{
// Invited Experts
if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getAccount()->getId() . '_EXPERT')) {
return true;
}
if ($this->auth->isGranted('ROLE_PROJECT_' . $project->getAccount()->getId() . '_EXPERTVIEWER')) {
return true;
}
// Uninvited Experts can only view if they are participants in the message thread for the project
$thread = $this->messageThreadRepository->findOneBy(['project' => $project]) ?? null;
if ($thread instanceof MessageThread) {
$threadParticipant = $this->messageThreadParticipantRepository->findParticipant($thread, $user);
if ($threadParticipant instanceof MessageThreadParticipant) {
return true;
}
}
return false;
}
}