<?php
namespace MedBrief\MSR\Security\Voter;
use InvalidArgumentException;
use MedBrief\MSR\Entity\Account;
use MedBrief\MSR\Entity\Firm;
use MedBrief\MSR\Entity\User;
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;
/**
* This Voter handles the voting on checks that don't involve a specific
* entity. Things like checking if a user can do some task like add a new
* Account, or Project etc
*/
class AttributeOnlyVoter implements VoterInterface
{
// these should be expanded over time as we need them
public const CREATE_ACCOUNT = 'CREATE_ACCOUNT';
public const CREATE_PROJECT = 'CREATE_PROJECT';
public const CREATE_SERVICE_OPTION = 'CREATE_SERVICE_OPTION';
public const CREATE_BILLING_ITEM = 'CREATE_BILLING_ITEM';
public const CREATE_INVOICE = 'CREATE_INVOICE';
public const CREATE_SORTING_MEMO_PHRASE = 'CREATE_SORTING_MEMO_PHRASE';
public const CREATE_MATTER_REQUEST = 'CREATE_MATTER_REQUEST';
public const CREATE_RECORDS_REQUEST_CENTRE_PARENT = 'CREATE_RECORDS_REQUEST_CENTRE_PARENT';
public const CREATE_RECORDS_REQUEST_CENTRE_CHILD = 'CREATE_RECORDS_REQUEST_CENTRE_CHILD';
public const CREATE_RECORDS_REQUEST_CENTRE_CONTACT = 'CREATE_RECORDS_REQUEST_CENTRE_CONTACT';
public const CREATE_RECORDS_REQUEST_LETTER_TEMPLATE = 'CREATE_RECORDS_REQUEST_LETTER_TEMPLATE';
public const CREATE_FIRM = 'CREATE_FIRM';
// Manage records request centre contact
public const MANAGE_RECORDS_REQUEST_CENTRE_CONTACT = 'MANAGE_RECORDS_REQUEST_CENTRE_CONTACT';
public const LIST_ACCOUNT = 'LIST_ACCOUNT';
public const LIST_PROJECT = 'LIST_PROJECT';
public const LIST_PROJECT_CLOSED = 'LIST_PROJECT_CLOSED';
public const LIST_USER = 'LIST_USER';
public const LIST_SERVICE_OPTION = 'LIST_SERVICE_OPTION';
public const LIST_BILLING_ITEM = 'LIST_BILLING_ITEM';
public const LIST_PENDING_INVOICE = 'LIST_PENDING_INVOICE';
public const LIST_INVOICE = 'LIST_INVOICE';
public const LIST_SORTING_MEMO_PHRASE = 'LIST_SORTING_MEMO_PHRASE';
public const LIST_MATTER_REQUEST = 'LIST_MATTER_REQUEST';
public const LIST_RECORDS_REQUEST_CENTRE_PARENT = 'LIST_RECORDS_REQUEST_CENTRE_PARENT';
public const LIST_RECORDS_REQUEST_CENTRE_CHILD = 'LIST_RECORDS_REQUEST_CENTRE_CHILD';
public const LIST_RECORDS_REQUEST_CENTRE_ARCHIVED = 'LIST_RECORDS_REQUEST_CENTRE_ARCHIVED';
public const LIST_RECORDS_REQUEST_LETTER_TEMPLATE = 'LIST_RECORDS_REQUEST_LETTER_TEMPLATE';
public const LIST_FIRM = 'LIST_FIRM';
public const LIST_PROJECT_TYPE = 'LIST_PROJECT_TYPE';
public const LIST_PROJECT_RENEWALS = 'LIST_PROJECT_RENEWALS';
public const USER_ADMINISTRATION = 'USER_ADMINISTRATION'; // indicates full user administration rights (CRUD)
public const HUMAN_RESOURCE_ADMINISTRATION = 'HUMAN_RESOURCE_ADMINISTRATION'; // indicates full human resource administration rights (CRUD)
public const USER_HELP_ADMINISTRATION = 'USER_HELP_ADMINISTRATION'; // indicates user Help Guide administration rights (CRUD)
public const PRIORITISE_DISCS = 'PRIORITISE_DISCS';
// Allow specific Users to mark stuffs as billed
public const MARK_AS_BILLED = 'MARK_AS_BILLED';
// Grants access to the 'Advanced' fields of all ServiceableInterface entities.
public const SERVICE_REQUEST_ADVANCED = 'SERVICE_REQUEST_ADVANCED';
public const ALLOW_USER_TYPE_CHANGE = 'ALLOW_USER_TYPE_CHANGE';
// Migrated from ReportsBundle
public const CREATE_REPORT = 'CREATE_REPORT';
public const LIST_REPORT = 'LIST_REPORT';
public const DOWNLOAD_REPORT = 'DOWNLOAD_REPORT';
// Download Lists
public const DOWNLOAD_PROJECT_LIST = 'DOWNLOAD_PROJECT_LIST';
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly UserHelper $userHelper)
{
}
public function supportsAttribute($attribute): bool
{
return in_array($attribute, [
self::CREATE_ACCOUNT,
self::CREATE_PROJECT,
self::LIST_ACCOUNT,
self::LIST_PROJECT,
self::LIST_USER,
self::USER_ADMINISTRATION,
self::USER_HELP_ADMINISTRATION,
self::LIST_PROJECT_CLOSED,
self::PRIORITISE_DISCS,
self::LIST_SERVICE_OPTION,
self::CREATE_SERVICE_OPTION,
self::LIST_BILLING_ITEM,
self::CREATE_BILLING_ITEM,
self::CREATE_INVOICE,
self::LIST_PENDING_INVOICE,
self::LIST_INVOICE,
self::HUMAN_RESOURCE_ADMINISTRATION,
self::SERVICE_REQUEST_ADVANCED,
self::MARK_AS_BILLED,
self::CREATE_SORTING_MEMO_PHRASE,
self::LIST_SORTING_MEMO_PHRASE,
self::LIST_MATTER_REQUEST,
self::CREATE_MATTER_REQUEST,
self::CREATE_RECORDS_REQUEST_CENTRE_PARENT,
self::LIST_RECORDS_REQUEST_CENTRE_PARENT,
self::CREATE_RECORDS_REQUEST_CENTRE_CHILD,
self::LIST_RECORDS_REQUEST_CENTRE_CHILD,
self::CREATE_RECORDS_REQUEST_CENTRE_CONTACT,
self::MANAGE_RECORDS_REQUEST_CENTRE_CONTACT,
self::LIST_RECORDS_REQUEST_CENTRE_ARCHIVED,
self::CREATE_RECORDS_REQUEST_LETTER_TEMPLATE,
self::LIST_RECORDS_REQUEST_LETTER_TEMPLATE,
self::ALLOW_USER_TYPE_CHANGE,
self::CREATE_REPORT,
self::LIST_REPORT,
self::DOWNLOAD_REPORT,
self::DOWNLOAD_PROJECT_LIST,
self::LIST_FIRM,
self::CREATE_FIRM,
self::LIST_PROJECT_TYPE,
self::LIST_PROJECT_RENEWALS,
'EA_EXECUTE_ACTION',
]);
}
public function supportsClass($class): bool
{
// the class is inconsequential for this Voter
return true;
}
/**
*
* @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;
}
// 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;
}
// 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.'
);
}
// 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;
}
// Universally disable this for now, as we will launch without the ability to add new ones.
if ($attribute === self::CREATE_SORTING_MEMO_PHRASE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only ROLE_ADMIN and ROLE_SUPER_ADMIN can PRIORITISE_DISCS at this stage.
if ($attribute === self::PRIORITISE_DISCS && $this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return VoterInterface::ACCESS_GRANTED;
}
// Only ROLE_HR_ADMIN can HUMAN_RESOURCE_ADMINISTRATION at this stage.
// The ROLE_HR_ADMIN role can only be added via a command.
if ($attribute === self::HUMAN_RESOURCE_ADMINISTRATION && (!$this->authorizationChecker->isGranted('ROLE_HR_ADMIN') || $this->authorizationChecker->isGranted('IS_IMPERSONATOR'))) {
return VoterInterface::ACCESS_DENIED;
}
// Only SUPER_ADMIN can CREATE_SERVICE_OPTION at this stage.
if ($attribute === self::CREATE_SERVICE_OPTION && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only SUPER_ADMIN can CREATE_BILLING_ITEM at this stage.
if ($attribute === self::CREATE_BILLING_ITEM && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only SUPER_ADMIN can CREATE_RECORDS_REQUEST_LETTER_TEMPLATE.
if ($attribute === self::CREATE_RECORDS_REQUEST_LETTER_TEMPLATE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only SUPER_ADMIN can LIST_RECORDS_REQUEST_LETTER_TEMPLATE.
if ($attribute === self::LIST_RECORDS_REQUEST_LETTER_TEMPLATE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return VoterInterface::ACCESS_DENIED;
}
// Only Super Admins and those with isBillingAdmin can mark ServiceRequests as billed.
if ($attribute === self::MARK_AS_BILLED) {
if ($this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN') || $user->isBillingAdmin()) {
return VoterInterface::ACCESS_GRANTED;
}
return VoterInterface::ACCESS_DENIED;
}
// Only Help Guide Admins can create, edit or delete.
if ($attribute === self::USER_HELP_ADMINISTRATION) {
if ($this->authorizationChecker->isGranted('ROLE_HELP_ADMIN')) {
return VoterInterface::ACCESS_GRANTED;
}
return VoterInterface::ACCESS_DENIED;
}
// Super 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 we are looking if the user may create a matter ('Classic' matter creation process)
// if the user is a client administrator for at least one account
if ($attribute == self::CREATE_PROJECT && ($user->isAccountAdministrator() || $user->isAccountProjectManager())) {
// Grab all accounts that the user has a role on
$accountsWithRole = $this->userHelper->getAccountsWithAnyRole();
foreach ($accountsWithRole as $account) {
if ($account->hasAccessToClassicMatterCreation()) {
// Look for at least one account that has access to the 'Classic' matter creation form
return VoterInterface::ACCESS_GRANTED;
}
}
}
// If the User is a Super Admin, Admin, Client SuperAdmin, Client Admin, Client PM, Matter PM
if ($attribute == self::LIST_PROJECT_RENEWALS && ($user->isAccountAdministrator() || $user->isAccountProjectManager() || $user->isProjectManager())) {
return VoterInterface::ACCESS_GRANTED;
}
// if we are looking if the user may create a matter ('Standard' matter creation process)
if ($attribute === self::CREATE_MATTER_REQUEST || $attribute === self::LIST_MATTER_REQUEST) {
if ($user instanceof Firm) {
// If a Firm is authenticated, they by default have access to create/list matter requests
return VoterInterface::ACCESS_GRANTED;
}
// if the user is a client administrator for at least one account
if ($user->isAccountAdministrator() || $user->isAccountProjectManager() || $user->isAccountTechnicalAdmin()) {
// Grab all accounts that the user has a role on
$accountsWithRole = $this->userHelper->getAccountsWithAnyRole();
foreach ($accountsWithRole as $account) {
if ($account->hasAccessToStandardMatterCreation()) {
// Look for at least one account that has access to the 'Standard' matter creation form
return VoterInterface::ACCESS_GRANTED;
}
}
}
}
// if we are looking if the user may view a list of accounts
if ($attribute == self::LIST_ACCOUNT) {
// if the user is a client administrator for at least one account,
// then they may se a list of Accounts
if ($user->isAccountAdministrator()) {
return VoterInterface::ACCESS_GRANTED;
}
if ($user->isAccountTechnicalAdmin()) {
return VoterInterface::ACCESS_GRANTED;
}
}
//if we are looking if the user may view the 'Type' search filter in the matter dashboard, matter list, closed matters list
if ($attribute == self::LIST_PROJECT_TYPE) {
if ($user->isAccountAdministrator() && !$user->isAccountTechnicalAdmin()) {
return VoterInterface::ACCESS_GRANTED;
}
//if the user has an expert role but not an expert viewer role
if ($user->isExpertOnly() && !$user->isExpertViewerOnly()) {
return VoterInterface::ACCESS_GRANTED;
}
return VoterInterface::ACCESS_DENIED;
}
// if we are looking if the user may view a list of projects
if ($attribute == self::LIST_PROJECT) {
// 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;
}
$accessibleAccounts = $this->userHelper->getAccountIdsWithAnyRole();
$accessibleProjects = $this->userHelper->getProjectIdsWithAnyRole();
// if the user has been assigned at least one Account or Project
// role, then they have access
if ($accessibleAccounts !== [] || $accessibleProjects !== []) {
return VoterInterface::ACCESS_GRANTED;
}
}
// Check if the User can download a list of projects for all the Accounts they have access to.
if ($attribute === self::DOWNLOAD_PROJECT_LIST) {
return $this->canDownloadProjectList($user);
}
if ($attribute === self::LIST_FIRM) {
return $this->canListFirm($user);
}
// If we get to the end of this function, then no decisions have been
// made so we deny access
return VoterInterface::ACCESS_DENIED;
}
/**
* Checks if a user can download a project list.
*
* @param User $user
*/
protected function canDownloadProjectList(User $user): int
{
if ($user) {
switch (true) {
case $user->isAccountProjectManager():
case $user->isAccountAdministrator():
return self::ACCESS_GRANTED;
}
}
return self::ACCESS_DENIED;
}
private function canListFirm(User $user): int
{
if ($user && $user->isAccountTechnicalAdmin()) {
return self::ACCESS_GRANTED;
}
return self::ACCESS_DENIED;
}
}