From 5a1a5049f10fafa095428b697869aabdd7620c96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tonon=20Gr=C3=A9gory?= <gtonon@clever-age.com>
Date: Mon, 27 Jan 2025 11:21:12 +0100
Subject: [PATCH 1/3] Permissions on users logic

---
 config/services/controller.yaml               |   3 +
 src/CleverAgeUiProcessBundle.php              |   7 ++
 .../Admin/LogRecordCrudController.php         |  34 +++++-
 src/Controller/Admin/Process/LaunchAction.php |   4 +
 .../Admin/ProcessDashboardController.php      |  17 ++-
 .../Admin/ProcessExecutionCrudController.php  |  27 ++++-
 .../Admin/ProcessScheduleCrudController.php   |   2 +
 src/Controller/Admin/UserCrudController.php   |  23 +++-
 src/Controller/ProcessExecuteController.php   |   3 +
 .../SecurityRolesCompilerPass.php             |  34 ++++++
 templates/admin/process/list.html.twig        | 104 +++++++++---------
 translations/messages.fr.yaml                 |   2 +
 12 files changed, 193 insertions(+), 67 deletions(-)
 create mode 100644 src/DependencyInjection/CompilerPass/SecurityRolesCompilerPass.php

diff --git a/config/services/controller.yaml b/config/services/controller.yaml
index 4cec79e..f37eac3 100644
--- a/config/services/controller.yaml
+++ b/config/services/controller.yaml
@@ -12,5 +12,8 @@ services:
             $context: '@EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory'
             $logDirectory: '%kernel.logs_dir%'
             $processExecutionRepository: '@cleverage_ui_process.repository.process_execution'
+            CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry $processConfigurationRegistry: '@cleverage_process.registry.process_configuration'
+            Symfony\Component\Security\Core\Role\RoleHierarchy $roleHierarchy: '@security.role_hierarchy'
+            Symfony\Contracts\Translation\TranslatorInterface $translator: '@translator'
         tags:
             - { name: 'controller.service_arguments' }
diff --git a/src/CleverAgeUiProcessBundle.php b/src/CleverAgeUiProcessBundle.php
index 8acf1d7..6ede138 100644
--- a/src/CleverAgeUiProcessBundle.php
+++ b/src/CleverAgeUiProcessBundle.php
@@ -13,6 +13,8 @@
 
 namespace CleverAge\UiProcessBundle;
 
+use CleverAge\UiProcessBundle\DependencyInjection\CompilerPass\SecurityRolesCompilerPass;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
 class CleverAgeUiProcessBundle extends Bundle
@@ -21,4 +23,9 @@ public function getPath(): string
     {
         return \dirname(__DIR__);
     }
+
+    public function build(ContainerBuilder $container)
+    {
+        $container->addCompilerPass(new SecurityRolesCompilerPass());
+    }
 }
diff --git a/src/Controller/Admin/LogRecordCrudController.php b/src/Controller/Admin/LogRecordCrudController.php
index ce515a5..1987d43 100644
--- a/src/Controller/Admin/LogRecordCrudController.php
+++ b/src/Controller/Admin/LogRecordCrudController.php
@@ -19,25 +19,32 @@
 use CleverAge\UiProcessBundle\Admin\Filter\LogProcessFilter;
 use CleverAge\UiProcessBundle\Entity\LogRecord;
 use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager;
+use Doctrine\ORM\QueryBuilder;
+use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
+use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
 use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
+use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
+use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
 use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
 use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter;
 use Monolog\Level;
 use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Security\Core\Role\RoleHierarchy;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
