src/Twig/Runtime/AclRuntime.php line 97

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