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) }}
@@ -206,9 +207,34 @@ {{ form_row(form.disableUrl) }} {{ form_row(form.enableAnalytics) }} - {{ form_row(form.code) }} - {{ form_row(form.contentType) }} - {{ form_row(form.controller) }} + +
+
+ {{ form_row(form.controller) }} +
+
+ {{ form_row(form.contentType) }} +
+
+ + {% if form.securityRoles is defined %} +
+
+ + {{ 'Security'|trans }} + + +
+
+ {{ form_row(form.securityRoles) }} +
+
+ {{ form_row(form.securityOperator) }} +
+
+
+
+ {% endif %}
{% for item in form.parameters %} diff --git a/core/Site/ControllerLocator.php b/core/Site/ControllerLocator.php index 5956ca7..2991e67 100644 --- a/core/Site/ControllerLocator.php +++ b/core/Site/ControllerLocator.php @@ -30,13 +30,13 @@ class ControllerLocator $params = $this->params['site']['controllers'] ?? []; foreach ($params as $conf) { - $controllerConfiguration = new ControllerConfiguration(); - $controllerConfiguration + $configuration = new ControllerConfiguration(); + $configuration ->setName($conf['name']) ->setAction($conf['action']) ; - $this->controllers[$conf['action']] = $controllerConfiguration; + $this->controllers[$conf['action']] = $configuration; } } } diff --git a/core/Site/RoleConfiguration.php b/core/Site/RoleConfiguration.php new file mode 100644 index 0000000..c15f4f8 --- /dev/null +++ b/core/Site/RoleConfiguration.php @@ -0,0 +1,38 @@ + + */ +class RoleConfiguration +{ + protected string $name; + protected string $role; + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setRole(string $role): self + { + $this->role = $role; + + return $this; + } + + public function getRole(): string + { + return $this->role; + } +} diff --git a/core/Site/RoleLocator.php b/core/Site/RoleLocator.php new file mode 100644 index 0000000..5dcbdd5 --- /dev/null +++ b/core/Site/RoleLocator.php @@ -0,0 +1,42 @@ + + */ +class RoleLocator +{ + protected array $params; + protected array $roles = []; + + public function __construct(ParameterBagInterface $bag) + { + $this->params = $bag->get('core'); + $this->loadRoles(); + } + + public function getRoles(): array + { + return $this->roles; + } + + protected function loadRoles(): void + { + $params = $this->params['site']['security']['roles'] ?? []; + + foreach ($params as $conf) { + $configuration = new RoleConfiguration(); + $configuration + ->setName($conf['name']) + ->setRole($conf['role']) + ; + + $this->roles[$conf['name']] = $configuration; + } + } +} diff --git a/src/EventSuscriber/NavigationSettingEventSubscriber.php b/src/EventSubscriber/NavigationSettingEventSubscriber.php similarity index 94% rename from src/EventSuscriber/NavigationSettingEventSubscriber.php rename to src/EventSubscriber/NavigationSettingEventSubscriber.php index 962b41b..d6cbfa5 100644 --- a/src/EventSuscriber/NavigationSettingEventSubscriber.php +++ b/src/EventSubscriber/NavigationSettingEventSubscriber.php @@ -1,9 +1,9 @@