src/Security/Voter/AttributeOnlyVoter.php line 23

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