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..21da6e1 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; @@ -30,20 +31,31 @@ 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; -#[IsGranted('ROLE_USER')] +#[IsGranted('ROLE_SUPER_ADMIN')] class UserCrudController extends AbstractCrudController { - /** @param array $roles */ - public function __construct(private readonly array $roles) - { + /** @param array|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 +91,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'); @@ -95,7 +107,8 @@ 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(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 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/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, ], ], ] 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 @@ +hasDefinition('security.role_hierarchy')) { + // For each configured process, add ROLE_PROCESS_VIEW# and ROLE_PROCESS_EXECUTE# 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 @@ {{ 'Status'|trans }} {{ 'Source'|trans }} {{ 'Target'|trans }} - {{ 'Actions'|trans }} + {{ 'Actions'|trans }} {% endblock %} @@ -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 %} - - {{ process.code }} - {% if lastExecution is not null %}{{ lastExecution.startDate|date('Y/m/d H:i:s') }}{% endif %} - {% if lastExecution is not null %}{{ lastExecution.status.value }}{% endif %} - {% if process.options.ui.source is defined %}{{ process.options.ui.source }}{% endif %} - {% if process.options.ui.target is defined %}{{ process.options.ui.target }}{% endif %} - - {% if ('modal' == uiOptions.ui_launch_mode) %} - - - - {% else %} - - - - {% endif %} - + {{ process.code }} + {% if lastExecution is not null %}{{ lastExecution.startDate|date('Y/m/d H:i:s') }}{% endif %} + {% if lastExecution is not null %}{{ lastExecution.status.value }}{% endif %} + {% if process.options.ui.source is defined %}{{ process.options.ui.source }}{% endif %} + {% if process.options.ui.target is defined %}{{ process.options.ui.target }}{% endif %} + + {% if is_granted("ROLE_PROCESS_EXECUTE##{process.code}", process) %} + {% if ('modal' == uiOptions.ui_launch_mode) %} + + + + {% else %} + + + + {% endif %} + {% endif %} + - - - - - + ) }}" + > + + + + + + {% endif %} {% endfor %} {% endblock %} 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