-#[IsGranted('ROLE_USER')]
+#[IsGranted('ROLE_ADMIN')]
 class LogRecordCrudController extends AbstractCrudController
 {
     public function __construct(
         private readonly ProcessConfigurationsManager $processConfigurationsManager,
         private readonly RequestStack $requestStack,
+        private readonly RoleHierarchy $roleHierarchy,
     ) {
     }
 
@@ -83,8 +90,14 @@ public function configureActions(Actions $actions): Actions
     public function configureFilters(Filters $filters): Filters
     {
         $id = $this->requestStack->getMainRequest()?->query->all('filters')['process']['value'] ?? null;
+        $roles = $this->roleHierarchy->getReachableRoleNames($this->getUser()?->getRoles() ?? []);
         $processList = $this->processConfigurationsManager->getPublicProcesses();
         $processList = array_map(fn (ProcessConfiguration $cfg) => $cfg->getCode(), $processList);
+        $processList = array_filter(
+            $processList, fn (string $code) => \in_array('ROLE_PROCESS_VIEW#'.$code,
+                $roles
+            )
+        );
 
         return $filters->add(
             LogProcessFilter::new('process', $processList, $id)
@@ -92,4 +105,23 @@ public function configureFilters(Filters $filters): Filters
             ChoiceFilter::new('level')->setChoices(array_combine(Level::NAMES, Level::VALUES))
         )->add('message')->add('context')->add('createdAt');
     }
+
+    public function createIndexQueryBuilder(
+        SearchDto $searchDto,
+        EntityDto $entityDto,
+        FieldCollection $fields,
+        FilterCollection $filters,
+    ): QueryBuilder {
+        $roles = $this->roleHierarchy->getReachableRoleNames($this->getUser()?->getRoles() ?? []);
+        $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
+        $qb->join('entity.processExecution', 'processExecution');
+        $qb->andWhere(
+            $qb->expr()->in(
+                (string) $qb->expr()->concat($qb->expr()->literal('ROLE_PROCESS_VIEW#'), 'processExecution.code'),
+                ':roles'
+            )
+        )->setParameter('roles', $roles);
+
+        return $qb;
+    }
 }
