diff --git a/config/packages/app.yaml b/config/packages/app.yaml index 07a8def..b3ee84a 100644 --- a/config/packages/app.yaml +++ b/config/packages/app.yaml @@ -9,6 +9,10 @@ core: name: 'Simple page' templates: - {name: "Default", file: "page/simple/default.html.twig"} + # security: + # roles: + # - {name: 'Role foo', role: 'ROLE_FOO'} + # - {name: 'Role bar', role: 'ROLE_BAR'} file_manager: # mimes: # - image/png diff --git a/core/Controller/Site/NodeAdminController.php b/core/Controller/Site/NodeAdminController.php index 327bd34..5149078 100644 --- a/core/Controller/Site/NodeAdminController.php +++ b/core/Controller/Site/NodeAdminController.php @@ -14,6 +14,7 @@ use App\Core\Form\Site\NodeType as EntityType; use App\Core\Manager\EntityManager; use App\Core\Repository\Site\NodeRepository; use App\Core\Site\ControllerLocator; +use App\Core\Site\RoleLocator; use App\Core\Site\PageLocator; use App\Core\Sitemap\SitemapBuilder; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -39,12 +40,14 @@ class NodeAdminController extends AbstractController NodeRepository $nodeRepository, PageLocator $pageLocator, ControllerLocator $controllerLocator, + RoleLocator $roleLocator, Request $request ): Response { $entity = $factory->create($node->getMenu()); $form = $this->createForm(EntityType::class, $entity, [ 'pages' => $pageLocator->getPages(), 'controllers' => $controllerLocator->getControllers(), + 'roles' => $roleLocator->getRoles(), 'navigation' => $node->getMenu()->getNavigation(), ]); @@ -109,12 +112,14 @@ class NodeAdminController extends AbstractController PageFactory $pageFactory, PageLocator $pageLocator, ControllerLocator $controllerLocator, + RoleLocator $roleLocator, Request $request, string $tab = 'content' ): Response { $form = $this->createForm(EntityType::class, $entity, [ 'pages' => $pageLocator->getPages(), 'controllers' => $controllerLocator->getControllers(), + 'roles' => $roleLocator->getRoles(), 'navigation' => $entity->getMenu()->getNavigation(), ]); diff --git a/core/DependencyInjection/Configuration.php b/core/DependencyInjection/Configuration.php index 8334f69..9d6b419 100644 --- a/core/DependencyInjection/Configuration.php +++ b/core/DependencyInjection/Configuration.php @@ -69,6 +69,22 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('security') + ->children() + ->arrayNode('roles') + ->prototype('array') + ->children() + ->scalarNode('name') + ->cannotBeEmpty() + ->end() + ->scalarNode('role') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('pages') ->prototype('array') ->children() diff --git a/core/Entity/Site/Node.php b/core/Entity/Site/Node.php index 4196966..a5c2397 100644 --- a/core/Entity/Site/Node.php +++ b/core/Entity/Site/Node.php @@ -157,6 +157,16 @@ class Node implements EntityInterface */ protected $analyticReferers; + /** + * @ORM\Column(type="array") + */ + private $securityRoles = []; + + /** + * @ORM\Column(type="string", length=3, options={"default"="or"}) + */ + private $securityOperator; + public function __construct() { $this->children = new ArrayCollection(); @@ -640,4 +650,28 @@ class Node implements EntityInterface return $this; } + + public function getSecurityRoles(): array + { + return (array) $this->securityRoles; + } + + public function setSecurityRoles(array $securityRoles): self + { + $this->securityRoles = $securityRoles; + + return $this; + } + + public function getSecurityOperator(): ?string + { + return $this->securityOperator; + } + + public function setSecurityOperator(string $securityOperator): self + { + $this->securityOperator = $securityOperator; + + return $this; + } } diff --git a/core/EventSuscriber/Account/PasswordRequestEventSubscriber.php b/core/EventSubscriber/Account/PasswordRequestEventSubscriber.php similarity index 98% rename from core/EventSuscriber/Account/PasswordRequestEventSubscriber.php rename to core/EventSubscriber/Account/PasswordRequestEventSubscriber.php index 909cb22..8f2c5d2 100644 --- a/core/EventSuscriber/Account/PasswordRequestEventSubscriber.php +++ b/core/EventSubscriber/Account/PasswordRequestEventSubscriber.php @@ -1,6 +1,6 @@ + */ +class RequestSecurityEventSubscriber implements EventSubscriberInterface +{ + protected NodeRepository $nodeRepository; + protected AuthorizationChecker $authorizationChecker; + + public function __construct(NodeRepository $nodeRepository, ContainerInterface $container) + { + $this->nodeRepository = $nodeRepository; + $this->authorizationChecker = $container->get('security.authorization_checker'); + } + + public function onKernelRequest(RequestEvent $event) + { + $request = $event->getRequest(); + + if (!$request->attributes->has('_node')) { + return; + } + + $node = $this->nodeRepository->findOneBy([ + 'id' => $request->attributes->get('_node'), + ]); + + $roles = $node->getSecurityRoles(); + + if (empty($roles)) { + return; + } + + $operator = $node->getSecurityOperator(); + $exception = new AccessDeniedException('Access denied'); + $isAuthorized = false; + + foreach ($roles as $role) { + $isGranted = $this->authorizationChecker->isGranted($role); + + if ('or' === $operator && $isGranted) { + $isAuthorized = true; + } elseif ('and' === $operator && !$isGranted) { + throw $exception; + } + } + + if (!$isAuthorized) { + throw $exception; + } + } + + public static function getSubscribedEvents(): array + { + return [ + RequestEvent::class => ['onKernelRequest', 1], + ]; + } +} diff --git a/core/EventSuscriber/SettingEventSubscriber.php b/core/EventSubscriber/SettingEventSubscriber.php similarity index 94% rename from core/EventSuscriber/SettingEventSubscriber.php rename to core/EventSubscriber/SettingEventSubscriber.php index d8db765..c0ff1c4 100644 --- a/core/EventSuscriber/SettingEventSubscriber.php +++ b/core/EventSubscriber/SettingEventSubscriber.php @@ -1,6 +1,6 @@ 0) { + $builder->add( + 'securityRoles', + ChoiceType::class, + [ + 'label' => 'Roles', + 'required' => false, + 'multiple' => true, + 'expanded' => true, + 'choices' => call_user_func(function () use ($options) { + $choices = []; + + foreach ($options['roles'] as $role) { + $choices[$role->getName()] = $role->getRole(); + } + + return $choices; + }), + ] + ); + + $builder->add( + 'securityOperator', + ChoiceType::class, + [ + 'label' => 'Condition', + 'required' => true, + 'choices' => [ + 'At least one role' => 'or', + 'All roles' => 'and', + ], + ] + ); + } + $actions = [ 'New page' => 'new', 'Use an existing page' => 'existing', @@ -278,6 +313,7 @@ class NodeType extends AbstractType 'data_class' => Node::class, 'pages' => [], 'controllers' => [], + 'roles' => [], 'navigation' => null, ]); } diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml index c2f6d2b..cdba966 100644 --- a/core/Resources/translations/messages.fr.yaml +++ b/core/Resources/translations/messages.fr.yaml @@ -217,3 +217,6 @@ "Disable": "Désactiver" "Reuse the query string": "Réutiliser la chaîne de requête" "First element": "Premier élément" +"Roles": "Rôles" +"Condition": "Condition" +"Security": "Sécurité" diff --git a/core/Resources/views/site/node_admin/_form.html.twig b/core/Resources/views/site/node_admin/_form.html.twig index 42079fd..0496509 100644 --- a/core/Resources/views/site/node_admin/_form.html.twig +++ b/core/Resources/views/site/node_admin/_form.html.twig @@ -173,6 +173,7 @@ {% endif %} {{ form_row(form.url) }} + {{ form_row(form.code) }}