src/Twig/Runtime/AclRuntime.php line 101

Open in your IDE?
  1. <?php
  2. /** @noinspection ALL */
  3. namespace App\Twig\Runtime;
  4. use App\Constants\ACL;
  5. use App\Entity\Parameter;
  6. use App\Entity\SliderItem;
  7. use App\Entity\User;
  8. use App\Model\Product;
  9. use App\Services\Back\ParameterService;
  10. use App\Services\Common\AclServiceV2;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use Psr\Cache\InvalidArgumentException;
  13. use Symfony\Component\HttpFoundation\RequestStack;
  14. use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
  15. use Symfony\Component\Security\Core\Security;
  16. use Symfony\Component\Security\Core\User\UserInterface;
  17. use Twig\Extension\RuntimeExtensionInterface;
  18. use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
  19. /**
  20. * Rassemble toute les fonction disponible dans twig qui touchent aux ACL et à l'affichage des données via les règles acl ou les systèmes paralèlles (roles, jobs sur les entités comme pour les slider ou les documents)
  21. */
  22. class AclRuntime implements RuntimeExtensionInterface
  23. {
  24. private AclServiceV2 $aclService;
  25. private RequestStack $requestStack;
  26. private ParameterService $parameterService;
  27. private Security $security;
  28. private RoleHierarchyInterface $roleHierarchy;
  29. private EntityManagerInterface $em;
  30. private array $userIsGrantedCache = [];
  31. private array $componentVisibilityCache = [];
  32. private array $displayConfigCache = [];
  33. private ?string $currentRouteCache = null;
  34. private mixed $currentRouteParamsCache = null;
  35. private bool $currentRequestResolved = false;
  36. private User|UserInterface|null $currentUserCache = null;
  37. private bool $currentUserResolved = false;
  38. public function __construct(AclServiceV2 $aclService, RequestStack $requestStack, ParameterService $parameterService, Security $security, RoleHierarchyInterface $roleHierarchy, EntityManagerInterface $em)
  39. {
  40. $this->aclService = $aclService;
  41. $this->requestStack = $requestStack;
  42. $this->parameterService = $parameterService;
  43. $this->security = $security;
  44. $this->roleHierarchy = $roleHierarchy;
  45. $this->em = $em;
  46. }
  47. /**
  48. * Permet de savoir si le user a le droit d'accéder à la route
  49. * @param User|null $user
  50. * @param string $route
  51. * @param array $params
  52. * @param string $env
  53. * @return bool
  54. */
  55. public function userIsGrantedRoute(?User $user, string $route, array $params = [], string $env = ACL::FRONT_ENV, bool $debug = FALSE)
  56. {
  57. $config = [
  58. 'route' => $route,
  59. 'params' => $params,
  60. 'component' => ACL::ACL_NO_COMPONENT,
  61. 'slug' => ACL::ACL_NO_SLUG,
  62. 'action' => ACL::READ,
  63. 'env' => $env,
  64. ];
  65. return $this->aclService->userIsGranted($user, $config, $debug);
  66. }
  67. /**
  68. * Permet de savoir si un user peut faire une action en fonction de son rôle ou de son job
  69. *
  70. * @param User|null $user
  71. * @param string $slug
  72. * @param string $action
  73. * @param string $env
  74. *
  75. * @return bool
  76. *
  77. * @throws InvalidArgumentException
  78. */
  79. public function userIsGranted(?User $user, string $component, string $slug = ACL::ACL_NO_SLUG, string $action = ACL::READ, string $env = ACL::FRONT_ENV, string $route = NULL, string $params = NULL, bool $debug = FALSE): bool
  80. {
  81. [$resolvedRoute, $resolvedParams] = $this->getCurrentRouteContext();
  82. $currentRoute = $route ?? $resolvedRoute;
  83. $currentParams = $params ?? $resolvedParams;
  84. if($currentRoute === NULL)
  85. {
  86. return $this->checkWhenRouteIsNull();
  87. }
  88. $config = [
  89. 'route' => $currentRoute,
  90. 'params' => $this->aclService->getRouteParamsForAcl($currentRoute, $currentParams),
  91. 'component' => $component,
  92. 'slug' => $slug,
  93. 'action' => $action,
  94. 'env' => $env,
  95. ];
  96. $cacheKey = implode('|', [
  97. $user instanceof User ? $user->getId() : 'anonymous',
  98. $currentRoute,
  99. $config['params'],
  100. $component,
  101. $slug,
  102. $action,
  103. $env,
  104. ]);
  105. if(array_key_exists($cacheKey, $this->userIsGrantedCache))
  106. {
  107. return $this->userIsGrantedCache[$cacheKey];
  108. }
  109. return $this->userIsGrantedCache[$cacheKey] = $this->aclService->userIsGranted($user, $config, $debug);
  110. }
  111. /**
  112. * Logique pour éviter des erreurs 500 si jamais une route est évaluée à NULL
  113. * Si on est dans le cas d'une exception on autorise la visualition
  114. * @return true
  115. * @throws \Exception
  116. */
  117. private function checkWhenRouteIsNull()
  118. {
  119. $request = $this->requestStack->getCurrentRequest();
  120. $exeption = $request->attributes->get('exception');
  121. if($exeption !== null)
  122. {
  123. return TRUE;
  124. }
  125. throw new \Exception('Une erreur est survenue, la route ne peut pas être null et ne pas être une exception');
  126. }
  127. /**
  128. * Permet de savoir si le current user à le droit de voir le catalogue par son slug
  129. * @param $catalogue
  130. * @return bool
  131. * @throws InvalidArgumentException
  132. */
  133. public function userIsGrantedCatalogue($catalogue)
  134. {
  135. $currentUser = $this->security->getUser();
  136. return $this->aclService->userIsGrantedCatalogue($currentUser, $catalogue);
  137. }
  138. /**
  139. * Permet de savoir si le current user à le droit de voir le produit
  140. * @param Product $product
  141. *
  142. * @return bool
  143. * @throws \JsonException
  144. */
  145. public function userIsGrantedProduct(Product $product)
  146. {
  147. $currentUser = $this->security->getUser();
  148. return $this->aclService->userIsGrantedProduct($currentUser, $product);
  149. }
  150. /**
  151. * Retourne le slug du premier catalogue qui contient le produit et qui est accèssible au user courant
  152. * @param Product $product
  153. *
  154. * @return mixed|null
  155. * @throws \JsonException
  156. */
  157. public function getUserFirstGrantedCatalogSlugForProduct(Product $product)
  158. {
  159. $currentUser = $this->security->getUser();
  160. return $this->aclService->getUserFirstGrantedCatalogSlugForProduct($currentUser, $product);
  161. }
  162. /**
  163. * Défini si l'utilisateur à le droit de voir le document
  164. *
  165. * @param User|null $user
  166. * @param Parameter $document
  167. *
  168. * @return bool
  169. *
  170. * @throws \JsonException
  171. */
  172. public function canDisplayDocument(?User $user, Parameter $document): bool
  173. {
  174. return $this->aclService->userIsGrantedOnDocument($user, $document);
  175. }
  176. public function filterVisibleDocuments(?User $user, array $documents, bool $onlyPublicOnSecurityRoute = false): array
  177. {
  178. [$currentRoute] = $this->getCurrentRouteContext();
  179. $isSecurityRoute = $currentRoute !== null && in_array($currentRoute, ACL::ACL_SECURITY_ROUTES, true);
  180. return array_values(array_filter($documents, function(Parameter $document) use ($user, $onlyPublicOnSecurityRoute, $isSecurityRoute)
  181. {
  182. if(!$this->canDisplayDocument($user, $document))
  183. {
  184. return false;
  185. }
  186. if($onlyPublicOnSecurityRoute && $isSecurityRoute && !$document->isPublic())
  187. {
  188. return false;
  189. }
  190. return true;
  191. }));
  192. }
  193. /**
  194. * Lors des documents personnalisé, permet de savoit si le user peut voir le document
  195. *
  196. * @param User|null $user
  197. * @param $id
  198. *
  199. * @return bool
  200. *
  201. * @throws \JsonException
  202. */
  203. public function isDocumentSelectedForUser(?User $user, $id): bool
  204. {
  205. return $this->parameterService->isDocumentSelectedForUser($user, $id);
  206. }
  207. /**
  208. * Permet de savoir si on component est visible par le user courant
  209. *
  210. * Ne doit être utilisé que pour l'affichage twig des components en front
  211. *
  212. * @param array|null $componentOptions
  213. * @param bool $debug
  214. * @return bool
  215. * @throws InvalidArgumentException
  216. */
  217. public function canDisplayComponentByAcl(?array $componentOptions, bool $debug = FALSE)
  218. {
  219. if($componentOptions === NULL || $componentOptions === [])
  220. {
  221. return TRUE;
  222. }
  223. [$currentRoute] = $this->getCurrentRouteContext();
  224. $currentUser = $this->getCurrentUser();
  225. $item = $componentOptions['item'] ?? $componentOptions;
  226. $display = $item['display'] ?? [];
  227. $univers = $item['univers'] ?? [];
  228. $componentAcl = $item['data']['data-component-acl'] ?? ACL::ACL_NO_COMPONENT;
  229. $cacheKey = implode('|', [
  230. $currentRoute ?? 'no-route',
  231. $currentUser instanceof User ? $currentUser->getId() : 'anonymous',
  232. $componentAcl,
  233. (int)($item['enabled'] ?? true),
  234. md5(json_encode($display)),
  235. md5(json_encode($univers)),
  236. ]);
  237. if(array_key_exists($cacheKey, $this->componentVisibilityCache))
  238. {
  239. return $this->componentVisibilityCache[$cacheKey];
  240. }
  241. // Le component est actif sur la page ?
  242. $canDisplay = (bool)$item['enabled'] ?? TRUE;
  243. if(!$canDisplay)
  244. {
  245. return $this->componentVisibilityCache[$cacheKey] = FALSE;
  246. }
  247. // on regarde s'il peut s'afficher sur la page via la clef display
  248. $canDisplay = $this->canDisplayOnPageByConfig($display, $debug);
  249. if(!$canDisplay)
  250. {
  251. return $this->componentVisibilityCache[$cacheKey] = FALSE;
  252. }
  253. // on recherche l'acl si on peut récupérer son slug data-component-acl
  254. if(isset($item['data']['data-component-acl']))
  255. {
  256. $canDisplay = $this->userIsGranted($currentUser instanceof User ? $currentUser : null, $componentAcl);
  257. }
  258. if(!$canDisplay)
  259. {
  260. return $this->componentVisibilityCache[$cacheKey] = FALSE;
  261. }
  262. // on regarde s'il y a des univers... on verifie le currentUser car le component peut être utilisé en partie security
  263. if($currentUser instanceof User && $univers !== [])
  264. {
  265. if($canDisplay && !$currentUser->isDeveloper() && !$currentUser->isSuperAdmin())
  266. {
  267. $canDisplay = $this->aclService->canDisplayByUniverses($currentUser, $univers);
  268. }
  269. }
  270. return $this->componentVisibilityCache[$cacheKey] = $canDisplay;
  271. }
  272. /**
  273. * Défini si un component s'affiche via la clef display
  274. *
  275. * Elle contient 2 enfants:
  276. * enabled_on : array (tableau de route)|null
  277. * disabled_on : array (tableau de route)|null
  278. * Si disabled_on est a autre chose que null, alors c'est lui qui prend l'ascendant
  279. * Afficher partout => enabled_on: null + disabled_on: [] ou null
  280. *
  281. * @param array $display
  282. *
  283. * @return bool
  284. */
  285. private function canDisplayOnPageByConfig(array $display, bool $debug = FALSE)
  286. {
  287. [$currentRoute] = $this->getCurrentRouteContext();
  288. if($currentRoute === NULL)
  289. {
  290. return $this->checkWhenRouteIsNull();
  291. }
  292. $cacheKey = md5(json_encode([$currentRoute, $display]));
  293. if(array_key_exists($cacheKey, $this->displayConfigCache))
  294. {
  295. return $this->displayConfigCache[$cacheKey];
  296. }
  297. $canDisplay = TRUE; // par défaut on affiche
  298. // On prend la config du disabled_on si elle n'est pas nulle
  299. $displayByDisabled = FALSE;
  300. if(isset($display['disabled_on']) && $display['disabled_on'] !== NULL)
  301. {
  302. $displayByDisabled = TRUE;
  303. }
  304. // On prend la config enbaled_on
  305. if(isset($display['enabled_on']) && !$displayByDisabled)
  306. {
  307. $arrayRoute = $display['enabled_on'];
  308. if(gettype($display['enabled_on']) === "string")
  309. {
  310. $arrayRoute = json_decode($display['enabled_on'], true);
  311. }
  312. switch(TRUE)
  313. {
  314. // tableau vide, ce n'est pas visible
  315. case $arrayRoute === []:
  316. $canDisplay = FALSE;
  317. break;
  318. // NULL ou valeur qui n'est pas un tableau, on considère que c'est visible
  319. // ainsi si enabled_on et disabled_on sont NULL, on affiche
  320. case $arrayRoute === NULL:
  321. case !is_array($arrayRoute):
  322. $canDisplay = TRUE;
  323. break;
  324. // on regarde si la route actuelle est dans le tableau pour l'afficher
  325. default:
  326. $canDisplay = in_array($currentRoute, $arrayRoute, TRUE);
  327. break;
  328. }
  329. }
  330. // on prend la config disabled_on
  331. if(isset($display['disabled_on']) && $displayByDisabled)
  332. {
  333. $arrayRoute = $display['disabled_on'];
  334. if(gettype($display['disabled_on']) === "string")
  335. {
  336. $arrayRoute = json_decode($display['disabled_on'], true);
  337. }
  338. switch(TRUE)
  339. {
  340. // tableau vide, c'est visible partout
  341. case $arrayRoute === []:
  342. $canDisplay = TRUE;
  343. break;
  344. // NULL ou valeur qui n'est pas un tableau, on refuse l'affichage
  345. // Normalement on ne tombe pas dans cette configuration puisque $displayByDisabled est FALSE
  346. case $arrayRoute === NULL:
  347. case !is_array($arrayRoute):
  348. $canDisplay = FALSE;
  349. break;
  350. // on regarde si la route actuelle est dans le tableau pour refuser l'affichage
  351. default:
  352. $canDisplay = !in_array($currentRoute, $arrayRoute, TRUE);
  353. break;
  354. }
  355. }
  356. return $this->displayConfigCache[$cacheKey] = $canDisplay;
  357. }
  358. private function getCurrentRouteContext(): array
  359. {
  360. if($this->currentRequestResolved)
  361. {
  362. return [$this->currentRouteCache, $this->currentRouteParamsCache];
  363. }
  364. $request = $this->requestStack->getCurrentRequest();
  365. $this->currentRouteCache = $request?->get('_route');
  366. $this->currentRouteParamsCache = $request?->get('_route_params');
  367. $this->currentRequestResolved = true;
  368. return [$this->currentRouteCache, $this->currentRouteParamsCache];
  369. }
  370. private function getCurrentUser(): User|UserInterface|null
  371. {
  372. if($this->currentUserResolved)
  373. {
  374. return $this->currentUserCache;
  375. }
  376. $this->currentUserCache = $this->security->getUser();
  377. $this->currentUserResolved = true;
  378. return $this->currentUserCache;
  379. }
  380. /**
  381. * Permet de savoir si le user peut voir le slider
  382. *
  383. * Les acl du slider sont intégré à l'édition de l'entité
  384. *
  385. * @param User $user
  386. * @param SliderItem $item
  387. *
  388. * @return bool
  389. */
  390. public function canDisplaySliderItem(?User $user, SliderItem $item): bool
  391. {
  392. if(!$user) return FALSE;
  393. $jobs = $item->getDisplayJob();
  394. $universes = $item->getDisplayUniverses();
  395. // Le superadmin et dev doivent pouvoir tout voir...
  396. if($user->isDeveloper() || $user->isSuperAdmin())
  397. {
  398. return TRUE;
  399. }
  400. // Par défaut, tout s'affiche
  401. $canDisplay = TRUE;
  402. // SI config par job on regarde si ça match
  403. if($jobs !== NULL && !in_array($user->getJob(), $jobs, TRUE))
  404. {
  405. $canDisplay = FALSE;
  406. }
  407. // Si config par univers on regarde si ça match
  408. if($universes !== NULL)
  409. {
  410. foreach($user->getUniverses() as $userUnivers)
  411. {
  412. if(in_array($userUnivers->getSlug(), $universes, TRUE))
  413. {
  414. $canDisplay = TRUE;
  415. break;
  416. }
  417. $canDisplay = FALSE;
  418. }
  419. }
  420. return $canDisplay;
  421. }
  422. /**
  423. * Normalise la transformation de l'array qui contient les params d'une route pour la transformer en string
  424. * @param array $params
  425. *
  426. * @return false|string
  427. * @throws \JsonException
  428. */
  429. public function formatParamsToString(array $params)
  430. {
  431. return $this->aclService->formatArrayParamsToString($params);
  432. }
  433. /**
  434. * Retourne un tableau avec la liste de roles et les jobs relatif à ces roles
  435. *
  436. * @return array[]
  437. */
  438. public function getDefaultRolesAndJobs()
  439. {
  440. return $this->aclService->getDefaultRoleAndJob();
  441. }
  442. /**
  443. * @return UserInterface|null
  444. */
  445. public function getOriginalUser(): ?UserInterface
  446. {
  447. $token = $this->security->getToken();
  448. if($token instanceof SwitchUserToken)
  449. {
  450. $user = $token->getOriginalToken()->getUser();
  451. $user = $this->em->getRepository(User::class)->findOneBy(['email' => $user->getEmail()]);
  452. return $user;
  453. }
  454. return $this->security->getUser();
  455. }
  456. /**
  457. * @param User $user
  458. * @param string $role
  459. *
  460. * @return bool
  461. */
  462. public function hasRole(User $user, string $role): bool
  463. {
  464. $reachableRoles = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
  465. return in_array($role, $reachableRoles);
  466. }
  467. }