diff --git a/src/Controller/Admin/Process/LaunchAction.php b/src/Controller/Admin/Process/LaunchAction.php
index 8f6bed5..7afdede 100644
--- a/src/Controller/Admin/Process/LaunchAction.php
+++ b/src/Controller/Admin/Process/LaunchAction.php
@@ -54,6 +54,10 @@ public function __invoke(
         if (null === $processCode) {
             throw new MissingProcessException();
         }
+
+        if (false === $this->isGranted('ROLE_PROCESS_EXECUTE#'.$processCode)) {
+            throw $this->createAccessDeniedException();
+        }
         $uiOptions = $processConfigurationsManager->getUiOptions($processCode);
         if (null === $uiOptions) {
             throw new \InvalidArgumentException('Missing UI Options');
diff --git a/src/Controller/Admin/ProcessDashboardController.php b/src/Controller/Admin/ProcessDashboardController.php
index 9ec0908..872b8a5 100644
--- a/src/Controller/Admin/ProcessDashboardController.php
+++ b/src/Controller/Admin/ProcessDashboardController.php
@@ -27,7 +27,7 @@
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\Component\Translation\LocaleSwitcher;
 
-#[IsGranted('ROLE_USER')]
+#[IsGranted('ROLE_ADMIN')]
 class ProcessDashboardController extends AbstractDashboardController
 {
     public function __construct(
@@ -59,16 +59,15 @@ public function configureMenuItems(): iterable
                 MenuItem::linkToRoute('Process list', 'fas fa-list', 'process_list'),
                 MenuItem::linkToCrud('Executions', 'fas fa-rocket', ProcessExecution::class),
                 MenuItem::linkToCrud('Logs', 'fas fa-pen', LogRecord::class),
-                MenuItem::linkToCrud('Scheduler', 'fas fa-solid fa-clock', ProcessSchedule::class),
+                MenuItem::linkToCrud('Scheduler', 'fas fa-solid fa-clock', ProcessSchedule::class)
+                    ->setPermission('ROLE_SUPER_ADMIN'),
             ]
         );
-        if ($this->isGranted('ROLE_ADMIN')) {
-            yield MenuItem::subMenu('Users', 'fas fa-user')->setSubItems(
-                [
-                    MenuItem::linkToCrud('User List', 'fas fa-user', User::class),
-                ]
-            );
-        }
+        yield MenuItem::subMenu('Users', 'fas fa-user')->setSubItems(
+            [
+                MenuItem::linkToCrud('User List', 'fas fa-user', User::class),
+            ]
+        )->setPermission('ROLE_SUPER_ADMIN');
     }
 
     public function configureCrud(): Crud
diff --git a/src/Controller/Admin/ProcessExecutionCrudController.php b/src/Controller/Admin/ProcessExecutionCrudController.php
index 5e60a60..63cc798 100644
--- a/src/Controller/Admin/ProcessExecutionCrudController.php
+++ b/src/Controller/Admin/ProcessExecutionCrudController.php
@@ -17,12 +17,17 @@
 use CleverAge\UiProcessBundle\Admin\Field\EnumField;
 use CleverAge\UiProcessBundle\Entity\ProcessExecution;
 use CleverAge\UiProcessBundle\Repository\ProcessExecutionRepository;
+use Doctrine\ORM\QueryBuilder;
+use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
+use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
 use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
 use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
+use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
+use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
 use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
 use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
@@ -30,14 +35,16 @@
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Security\Core\Role\RoleHierarchy;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 
-#[IsGranted('ROLE_USER')]
+#[IsGranted('ROLE_ADMIN')]
 class ProcessExecutionCrudController extends AbstractCrudController
 {
     public function __construct(
         private readonly ProcessExecutionRepository $processExecutionRepository,
         private readonly string $logDirectory,
+        private readonly RoleHierarchy $roleHierarchy,
     ) {
     }
 
@@ -153,4 +160,22 @@ private function getLogFilePath(ProcessExecution $processExecution): string
             \DIRECTORY_SEPARATOR.$processExecution->logFilename
         ;
     }
+
+    public function createIndexQueryBuilder(
+        SearchDto $searchDto,
+        EntityDto $entityDto,
+        FieldCollection $fields,
+        FilterCollection $filters,
+    ): QueryBuilder {
+        $roles = $this->roleHierarchy->getReachableRoleNames($this->getUser()?->getRoles() ?? []);
+        $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
+        $qb->andWhere(
+            $qb->expr()->in(
+                (string) $qb->expr()->concat($qb->expr()->literal('ROLE_PROCESS_VIEW#'), 'entity.code'),
+                ':roles'
+            )
+        )->setParameter('roles', $roles);
+
+        return $qb;
+    }
 }
diff --git a/src/Controller/Admin/ProcessScheduleCrudController.php b/src/Controller/Admin/ProcessScheduleCrudController.php
index df5b2f2..4c0edc7 100644
--- a/src/Controller/Admin/ProcessScheduleCrudController.php
+++ b/src/Controller/Admin/ProcessScheduleCrudController.php
@@ -35,7 +35,9 @@
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Process\Process;
 use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger;
+use Symfony\Component\Security\Http\Attribute\IsGranted;
 
