src/Security/Voter/AttributeOnlyVoter.php line 21

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