<?php
namespace MedBrief\MSR\Security\Voter;
use InvalidArgumentException;
use MedBrief\MSR\Entity\Account;
use MedBrief\MSR\Entity\Firm;
use MedBrief\MSR\Entity\InterpartyDisclosure;
use MedBrief\MSR\Entity\Project;
use MedBrief\MSR\Entity\User;
use MedBrief\MSR\Service\EntityHelper\UserHelper;
use MedBrief\MSR\Service\Role\RoleParserService;
use MedBrief\MSR\Traits\Security\Authorization\Voter\ClientSortingSessionTrait;
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 ProjectVoter implements VoterInterface
{
use ClientSortingSessionTrait;
// CONSTANTS
public const CREATE = 'CREATE';
public const READ = 'READ';
public const UPDATE = 'UPDATE';
public const DELETE = 'DELETE';
public const ADMINISTRATION = 'ADMINISTRATION';
public const MEDICAL_RECORDS_ADMINISTRATION = 'MEDICAL_RECORDS_ADMINISTRATION';
public const RADIOLOGY_ADMINISTRATION = 'RADIOLOGY_ADMINISTRATION';
public const USER_ADMINISTRATION = 'USER_ADMINISTRATION';
public const CLINICAL_SUMMARY_PROJECT_ADMINISTRATION = 'CLINICAL_SUMMARY_PROJECT_ADMINISTRATION';
public const PROJECT_USER_LIST = 'PROJECT_USER_LIST';
public const VIEW_DASHBOARD = 'VIEW_DASHBOARD';
public const TOGGLE_STATUS = 'TOGGLE_STATUS';
public const ARCHIVE = 'ARCHIVE';
public const CANCEL_DELETE = 'CANCEL_DELETE';
public const RADIOLOGY_DOWNLOAD = 'RADIOLOGY_DOWNLOAD';
public const RADIOLOGY_DOWNLOAD_AUDIT_REPORT = 'RADIOLOGY_DOWNLOAD_AUDIT_REPORT';
public const MEDICAL_RECORD_DOWNLOAD = 'MEDICAL_RECORD_DOWNLOAD';
public const DELETION_REPORT_DOWNLOAD = 'DELETION_REPORT_DOWNLOAD';
public const INTERNAL_USER_ACCESS_REPORT_DOWNLOAD = 'INTERNAL_USER_ACCESS_REPORT_DOWNLOAD';
public const CHRONOLOGY_ADMINISTRATION = 'CHRONOLOGY_ADMINISTRATION';
// This permission determines if a user sees an inactive notice message
// when an inactive Project is accessed.
public const BYPASS_INACTIVE_NOTICE = 'BYPASS_INACTIVE_NOTICE';
// This permission determines if a user can view a closed project
public const BYPASS_CLOSED_NOTICE = 'BYPASS_CLOSED_NOTICE';
// Keep these permissions on a Project, as creating any of these
// directly affects a Project.
public const CREATE_BATCH_REQUEST = 'CREATE_BATCH_REQUEST';
public const CREATE_BATCH_REQUEST_SIMPLE = 'CREATE_BATCH_REQUEST_SIMPLE';
public const CREATE_CHRONOLOGY_REQUEST = 'CREATE_CHRONOLOGY_REQUEST';
public const CREATE_ADDITIONAL_REQUEST = 'CREATE_ADDITIONAL_REQUEST';
// Sorting Session
public const SORTING_SESSION_LIST = 'SORTING_SESSION_LIST';
public const CREATE_SORTING_SESSION = 'CREATE_SORTING_SESSION';
public const CREATE_SORTING_SESSION_SIMPLE = 'CREATE_SORTING_SESSION_SIMPLE';
// Creating a matter note
public const CREATE_MATTER_NOTE = 'CREATE_MATTER_NOTE';
// Allow list of matter notes
public const LIST_MATTER_NOTES = 'LIST_MATTER_NOTES';
// Allow view of matter communications
public const VIEW_MATTER_COMMUNICATIONS = 'VIEW_MATTER_COMMUNICATIONS';
// Disclosure permissions
public const MEDICAL_RECORDS_DISCLOSE = 'MEDICAL_RECORDS_DISCLOSE';
public const DISCLOSE_DISC = 'DISCLOSE_DISC';
public const BYPASS_AUTHENTICATION = 'BYPASS_AUTHENTICATION';
// Allows changing the account value of a Matter
public const CHANGE_ACCOUNT = 'CHANGE_ACCOUNT';
// Allows user to create a record request letter
public const MANAGE_REQUEST_LETTERS = 'MANAGE_REQUEST_LETTERS';
// Allows a user to see the 'Unsorted' records for a Project
public const VIEW_UNSORTED_RECORDS = 'VIEW_UNSORTED_RECORDS';
// Project Closure Permissions
public const CREATE_PROJECT_CLOSURE = 'CREATE_PROJECT_CLOSURE';
public const DOWNLOAD_ALL_PROJECT_FILES = 'DOWNLOAD_ALL_PROJECT_FILES';
public const DOWNLOAD_PROJECT_CLOSURE_REPORT = 'DOWNLOAD_PROJECT_CLOSURE_REPORT';
// Allows the user to see a modal showing important notes on the matter, if any.
public const VIEW_IMPORTANT_NOTES = 'VIEW_IMPORTANT_NOTES';
// Allows the user to see the service request requirement banners on the matter dashboard.
public const VIEW_SERVICE_REQUEST_REQUIREMENT_BANNERS = 'VIEW_SERVICE_REQUEST_REQUIREMENT_BANNERS';
// Inter-party Disclosure
public const CREATE_INTERPARTY_DISCLOSURE = 'CREATE_INTERPARTY_DISCLOSURE';
public const LIST_INTERPARTY_DISCLOSURE = 'LIST_INTERPARTY_DISCLOSURE';
// Allows by passing the disabled state of service requests when a matter/project is closed or in the process of being closed.
public const BYPASS_SERVICE_REQUEST_DISABLED = 'BYPASS_SERVICE_REQUEST_DISABLED';
// Allows the user to download the 'user download medical records viewed report'
public const VIEW_THIRD_PARTY_ACCESS_REPORT = 'VIEW_THIRD_PARTY_ACCESS_REPORT';
public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private UserHelper $userHelper)
{
}
/**
* Whether or not this User is allowed to perform specific actions on this Entity
*/
public function supportsAttribute(mixed $attribute): bool
{
return in_array($attribute, [
self::CREATE,
self::READ,
self::UPDATE,
self::DELETE,
self::ADMINISTRATION,
self::MEDICAL_RECORDS_ADMINISTRATION,
self::RADIOLOGY_ADMINISTRATION,
self::USER_ADMINISTRATION,
self::VIEW_DASHBOARD,
self::TOGGLE_STATUS,
self::PROJECT_USER_LIST,
self::RADIOLOGY_DOWNLOAD,
self::RADIOLOGY_DOWNLOAD_AUDIT_REPORT,
self::MEDICAL_RECORD_DOWNLOAD,
self::DELETION_REPORT_DOWNLOAD,
self::INTERNAL_USER_ACCESS_REPORT_DOWNLOAD,
self::CHRONOLOGY_ADMINISTRATION,
self::ARCHIVE,
self::CANCEL_DELETE,
self::BYPASS_INACTIVE_NOTICE,
self::BYPASS_CLOSED_NOTICE,
self::CREATE_BATCH_REQUEST,
self::CREATE_BATCH_REQUEST_SIMPLE,
self::CREATE_CHRONOLOGY_REQUEST,
self::CREATE_ADDITIONAL_REQUEST,
self::SORTING_SESSION_LIST,
self::CREATE_SORTING_SESSION,
self::CREATE_SORTING_SESSION_SIMPLE,
self::CREATE_MATTER_NOTE,
self::LIST_MATTER_NOTES,
self::VIEW_MATTER_COMMUNICATIONS,
self::MEDICAL_RECORDS_DISCLOSE,
self::DISCLOSE_DISC,
self::BYPASS_AUTHENTICATION,
self::CHANGE_ACCOUNT,
self::MANAGE_REQUEST_LETTERS,
self::VIEW_UNSORTED_RECORDS,
self::CREATE_PROJECT_CLOSURE,
self::DOWNLOAD_ALL_PROJECT_FILES,
self::DOWNLOAD_PROJECT_CLOSURE_REPORT,
self::VIEW_IMPORTANT_NOTES,
self::VIEW_SERVICE_REQUEST_REQUIREMENT_BANNERS,
self::CREATE_INTERPARTY_DISCLOSURE,
self::LIST_INTERPARTY_DISCLOSURE,
self::BYPASS_SERVICE_REQUEST_DISABLED,
self::VIEW_THIRD_PARTY_ACCESS_REPORT,
self::CLINICAL_SUMMARY_PROJECT_ADMINISTRATION,
]);
}
/**
* Whether or not this is a supported Class
*
* @param string $class
*/
public function supportsClass($class): bool
{
$supportedClass = Project::class;
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* @param Project $entity
*
* @return int
*/
#[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
/** @var User $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;
}
// Only allow Super Admins and Admins to change accounts
if ($attribute === self::CHANGE_ACCOUNT && !$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only allow Super Admins to delete a project
if ($attribute === self::DELETE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only super admins can update service requests when the project is in a closed or closing state
if ($attribute === self::BYPASS_SERVICE_REQUEST_DISABLED) {
if ($this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN') === true) {
return VoterInterface::ACCESS_GRANTED;
}
return VoterInterface::ACCESS_DENIED;
}
/**
* Clinical Summary Access Control with project entity passed in as the subject
*
* We need to put this before we grant admin users rights to everything otherwise the
* isCloseInProgressOrComplete check has no effect
*/
if ($attribute === self::CLINICAL_SUMMARY_PROJECT_ADMINISTRATION) {
if ($this->canAccessClinicalSummaryWizard($entity)) {
return VoterInterface::ACCESS_GRANTED;
}
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.
*/
/**
* API (Firm) Access Control
*/
if ($user instanceof Firm) {
// If the account that belongs to the project is allocated to the firm's client areas, allow everything.
if ($entity->getAccount() instanceof Account && $user->getClientAreas()->contains($entity->getAccount()) === true) {
return self::ACCESS_GRANTED;
}
return self::ACCESS_DENIED;
}
/**
* Disclosure matter access control
*/
// Grab all project levels roles related to this project.
$allProjectRoles = RoleParserService::getAllRolesForProject($entity->getId());
// Users that have been directly invited to the Disclosure will have permission granted
$isDirectlyInvitedToDisclosureMatter = array_filter($allProjectRoles, fn ($role) => $this->authorizationChecker->isGranted($role)) !== [];
// Check if the project is a disclosure, and exclude anyone who has been directly invited to the disclosure matter (i.e. those that were
// added when creating the disclosure). Project level project managers of the original project will not have a project role on the disclosure target project.
if ($entity->isTypeDisclosure() && $isDirectlyInvitedToDisclosureMatter === false) {
$allowedAttributes = [
self::READ,
self::RADIOLOGY_DOWNLOAD,
self::MEDICAL_RECORD_DOWNLOAD,
self::BYPASS_INACTIVE_NOTICE,
self::VIEW_THIRD_PARTY_ACCESS_REPORT,
];
// Disclosure matters only allow certain actions for the those who can VIEW the source disclosure entity
if (in_array($attribute, $allowedAttributes) && $entity->getDisclosureSources()->count() > 0) {
// Grant access if the user has access to VIEW the original source disclosure (we take the latest one in the chain of sources)
/** @var InterpartyDisclosure $disclosure */
$disclosure = $entity->getDisclosureSources()->last();
if ($this->authorizationChecker->isGranted(InterpartyDisclosureVoter::VIEW, $disclosure)) {
return self::ACCESS_GRANTED;
}
}
return self::ACCESS_DENIED;
}
//Checks if user can download radiology audit report
if ($attribute === self::RADIOLOGY_DOWNLOAD_AUDIT_REPORT) {
return $this->canRadiologyDownloadAuditReport($entity);
}
$this->userHelper->setUser($user);
$denyAccess = [
self::CREATE_SORTING_SESSION,
self::LIST_MATTER_NOTES,
self::CREATE_MATTER_NOTE,
self::MEDICAL_RECORDS_DISCLOSE,
self::DISCLOSE_DISC,
self::VIEW_MATTER_COMMUNICATIONS,
self::DELETION_REPORT_DOWNLOAD,
self::INTERNAL_USER_ACCESS_REPORT_DOWNLOAD,
self::MANAGE_REQUEST_LETTERS,
self::DELETE,
self::VIEW_IMPORTANT_NOTES,
self::VIEW_SERVICE_REQUEST_REQUIREMENT_BANNERS,
];
// Deny all other roles these permissions
if (in_array($attribute, $denyAccess)) {
return VoterInterface::ACCESS_DENIED;
}
// Permissions related to the creation and management of sorting sessions and batches.
$attributeGroup = [
self::CREATE_BATCH_REQUEST_SIMPLE,
self::SORTING_SESSION_LIST,
self::CREATE_SORTING_SESSION_SIMPLE,
];
if (in_array($attribute, $attributeGroup)) {
if ($this->hasClientSessionAccess($entity, $user)) {
return VoterInterface::ACCESS_GRANTED;
};
return VoterInterface::ACCESS_DENIED;
}
// Deny all other roles from creating these ServiceRequests
$serviceRequestCreateAttributes = [
self::CREATE_BATCH_REQUEST,
self::CREATE_CHRONOLOGY_REQUEST,
self::CREATE_ADDITIONAL_REQUEST,
];
if (in_array($attribute, $serviceRequestCreateAttributes)) {
return VoterInterface::ACCESS_DENIED;
}
if ($attribute === self::VIEW_DASHBOARD && $user->getMatterDashboardEnabled()) {
// then they have access to do anything
return VoterInterface::ACCESS_GRANTED;
}
// if this user is a Super Administrator for the Account for which this Project belongs
if ($this->authorizationChecker->isGranted('ROLE_ACCOUNT_' . $entity->getAccount()->getId() . '_SUPERADMINISTRATOR')) {
// then they have access to do anything, except delete.
return VoterInterface::ACCESS_GRANTED;
}
// Otherwise if this user is a Client Administrator for the Account then they can
// do everything else except for User Administration and Deletion.
if ($this->authorizationChecker->isGranted('ROLE_ACCOUNT_' . $entity->getAccount()->getId() . '_ADMINISTRATOR')) {
// Commenting this out for now because Kennedy's actually need
// regular Client Administrators to still have this access for now. - RR
//if ($attribute != self::USER_ADMINISTRATION) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
//}
}
// if this user is a Sorter for the Account for which this Project belongs
// if we are looking for access other than delete, full administration and user management abilities
if ($this->authorizationChecker->isGranted('ROLE_ACCOUNT_' . $entity->getAccount()->getId() . '_SORTER') && ($attribute != self::DELETE
&& $attribute != self::ADMINISTRATION
&& $attribute != self::PROJECT_USER_LIST
&& $attribute != self::USER_ADMINISTRATION
&& $attribute != self::CREATE_PROJECT_CLOSURE
&& $attribute != self::CANCEL_DELETE
&& $attribute != self::ARCHIVE
&& $attribute != self::DOWNLOAD_ALL_PROJECT_FILES
&& $attribute != self::DOWNLOAD_PROJECT_CLOSURE_REPORT
&& $attribute != self::CREATE_INTERPARTY_DISCLOSURE
&& $attribute != self::LIST_INTERPARTY_DISCLOSURE)) {
// then account level sorters have this access
return VoterInterface::ACCESS_GRANTED;
}
// if we are looking for any access other than delete and full administration and the Closure of matters.
if ($attribute != self::DELETE
&& $attribute != self::ADMINISTRATION
&& $attribute != self::CREATE_PROJECT_CLOSURE
&& $attribute != self::CANCEL_DELETE
&& $attribute != self::ARCHIVE
&& $attribute != self::DOWNLOAD_ALL_PROJECT_FILES
&& $attribute != self::DOWNLOAD_PROJECT_CLOSURE_REPORT
) {
// then project managers may do this
if ($this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_PROJECTMANAGER')) {
return VoterInterface::ACCESS_GRANTED;
}
// Grant project manager access to a Project's manager, which will likely be a
// ACCOUNT_PROJECT_MANAGER
if ($entity->getManager() && $entity->getManager()->getId() === $user->getId()) {
return VoterInterface::ACCESS_GRANTED;
}
}
// if we are looking for the medical records administration or radiology administration attribute
if ($attribute == self::MEDICAL_RECORDS_ADMINISTRATION || $attribute == self::RADIOLOGY_ADMINISTRATION || $attribute === self::VIEW_UNSORTED_RECORDS) {
// any of the following roles will grant access
$allowedRoles = [
'ROLE_PROJECT_' . $entity->getId() . '_SCANNER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD',
'ROLE_PROJECT_' . $entity->getId() . '_PROJECTMANAGER',
];
// so if the user has any one of these
foreach ($allowedRoles as $role) {
if ($this->authorizationChecker->isGranted($role)) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
}
}
// If the project allows experts to see the unsorted records, and the user has an export role on the project.
if ($attribute === self::VIEW_UNSORTED_RECORDS && ($entity->getAllowExpertViewUnsortedRecords() === true
&& ($this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_EXPERT') || $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_EXPERTVIEWER')))) {
return VoterInterface::ACCESS_GRANTED;
}
// Certain roles will allow you to bypass the inactive notice on an inactive Project's related controller.
if ($attribute == self::BYPASS_INACTIVE_NOTICE) {
// any of the following roles will grant access
$allowedRoles = [
'ROLE_PROJECT_' . $entity->getId() . '_SCANNER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD',
'ROLE_PROJECT_' . $entity->getId() . '_PROJECTMANAGER',
];
// so if the user has any one of these
foreach ($allowedRoles as $role) {
if ($this->authorizationChecker->isGranted($role)) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
}
}
// Certain roles will allow you to bypass the closed notice on a closed Project's related controller.
if ($attribute == self::BYPASS_CLOSED_NOTICE) {
// any of the following roles will grant access
$allowedRoles = [
'ROLE_PROJECT_' . $entity->getId() . '_SCANNER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD',
'ROLE_PROJECT_' . $entity->getId() . '_PROJECTMANAGER',
];
// so if the user has any one of these
foreach ($allowedRoles as $role) {
if ($this->authorizationChecker->isGranted($role)) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
}
}
if ($attribute == self::RADIOLOGY_DOWNLOAD || $attribute == self::MEDICAL_RECORD_DOWNLOAD) {
// Experts may do this, but EXPERTVIEWER's may not
if ($this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_EXPERT')) {
return VoterInterface::ACCESS_GRANTED;
}
// Scanner - Download Enabled may do this, but Scanner's may not
if ($attribute == self::MEDICAL_RECORD_DOWNLOAD && $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD')) {
return VoterInterface::ACCESS_GRANTED;
}
// Certain roles may have access to Radiology
if ($attribute == self::RADIOLOGY_DOWNLOAD) {
// Array of permitted/allowed roles
$allowedRoles = [
'ROLE_PROJECT_' . $entity->getId() . '_SCANNER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD',
];
// Iterate through them and...
foreach ($allowedRoles as $role) {
// ... if they have the role...
if ($this->authorizationChecker->isGranted($role)) {
// SHAZAM!
return VoterInterface::ACCESS_GRANTED;
}
}
}
}
// if the user is wanting to read information about this Project
if ($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;
}
// if the user has any role related to this project
if ($this->userHelper->hasProjectRole($entity)) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
}
// if we are looking for updating a project
// if the user is a project manager
if ($attribute == self::UPDATE && $this->authorizationChecker->isGranted('ROLE_PROJECT_' . $entity->getId() . '_PROJECTMANAGER')) {
// then they have access
return VoterInterface::ACCESS_GRANTED;
}
// if we are checking to see if we can bypass authentication
if ($attribute == self::BYPASS_AUTHENTICATION && $this->authorizationChecker->isGranted('USER_ADMINISTRATION', $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;
}
/**
* Checks whether user has administrative rights for a clinical summary
*
*
*
* @param Project $entity
*
* @return int
*/
private function canAccessClinicalSummaryWizard(Project $entity)
{
// Deny access if the project is in the process of being closed or is closed
if ($entity->isCloseInProgressOrComplete()) {
return false;
}
// Allow access if the user has the ROLE_ADMIN or ROLE_SUPER_ADMIN role
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
}
/**
* Checks whether user can download radiology audit report
*
* @todo: Not type hinting this method now as this may change later
*
* @param Project $entity
*/
private function canRadiologyDownloadAuditReport(Project $entity): int
{
$deniedRoles = [
'ROLE_PROJECT_' . $entity->getId() . '_EXPERT',
'ROLE_PROJECT_' . $entity->getId() . '_EXPERTVIEWER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNER',
'ROLE_PROJECT_' . $entity->getId() . '_SCANNERDOWNLOAD',
];
foreach ($deniedRoles as $role) {
if ($this->authorizationChecker->isGranted($role)) {
return VoterInterface::ACCESS_DENIED;
}
}
return VoterInterface::ACCESS_GRANTED;
}
}