+#[IsGranted('ROLE_SUPER_ADMIN')]
 class ProcessScheduleCrudController extends AbstractCrudController
 {
     public function __construct(private readonly ProcessConfigurationsManager $processConfigurationsManager)
diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php
index d084fdd..e024018 100644
--- a/src/Controller/Admin/UserCrudController.php
+++ b/src/Controller/Admin/UserCrudController.php
@@ -13,6 +13,7 @@
 
 namespace CleverAge\UiProcessBundle\Controller\Admin;
 
+use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
 use CleverAge\UiProcessBundle\Entity\User;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
 use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
@@ -31,19 +32,29 @@
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
+use Symfony\Contracts\Translation\TranslatorInterface;
 
-#[IsGranted('ROLE_USER')]
+#[IsGranted('ROLE_SUPER_ADMIN')]
 class UserCrudController extends AbstractCrudController
 {
-    /** @param array<string, string> $roles */
-    public function __construct(private readonly array $roles)
-    {
+    /** @param array<string, array<string, string>|string> $roles */
+    public function __construct(
+        private array $roles,
+        private readonly ProcessConfigurationRegistry $processConfigurationRegistry,
+        private readonly TranslatorInterface $translator,
+    ) {
+        foreach ($this->processConfigurationRegistry->getProcessConfigurations() as $config) {
+            $this->roles[$config->getCode()] = [
+                $this->translator->trans('View process').' '.$config->getCode() => 'ROLE_PROCESS_VIEW#'.$config->getCode(),
+                $this->translator->trans('Execute process').' '.$config->getCode() => 'ROLE_PROCESS_EXECUTE#'.$config->getCode(),
+            ];
+        }
     }
 
     public function configureCrud(Crud $crud): Crud
     {
         $crud->showEntityActionsInlined();
-        $crud->setEntityPermission('ROLE_ADMIN');
+        $crud->setEntityPermission('ROLE_SUPER_ADMIN');
 
         return $crud;
     }
@@ -79,7 +90,7 @@ public function configureFields(string $pageName): iterable
         yield FormField::addTab('Roles')->setIcon('fa fa-theater-masks');
         yield ChoiceField::new('roles', false)
             ->setChoices($this->roles)
-            ->setFormTypeOptions(['multiple' => true, 'expanded' => true]);
+            ->setFormTypeOptions(['multiple' => true, 'expanded' => false]);
         yield FormField::addTab('Intl.')->setIcon('fa fa-flag');
         yield TimezoneField::new('timezone');
         yield LocaleField::new('locale');
diff --git a/src/Controller/ProcessExecuteController.php b/src/Controller/ProcessExecuteController.php
index 5de49d2..3b070c2 100644
--- a/src/Controller/ProcessExecuteController.php
+++ b/src/Controller/ProcessExecuteController.php
@@ -39,6 +39,9 @@ public function __invoke(
             }
             throw new UnprocessableEntityHttpException(implode('. ', $violationsMessages));
         }
+        if (false === $this->isGranted('ROLE_PROCESS_EXECUTE#'.$httpProcessExecution->code)) {
+            throw $this->createAccessDeniedException();
+        }
         $bus->dispatch(
             new ProcessExecuteMessage(
                 $httpProcessExecution->code ?? '',
diff --git a/src/DependencyInjection/CompilerPass/SecurityRolesCompilerPass.php b/src/DependencyInjection/CompilerPass/SecurityRolesCompilerPass.php
new file mode 100644
index 0000000..e4c049f
--- /dev/null
+++ b/src/DependencyInjection/CompilerPass/SecurityRolesCompilerPass.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the CleverAge/UiProcessBundle package.
+ *
+ * Copyright (c) Clever-Age
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CleverAge\UiProcessBundle\DependencyInjection\CompilerPass;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class SecurityRolesCompilerPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        if ($container->hasDefinition('security.role_hierarchy')) {
+            // For each configured process, add ROLE_PROCESS_VIEW#<code> and ROLE_PROCESS_EXECUTE#<code> under ROLE_SUPER_ADMIN role
+            $pbExtCfg = $container->getExtensionConfig('clever_age_process');
+            $processCodes = array_keys(array_merge(...array_column($pbExtCfg, 'configurations')));
+            $processRoles = array_merge(...array_map(fn ($code) => ['ROLE_PROCESS_VIEW#'.$code, 'ROLE_PROCESS_EXECUTE#'.$code], $processCodes));
+            $roleHierarchy = $container->getParameter('security.role_hierarchy.roles');
+            if (\is_array($roleHierarchy)) {
+                $roleHierarchy['ROLE_SUPER_ADMIN'] = array_merge($roleHierarchy['ROLE_SUPER_ADMIN'] ?? [], $processRoles);
+                $container->setParameter('security.role_hierarchy.roles', $roleHierarchy);
+                $container->getDefinition('security.role_hierarchy')->replaceArgument(0, $roleHierarchy);
+            }
+        }
+    }
+}
diff --git a/templates/admin/process/list.html.twig b/templates/admin/process/list.html.twig
index 06b5cff..2a63136 100644
--- a/templates/admin/process/list.html.twig
+++ b/templates/admin/process/list.html.twig
@@ -14,7 +14,7 @@
                 <th><span>{{ 'Status'|trans }}</span></th>
                 <th><span>{{ 'Source'|trans }}</span></th>
                 <th><span>{{ 'Target'|trans }}</span></th>
-                <th class="text-center"><span>{{ 'Actions'|trans }}</span></th>
+                <th class="text-right"><span>{{ 'Actions'|trans }}</span></th>
             </tr>
         {% endblock %}
         </thead>
@@ -22,57 +22,61 @@
         {% block table_body %}
             {# @var process \CleverAge\ProcessBundle\Configuration\ProcessConfiguration #}
             {% for process in processes %}
-                {% set lastExecution = get_last_execution_date(process.code) %}
-                {% set uiOptions = resolve_ui_options(process.code) %}
-                {% set statusClass = '' %}
-                {% if lastExecution is not null %}
-                    {% set statusClass = lastExecution.status.value == 'failed' ? 'danger' : 'success' %}
-                {% endif %}
-                <tr>
-                    <td>{{ process.code }}</td>
-                    <td>{% if lastExecution is not null %}{{ lastExecution.startDate|date('Y/m/d H:i:s') }}{% endif %}</td>
-                    <td><span class="badge badge-{{ statusClass }}">{% if lastExecution is not null %}{{ lastExecution.status.value }}{% endif %}</span></td>
-                    <td>{% if process.options.ui.source is defined %}{{ process.options.ui.source }}{% endif %}</td>
-                    <td>{% if process.options.ui.target is defined %}{{ process.options.ui.target }}{% endif %}</td>
-                    <td class="text-center">
-                        {% if ('modal' == uiOptions.ui_launch_mode) %}
-                            <a class="px-1" data-toggle="tooltip" data-placement="top" title="{{ 'Launch'|trans }}" data-bs-toggle="modal" data-bs-target="#{{ process.code }}">
-                                <i class="fas fa-rocket"></i>
-                            </a>
-                        {% else %}
-                            <a class="px-1" data-toggle="tooltip" data-placement="top" title="{{ 'Launch'|trans }}" href="{{ url('process', {routeName: 'process_launch', process: process.code}) }}">
-                                <i class="fas fa-rocket"></i>
-                            </a>
-                        {% endif %}
-                        <a
-                            class="px-1"
-                            data-toggle="tooltip"
-                            data-placement="top"
-                            title="{{ 'View executions'|trans }}"
-                            href="{{ url(
-                                'process',
-                                {
-                                    crudAction: 'index',
-                                    crudControllerFqcn: 'CleverAge\\UiProcessBundle\\Controller\\Admin\\ProcessExecutionCrudController',
-                                    filters: {
-                                        code: {
-                                            comparison: '=',
-                                            value: process.code,
+                {% if is_granted("ROLE_PROCESS_VIEW##{process.code}", process) %}
+                    {% set lastExecution = get_last_execution_date(process.code) %}
+                    {% set uiOptions = resolve_ui_options(process.code) %}
+                    {% set statusClass = '' %}
+                    {% if lastExecution is not null %}
+                        {% set statusClass = lastExecution.status.value == 'failed' ? 'danger' : 'success' %}
+                    {% endif %}
+                    <tr>
+                        <td>{{ process.code }}</td>
+                        <td>{% if lastExecution is not null %}{{ lastExecution.startDate|date('Y/m/d H:i:s') }}{% endif %}</td>
+                        <td><span class="badge badge-{{ statusClass }}">{% if lastExecution is not null %}{{ lastExecution.status.value }}{% endif %}</span></td>
+                        <td>{% if process.options.ui.source is defined %}{{ process.options.ui.source }}{% endif %}</td>
+                        <td>{% if process.options.ui.target is defined %}{{ process.options.ui.target }}{% endif %}</td>
+                        <td class="text-right">
+                            {% if is_granted("ROLE_PROCESS_EXECUTE##{process.code}", process) %}
+                                {% if ('modal' == uiOptions.ui_launch_mode) %}
+                                    <a class="px-1" data-toggle="tooltip" data-placement="top" title="{{ 'Launch'|trans }}" data-bs-toggle="modal" data-bs-target="#{{ process.code }}" role="button">
+                                        <i class="fas fa-rocket"></i>
+                                    </a>
+                                {% else %}
+                                    <a class="px-1" data-toggle="tooltip" data-placement="top" title="{{ 'Launch'|trans }}" href="{{ url('process', {routeName: 'process_launch', process: process.code}) }}" role="button">
+                                        <i class="fas fa-rocket"></i>
+                                    </a>
+                                {% endif %}
+                            {% endif %}
+                            <a
+                                class="px-1"
+                                data-toggle="tooltip"
+                                data-placement="top"
+                                title="{{ 'View executions'|trans }}"
+                                href="{{ url(
+                                    'process',
+                                    {
+                                        crudAction: 'index',
+                                        crudControllerFqcn: 'CleverAge\\UiProcessBundle\\Controller\\Admin\\ProcessExecutionCrudController',
+                                        filters: {
+                                            code: {
+                                                comparison: '=',
+                                                value: process.code,
+                                            },
                                         },
                                     },
-                                },
-                            ) }}"
-                        >
-                            <i class="fas fa-eye"></i>
-                        </a>
-                    </td>
-                </tr>
-                <twig:ui:BootstrapModal
-                        id="{{ process.code }}"
-                        title="{{ 'Run process'|trans }} {{ process.code }}"
-                        message="{{ 'Do you really want to run process %process% in background'|trans({'%process%': process.code}) }} ?"
-                        confirmUrl="{{ url('process', {routeName: 'process_launch', process: process.code}) }}"
-                />
+                                ) }}"
+                            >
+                                <i class="fas fa-eye"></i>
+                            </a>
+                        </td>
+                    </tr>
+                    <twig:ui:BootstrapModal
+                            id="{{ process.code }}"
+                            title="{{ 'Run process'|trans }} {{ process.code }}"
+                            message="{{ 'Do you really want to run process %process% in background'|trans({'%process%': process.code}) }} ?"
+                            confirmUrl="{{ url('process', {routeName: 'process_launch', process: process.code}) }}"
+                    />
+                {% endif %}
             {% endfor %}
         {% endblock %}
         </tbody>
diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml
index 4d276e3..e9126d0 100644
--- a/translations/messages.fr.yaml
+++ b/translations/messages.fr.yaml
@@ -52,3 +52,5 @@ key: clé
 value: valeur
 Context Key: Context clé
 Context Value: Context valeur
+View process: Voir le process
+Execute process: Exécuter le process

From 77d847a6ab974ebe11053d9dd10d2713f8c62830 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tonon=20Gr=C3=A9gory?= <gtonon@clever-age.com>
Date: Tue, 28 Jan 2025 10:27:17 +0100
Subject: [PATCH 2/3] Add impersonate action

---
 src/Controller/Admin/UserCrudController.php             | 6 +++++-
 src/DependencyInjection/CleverAgeUiProcessExtension.php | 1 +
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php
index e024018..0aa2f70 100644
--- a/src/Controller/Admin/UserCrudController.php
+++ b/src/Controller/Admin/UserCrudController.php
@@ -31,6 +31,7 @@
 use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
+use Symfony\Component\Routing\Generator\UrlGenerator;
 use Symfony\Component\Security\Http\Attribute\IsGranted;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
@@ -106,7 +107,10 @@ public function configureActions(Actions $actions): Actions
                 ->addCssClass('text-warning'))->update(Crud::PAGE_INDEX, Action::DELETE, fn (Action $action) => $action->setIcon('fa fa-trash-o')
                 ->setLabel(false)
                 ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, fn (Action $action) => $action->setLabel('Delete')
-                ->addCssClass(''))->add(Crud::PAGE_EDIT, Action::new('generateToken')->linkToCrudAction('generateToken'));
+                ->addCssClass(''))->add(Crud::PAGE_EDIT, Action::new('generateToken')->linkToCrudAction('generateToken'))
+                ->add(Crud::PAGE_INDEX, Action::new('ConnectAs')->linkToUrl(function (User $user) {
+                    return $this->generateUrl('process', ['_switch_user' => $user->getEmail()], UrlGenerator::ABSOLUTE_URL);
+                })->setLabel(false)->setIcon('fa-solid fa-right-to-bracket'))->setPermission('ConnectAs', 'ROLE_SUPER_ADMIN');
     }
 
     public function generateToken(AdminContext $adminContext, AdminUrlGenerator $adminUrlGenerator): Response
