src/Security/Voter/AttributeOnlyVoter.php line 25

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Security\Voter;
  3. use InvalidArgumentException;
  4. use MedBrief\MSR\Entity\Account;
  5. use MedBrief\MSR\Entity\Expert;
  6. use MedBrief\MSR\Entity\Firm;
  7. use MedBrief\MSR\Entity\Project;
  8. use MedBrief\MSR\Entity\User;
  9. use MedBrief\MSR\Repository\ExpertUserRepository;
  10. use MedBrief\MSR\Role\ParsedRole;
  11. use MedBrief\MSR\Service\EntityHelper\UserHelper;
  12. use Override;
  13. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  14. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  15. use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
  16. use Symfony\Component\Security\Core\User\UserInterface;
  17. /**
  18. * This Voter handles the voting on checks that don't involve a specific
  19. * entity. Things like checking if a user can do some task like add a new
  20. * Account, or Project etc
  21. */
  22. class AttributeOnlyVoter implements VoterInterface
  23. {
  24. // these should be expanded over time as we need them
  25. public const CREATE_ACCOUNT = 'CREATE_ACCOUNT';
  26. public const CREATE_PROJECT = 'CREATE_PROJECT';
  27. public const CREATE_SERVICE_OPTION = 'CREATE_SERVICE_OPTION';
  28. public const CREATE_BILLING_ITEM = 'CREATE_BILLING_ITEM';
  29. public const CREATE_INVOICE = 'CREATE_INVOICE';
  30. public const CREATE_SORTING_MEMO_PHRASE = 'CREATE_SORTING_MEMO_PHRASE';
  31. public const CREATE_MATTER_REQUEST = 'CREATE_MATTER_REQUEST';
  32. public const CREATE_RECORDS_REQUEST_CENTRE_PARENT = 'CREATE_RECORDS_REQUEST_CENTRE_PARENT';
  33. public const CREATE_RECORDS_REQUEST_CENTRE_CHILD = 'CREATE_RECORDS_REQUEST_CENTRE_CHILD';
  34. public const CREATE_RECORDS_REQUEST_CENTRE_CONTACT = 'CREATE_RECORDS_REQUEST_CENTRE_CONTACT';
  35. public const CREATE_RECORDS_REQUEST_LETTER_TEMPLATE = 'CREATE_RECORDS_REQUEST_LETTER_TEMPLATE';
  36. public const CREATE_FIRM = 'CREATE_FIRM';
  37. // Manage records request centre contact
  38. public const MANAGE_RECORDS_REQUEST_CENTRE_CONTACT = 'MANAGE_RECORDS_REQUEST_CENTRE_CONTACT';
  39. public const LIST_ACCOUNT = 'LIST_ACCOUNT';
  40. public const LIST_PROJECT = 'LIST_PROJECT';
  41. public const LIST_PROJECT_CLOSED = 'LIST_PROJECT_CLOSED';
  42. public const LIST_USER = 'LIST_USER';
  43. public const LIST_SERVICE_OPTION = 'LIST_SERVICE_OPTION';
  44. public const LIST_BILLING_ITEM = 'LIST_BILLING_ITEM';
  45. public const LIST_PENDING_INVOICE = 'LIST_PENDING_INVOICE';
  46. public const LIST_INVOICE = 'LIST_INVOICE';
  47. public const LIST_SORTING_MEMO_PHRASE = 'LIST_SORTING_MEMO_PHRASE';
  48. public const LIST_MATTER_REQUEST = 'LIST_MATTER_REQUEST';
  49. public const LIST_RECORDS_REQUEST_CENTRE_PARENT = 'LIST_RECORDS_REQUEST_CENTRE_PARENT';
  50. public const LIST_RECORDS_REQUEST_CENTRE_CHILD = 'LIST_RECORDS_REQUEST_CENTRE_CHILD';
  51. public const LIST_RECORDS_REQUEST_CENTRE_ARCHIVED = 'LIST_RECORDS_REQUEST_CENTRE_ARCHIVED';
  52. public const LIST_RECORDS_REQUEST_LETTER_TEMPLATE = 'LIST_RECORDS_REQUEST_LETTER_TEMPLATE';
  53. public const LIST_FIRM = 'LIST_FIRM';
  54. public const LIST_PROJECT_TYPE = 'LIST_PROJECT_TYPE';
  55. public const LIST_PROJECT_RENEWALS = 'LIST_PROJECT_RENEWALS';
  56. public const USER_ADMINISTRATION = 'USER_ADMINISTRATION'; // indicates full user administration rights (CRUD)
  57. public const HUMAN_RESOURCE_ADMINISTRATION = 'HUMAN_RESOURCE_ADMINISTRATION'; // indicates full human resource administration rights (CRUD)
  58. public const USER_HELP_ADMINISTRATION = 'USER_HELP_ADMINISTRATION'; // indicates user Help Guide administration rights (CRUD)
  59. public const PRIORITISE_DISCS = 'PRIORITISE_DISCS';
  60. // Allow specific Users to mark stuffs as billed
  61. public const MARK_AS_BILLED = 'MARK_AS_BILLED';
  62. // Grants access to the 'Advanced' fields of all ServiceableInterface entities.
  63. public const SERVICE_REQUEST_ADVANCED = 'SERVICE_REQUEST_ADVANCED';
  64. public const ALLOW_USER_TYPE_CHANGE = 'ALLOW_USER_TYPE_CHANGE';
  65. // Migrated from ReportsBundle
  66. public const CREATE_REPORT = 'CREATE_REPORT';
  67. public const LIST_REPORT = 'LIST_REPORT';
  68. public const DOWNLOAD_REPORT = 'DOWNLOAD_REPORT';
  69. // Download Lists
  70. public const DOWNLOAD_PROJECT_LIST = 'DOWNLOAD_PROJECT_LIST';
  71. // User profile access
  72. public const VIEW_MY_PROFILE = 'VIEW_MY_PROFILE';
  73. public function __construct(
  74. private readonly AuthorizationCheckerInterface $authorizationChecker,
  75. private readonly UserHelper $userHelper,
  76. private readonly ExpertUserRepository $expertUserRepository,
  77. ) {
  78. }
  79. public function supportsAttribute($attribute): bool
  80. {
  81. return in_array($attribute, [
  82. self::CREATE_ACCOUNT,
  83. self::CREATE_PROJECT,
  84. self::LIST_ACCOUNT,
  85. self::LIST_PROJECT,
  86. self::LIST_USER,
  87. self::USER_ADMINISTRATION,
  88. self::USER_HELP_ADMINISTRATION,
  89. self::LIST_PROJECT_CLOSED,
  90. self::PRIORITISE_DISCS,
  91. self::LIST_SERVICE_OPTION,
  92. self::CREATE_SERVICE_OPTION,
  93. self::LIST_BILLING_ITEM,
  94. self::CREATE_BILLING_ITEM,
  95. self::CREATE_INVOICE,
  96. self::LIST_PENDING_INVOICE,
  97. self::LIST_INVOICE,
  98. self::HUMAN_RESOURCE_ADMINISTRATION,
  99. self::SERVICE_REQUEST_ADVANCED,
  100. self::MARK_AS_BILLED,
  101. self::CREATE_SORTING_MEMO_PHRASE,
  102. self::LIST_SORTING_MEMO_PHRASE,
  103. self::LIST_MATTER_REQUEST,
  104. self::CREATE_MATTER_REQUEST,
  105. self::CREATE_RECORDS_REQUEST_CENTRE_PARENT,
  106. self::LIST_RECORDS_REQUEST_CENTRE_PARENT,
  107. self::CREATE_RECORDS_REQUEST_CENTRE_CHILD,
  108. self::LIST_RECORDS_REQUEST_CENTRE_CHILD,
  109. self::CREATE_RECORDS_REQUEST_CENTRE_CONTACT,
  110. self::MANAGE_RECORDS_REQUEST_CENTRE_CONTACT,
  111. self::LIST_RECORDS_REQUEST_CENTRE_ARCHIVED,
  112. self::CREATE_RECORDS_REQUEST_LETTER_TEMPLATE,
  113. self::LIST_RECORDS_REQUEST_LETTER_TEMPLATE,
  114. self::ALLOW_USER_TYPE_CHANGE,
  115. self::CREATE_REPORT,
  116. self::LIST_REPORT,
  117. self::DOWNLOAD_REPORT,
  118. self::DOWNLOAD_PROJECT_LIST,
  119. self::LIST_FIRM,
  120. self::CREATE_FIRM,
  121. self::LIST_PROJECT_TYPE,
  122. self::LIST_PROJECT_RENEWALS,
  123. self::VIEW_MY_PROFILE,
  124. 'EA_EXECUTE_ACTION',
  125. ]);
  126. }
  127. public function supportsClass($class): bool
  128. {
  129. // the class is inconsequential for this Voter
  130. return true;
  131. }
  132. /**
  133. *
  134. * @param mixed $entity
  135. */
  136. #[Override]
  137. public function vote(TokenInterface $token, $entity, array $attributes)
  138. {
  139. /**
  140. * START: This is common code for all Voter::vote() methods
  141. */
  142. // check if class of this object is supported by this voter
  143. if (!$this->supportsClass($entity && !is_array($entity) ? $entity::class : '')) {
  144. return VoterInterface::ACCESS_ABSTAIN;
  145. }
  146. // set the attribute to check against
  147. $attribute = $attributes[0];
  148. // check if the given attribute is covered by this voter
  149. if (!$this->supportsAttribute($attribute)) {
  150. return VoterInterface::ACCESS_ABSTAIN;
  151. }
  152. // check if the voter is used correct, only allow one attribute
  153. // this isn't a requirement, it's just one easy way for you to
  154. // design your voter
  155. if (1 !== count($attributes)) {
  156. throw new InvalidArgumentException(
  157. 'Only one attribute is allowed for medbrief Voters.'
  158. );
  159. }
  160. // get current logged in user
  161. /** @var User $user */
  162. $user = $token->getUser();
  163. // make sure there is a user object (i.e. that the user is logged in)
  164. if (!$user instanceof UserInterface) {
  165. return VoterInterface::ACCESS_DENIED;
  166. }
  167. // Check if the user can view the 'My Profile' section
  168. if ($attribute === self::VIEW_MY_PROFILE) {
  169. return $this->canViewMyProfile($user);
  170. }
  171. // Universally disable this for now, as we will launch without the ability to add new ones.
  172. if ($attribute === self::CREATE_SORTING_MEMO_PHRASE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
  173. return VoterInterface::ACCESS_DENIED;
  174. }
  175. // Only ROLE_ADMIN and ROLE_SUPER_ADMIN can PRIORITISE_DISCS at this stage.
  176. if ($attribute === self::PRIORITISE_DISCS && $this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  177. return VoterInterface::ACCESS_GRANTED;
  178. }
  179. // Only ROLE_HR_ADMIN can HUMAN_RESOURCE_ADMINISTRATION at this stage.
  180. // The ROLE_HR_ADMIN role can only be added via a command.
  181. if ($attribute === self::HUMAN_RESOURCE_ADMINISTRATION && (!$this->authorizationChecker->isGranted('ROLE_HR_ADMIN') || $this->authorizationChecker->isGranted('IS_IMPERSONATOR'))) {
  182. return VoterInterface::ACCESS_DENIED;
  183. }
  184. // Only SUPER_ADMIN can CREATE_SERVICE_OPTION at this stage.
  185. if ($attribute === self::CREATE_SERVICE_OPTION && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
  186. return VoterInterface::ACCESS_DENIED;
  187. }
  188. // Only SUPER_ADMIN can CREATE_BILLING_ITEM at this stage.
  189. if ($attribute === self::CREATE_BILLING_ITEM && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
  190. return VoterInterface::ACCESS_DENIED;
  191. }
  192. // Only SUPER_ADMIN can CREATE_RECORDS_REQUEST_LETTER_TEMPLATE.
  193. if ($attribute === self::CREATE_RECORDS_REQUEST_LETTER_TEMPLATE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
  194. return VoterInterface::ACCESS_DENIED;
  195. }
  196. // Only SUPER_ADMIN can LIST_RECORDS_REQUEST_LETTER_TEMPLATE.
  197. if ($attribute === self::LIST_RECORDS_REQUEST_LETTER_TEMPLATE && !$this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
  198. return VoterInterface::ACCESS_DENIED;
  199. }
  200. // Only Super Admins and those with isBillingAdmin can mark ServiceRequests as billed.
  201. if ($attribute === self::MARK_AS_BILLED) {
  202. if ($this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN') || $user->isBillingAdmin()) {
  203. return VoterInterface::ACCESS_GRANTED;
  204. }
  205. return VoterInterface::ACCESS_DENIED;
  206. }
  207. // Only Help Guide Admins can create, edit or delete.
  208. if ($attribute === self::USER_HELP_ADMINISTRATION) {
  209. if ($this->authorizationChecker->isGranted('ROLE_HELP_ADMIN')) {
  210. return VoterInterface::ACCESS_GRANTED;
  211. }
  212. return VoterInterface::ACCESS_DENIED;
  213. }
  214. // Super Admin users can do everything
  215. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  216. return VoterInterface::ACCESS_GRANTED;
  217. }
  218. /**
  219. * END: Common code for all Voter:vote() methods. Put custom logic below.
  220. */
  221. $this->userHelper->setUser($user);
  222. // if we are looking if the user may create a matter ('Classic' matter creation process)
  223. // if the user is a client administrator for at least one account
  224. if ($attribute == self::CREATE_PROJECT && ($user->isAccountAdministrator() || $user->isAccountProjectManager())) {
  225. // Grab all accounts that the user has a role on
  226. $accountsWithRole = $this->userHelper->getAccountsWithAnyRole();
  227. foreach ($accountsWithRole as $account) {
  228. if ($account->hasAccessToClassicMatterCreation()) {
  229. // Look for at least one account that has access to the 'Classic' matter creation form
  230. return VoterInterface::ACCESS_GRANTED;
  231. }
  232. }
  233. }
  234. // If the User is a Super Admin, Admin, Client SuperAdmin, Client Admin, Client PM, Matter PM
  235. if ($attribute == self::LIST_PROJECT_RENEWALS && ($user->isAccountAdministrator() || $user->isAccountProjectManager() || $user->isProjectManager())) {
  236. return VoterInterface::ACCESS_GRANTED;
  237. }
  238. // if we are looking if the user may create a matter ('Standard' matter creation process)
  239. if ($attribute === self::CREATE_MATTER_REQUEST || $attribute === self::LIST_MATTER_REQUEST) {
  240. if ($user instanceof Firm) {
  241. // If a Firm is authenticated, they by default have access to create/list matter requests
  242. return VoterInterface::ACCESS_GRANTED;
  243. }
  244. // if the user is a client administrator for at least one account
  245. if ($user->isAccountAdministrator() || $user->isAccountProjectManager() || $user->isAccountTechnicalAdmin()) {
  246. // Grab all accounts that the user has a role on
  247. $accountsWithRole = $this->userHelper->getAccountsWithAnyRole();
  248. foreach ($accountsWithRole as $account) {
  249. if ($account->hasAccessToStandardMatterCreation()) {
  250. // Look for at least one account that has access to the 'Standard' matter creation form
  251. return VoterInterface::ACCESS_GRANTED;
  252. }
  253. }
  254. }
  255. }
  256. // if we are looking if the user may view a list of accounts
  257. if ($attribute == self::LIST_ACCOUNT) {
  258. // if the user is a client administrator for at least one account,
  259. // then they may se a list of Accounts
  260. if ($user->isAccountAdministrator()) {
  261. return VoterInterface::ACCESS_GRANTED;
  262. }
  263. if ($user->isAccountTechnicalAdmin()) {
  264. return VoterInterface::ACCESS_GRANTED;
  265. }
  266. }
  267. //if we are looking if the user may view the 'Type' search filter in the matter dashboard, matter list, closed matters list
  268. if ($attribute == self::LIST_PROJECT_TYPE) {
  269. if ($user->isAccountAdministrator() && !$user->isAccountTechnicalAdmin()) {
  270. return VoterInterface::ACCESS_GRANTED;
  271. }
  272. //if the user has an expert role but not an expert viewer role
  273. if ($user->isExpertOnly() && !$user->isExpertViewerOnly()) {
  274. return VoterInterface::ACCESS_GRANTED;
  275. }
  276. return VoterInterface::ACCESS_DENIED;
  277. }
  278. // if we are looking if the user may view a list of projects
  279. if ($attribute == self::LIST_PROJECT) {
  280. // if the user is an expert agency administrator let them pass.
  281. // this should probably be done in a better way, tying permissions
  282. // to an entity in one place somehow....
  283. if ($user->isExpertAgencyAdministrator()) {
  284. return VoterInterface::ACCESS_GRANTED;
  285. }
  286. $accessibleAccounts = $this->userHelper->getAccountIdsWithAnyRole();
  287. $accessibleProjects = $this->userHelper->getProjectIdsWithAnyRole();
  288. // if the user has been assigned at least one Account or Project
  289. // role, then they have access
  290. if ($accessibleAccounts !== [] || $accessibleProjects !== []) {
  291. return VoterInterface::ACCESS_GRANTED;
  292. }
  293. }
  294. // Check if the User can download a list of projects for all the Accounts they have access to.
  295. if ($attribute === self::DOWNLOAD_PROJECT_LIST) {
  296. return $this->canDownloadProjectList($user);
  297. }
  298. if ($attribute === self::LIST_FIRM) {
  299. return $this->canListFirm($user);
  300. }
  301. // If we get to the end of this function, then no decisions have been
  302. // made so we deny access
  303. return VoterInterface::ACCESS_DENIED;
  304. }
  305. /**
  306. * Checks if a user can download a project list.
  307. *
  308. * @param User $user
  309. */
  310. protected function canDownloadProjectList(User $user): int
  311. {
  312. if ($user) {
  313. switch (true) {
  314. case $user->isAccountProjectManager():
  315. case $user->isAccountAdministrator():
  316. return self::ACCESS_GRANTED;
  317. }
  318. }
  319. return self::ACCESS_DENIED;
  320. }
  321. private function canListFirm(User $user): int
  322. {
  323. if ($user && $user->isAccountTechnicalAdmin()) {
  324. return self::ACCESS_GRANTED;
  325. }
  326. return self::ACCESS_DENIED;
  327. }
  328. /**
  329. * Determines if a user can view the 'My Profile' section of their account section.
  330. *
  331. * The tab is only activated for users invited to matters as:
  332. * - Expert
  333. * - Expert - View only
  334. *
  335. * The tab must NOT display for users that only have expert roles for disclosure matters.
  336. *
  337. * @param User $user
  338. *
  339. * @return int
  340. */
  341. private function canViewMyProfile(User $user): int
  342. {
  343. // Load the user into the UserHelper
  344. $this->userHelper->setUser($user);
  345. $parsedRoles = $this->userHelper->getParsedRoles();
  346. /** @var ParsedRole $parsedRole */
  347. foreach ($parsedRoles as $parsedRole) {
  348. // If subject isn't a Project, ignore
  349. if (!($parsedRole->getSubject() instanceof Project)) {
  350. continue;
  351. }
  352. // If it's not an Expert Role, ignore
  353. if ($parsedRole->isRoleLevelExpert() === false) {
  354. continue;
  355. }
  356. // Check if the project is NOT a disclosure matter
  357. // In which case, we grant access
  358. if ($parsedRole->isDisclosureSubject() === false) {
  359. return self::ACCESS_GRANTED;
  360. }
  361. }
  362. // Also check role invitations
  363. /** @var ParsedRole $parsedRole */
  364. foreach ($this->userHelper->getRoleInvitationsParsedRoles() as $parsedRole) {
  365. if (!($parsedRole->getSubject() instanceof Project)) {
  366. continue;
  367. }
  368. // If it's not an Expert Role, ignore
  369. if ($parsedRole->isRoleLevelExpert() === false) {
  370. continue;
  371. }
  372. // Check if the project is NOT a disclosure matter
  373. // In which case, we grant access
  374. if ($parsedRole->isDisclosureSubject() === false) {
  375. return self::ACCESS_GRANTED;
  376. }
  377. }
  378. // Grant access if they have an expert profile
  379. if ($this->expertUserRepository->getExpertByUser($user) instanceof Expert) {
  380. return self::ACCESS_GRANTED;
  381. }
  382. return self::ACCESS_DENIED;
  383. }
  384. }