<?php
namespace MedBrief\MSR\Security\Voter;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use MedBrief\MSR\Entity\Document;
use MedBrief\MSR\Entity\ProjectUser;
use MedBrief\MSR\Service\EntityHelper\UserHelper;
use Override;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class DocumentVoter implements VoterInterface
{
public const CREATE = 'CREATE';
public const READ = 'READ';
public const UPDATE = 'UPDATE';
public const DELETE = 'DELETE';
public const ADMINISTRATION = 'ADMINISTRATION';
public const DOWNLOAD_NATIVE = 'DOWNLOAD_NATIVE';
public const STREAM_NATIVE = 'STREAM_NATIVE';
// Controls if the user can update the annotation visibility of a Document's annotations.
public const UPDATE_ANNOTATION_VISIBILITY = 'UPDATE_ANNOTATION_VISIBILITY';
public const ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS = 'ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS';
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly EntityManagerInterface $entityManager, private readonly UserHelper $userHelper)
{
}
public function supportsAttribute($attribute): bool
{
return in_array($attribute, [
self::CREATE,
self::READ,
self::UPDATE,
self::DELETE,
self::ADMINISTRATION,
self::DOWNLOAD_NATIVE,
self::STREAM_NATIVE,
self::UPDATE_ANNOTATION_VISIBILITY,
self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS,
]);
}
public function supportsClass($class): bool
{
$supportedClass = Document::class;
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
*
* @param mixed $entity
*/
#[Override]
public function vote(TokenInterface $token, $entity, array $attributes)
{
/**
* START: This is common code for all Voter::vote() methods
*/
// check if class of this object is supported by this voter
if (!$this->supportsClass($entity && !is_array($entity) ? $entity::class : '')) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new InvalidArgumentException(
'Only one attribute is allowed for Medbrief Voters.'
);
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
// Admin users can do everything
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return VoterInterface::ACCESS_GRANTED;
}
/**
* END: Common code for all Voter:vote() methods. Put custom logic below.
*/
$this->userHelper->setUser($user);
// if this user has ADMINISTRATION rights for the project to which this
// document belongs
if ($this->authorizationChecker->isGranted('ADMINISTRATION', $entity->getProject())) {
// then they have access to do anything
return VoterInterface::ACCESS_GRANTED;
}
// Check if the user can update annotation visibility for a Project's documents.
// Project Managers can do this.
if (($attribute === self::UPDATE_ANNOTATION_VISIBILITY || $attribute === self::ANNOTATION_VISIBILITY_DOWNLOAD_OPTIONS) && $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getProject()->getId() . '_PROJECTMANAGER')) {
return VoterInterface::ACCESS_GRANTED;
}
// if this user has Medical Records administration or Radiology Administration - this was added to allow client
// level sorters to have the access they need
// @todo We should probably do an additiona check here to see what kind of document we have here (a medical record
// 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
// access levels always has the other one as well
// For everything, except updating the visibility of annotations.
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)) {
// then they have access to do anything
return VoterInterface::ACCESS_GRANTED;
}
// if the user is wanting to stream the native (I.E view) or read this document of this document
if ($attribute == self::STREAM_NATIVE || $attribute == self::READ) {
// if the user is an expert agency administrator let them pass.
// this should probably be done in a better way, tying permissions
// to an entity in one place somehow....
if ($user->isExpertAgencyAdministrator()) {
return VoterInterface::ACCESS_GRANTED;
}
$project = $entity->getProject();
// if the user has a role on the project that gives them
// administrative access to it
if ($this->userHelper->hasProjectAdministrativeRole($project)) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
// else if they have any other role on the project (currently just EXPERT or EXPERTVIEWER)
// Or, if the user has disclosure project read access (Project needs to then be type disclosure)
} elseif ($this->userHelper->hasProjectRole($project) || $this->userHelper->hasDisclosureProjectReadAccess($project)) {
// then grab their project user record (if they have one)
$projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
// if they have a project user record for the project
if ($projectUser) {
// and if the document is in the project's public collection or
// is in this user's private/PrivateSandbox collection or
// in the project's Unsorted Records collection
if ($project->getMedicalRecordsCollection()->containsDocumentRecursive($entity)
|| ($projectUser->getPrivateCollection() && $projectUser->getPrivateCollection()->containsDocumentRecursive($entity))
|| ($projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity))
|| ($project->getUnsortedRecordsCollection() && $project->getUnsortedRecordsCollection()->containsDocumentRecursive($entity))
) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
// if this is a radiology file
// @todo We need a better way to determine if this is a radiology file
if ($entity->getMimeType() == 'application/dicom') {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
}
}
}
// if the user wants to delete or update this document
if ($attribute == self::DELETE || $attribute == self::UPDATE) {
// if it is a dicom file
// @todo we should actually do a DB check to see if this document
// is connected toe a Disc
//then if this user may manage radiology for the documents project
if ($entity->getMimeType() == 'application/dicom' && $this->authorizationChecker->isGranted('RADIOLOGY_ADMINISTRATION', $entity->getProject())) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
//else we assume that this is a medical record (since there are no other kinds of document) So then if they have
// medical records administration access
if ($this->authorizationChecker->isGranted('MEDICAL_RECORDS_ADMINISTRATION', $entity->getProject())) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
// Grab their project user record (if they have one)
$projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
// If the document is in the PrivateSandbox collection of the user, and they created it, allow them to update/delete it.
if ($projectUser && $projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity) && $entity->getCreator() && $entity->getCreator()->getId() == $user->getId()) {
return VoterInterface::ACCESS_GRANTED;
}
}
// finally if the user is trying to download the document
if ($attribute == self::DOWNLOAD_NATIVE) {
// 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.
if (
($entity->getProject()->getMedicalRecordsCollection()->containsDocumentRecursive($entity) || $entity->getProject()->getUnsortedRecordsCollection() && $entity->getProject()->getUnsortedRecordsCollection()->containsDocumentRecursive($entity)) && $this->authorizationChecker->isGranted('MEDICAL_RECORD_DOWNLOAD', $entity->getProject())
) {
return VoterInterface::ACCESS_GRANTED;
}
// Grab the projectUser, so we can do some checks against the associated collections.
$projectUser = $this->entityManager->getRepository(ProjectUser::class)->findOneBy(['user' => $user, 'project' => $entity->getProject()]);
// If the document is in their own Private collection, a user with MEDICAL_RECORD_DOWNLOAD can download it.
if ($projectUser && $projectUser->getPrivateCollection() && $projectUser->getPrivateCollection()->containsDocumentRecursive($entity) && $this->authorizationChecker->isGranted('MEDICAL_RECORD_DOWNLOAD', $entity->getProject())) {
return VoterInterface::ACCESS_GRANTED;
}
// If the document is in the user's PrivateSandbox collection, they can download it.
if ($projectUser && $projectUser->getPrivateSandboxCollection() && $projectUser->getPrivateSandboxCollection()->containsDocumentRecursive($entity)) {
return VoterInterface::ACCESS_GRANTED;
}
}
// If we get to the end of this function, then no decisions have been
// made so we deny access
return VoterInterface::ACCESS_DENIED;
}
}