diff --git a/src/DependencyInjection/CleverAgeUiProcessExtension.php b/src/DependencyInjection/CleverAgeUiProcessExtension.php
index 2b7b302..c1a39ca 100644
--- a/src/DependencyInjection/CleverAgeUiProcessExtension.php
+++ b/src/DependencyInjection/CleverAgeUiProcessExtension.php
@@ -150,6 +150,7 @@ public function prepend(ContainerBuilder $container): void
                             'target' => 'process_login',
                             'clear_site_data' => '*',
                         ],
+                        'switch_user' => true,
                     ],
                 ],
             ]

From 312804102825fc5ccf43ad37ddca3c5cc4bc9210 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tonon=20Gr=C3=A9gory?= <gtonon@clever-age.com>
Date: Tue, 28 Jan 2025 10:33:16 +0100
Subject: [PATCH 3/3] Add impersonate action quality fix

---
 src/Controller/Admin/UserCrudController.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php
index 0aa2f70..21da6e1 100644
--- a/src/Controller/Admin/UserCrudController.php
+++ b/src/Controller/Admin/UserCrudController.php
@@ -108,9 +108,7 @@ public function configureActions(Actions $actions): Actions
                 ->setLabel(false)
                 ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, fn (Action $action) => $action->setLabel('Delete')
                 ->addCssClass(''))->add(Crud::PAGE_EDIT, Action::new('generateToken')->linkToCrudAction('generateToken'))
-                ->add(Crud::PAGE_INDEX, Action::new('ConnectAs')->linkToUrl(function (User $user) {
-                    return $this->generateUrl('process', ['_switch_user' => $user->getEmail()], UrlGenerator::ABSOLUTE_URL);
-                })->setLabel(false)->setIcon('fa-solid fa-right-to-bracket'))->setPermission('ConnectAs', 'ROLE_SUPER_ADMIN');
+                ->add(Crud::PAGE_INDEX, Action::new('ConnectAs')->linkToUrl(fn (User $user) => $this->generateUrl('process', ['_switch_user' => $user->getEmail()], UrlGenerator::ABSOLUTE_URL))->setLabel(false)->setIcon('fa-solid fa-right-to-bracket'))->setPermission('ConnectAs', 'ROLE_SUPER_ADMIN');
     }
 
     public function generateToken(AdminContext $adminContext, AdminUrlGenerator $adminUrlGenerator): Response