src/Twig/Runtime/AclRuntime.php line 122

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