rename core/EventSuscriber with core/EventSubscriber

add security roles in app configuration

add option to restrict node access to specific roles
This commit is contained in:
Simon Vieille 2022-03-10 21:32:22 +01:00
parent 632e6b7c7a
commit ce5c69d467
26 changed files with 304 additions and 29 deletions

View file

@ -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

View file

@ -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(),
]);

View file

@ -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()

View file

@ -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;
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber\Account;
namespace App\Core\EventSubscriber\Account;
use App\Core\Event\Account\PasswordRequestEvent;
use App\Core\Manager\EntityManager;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber;
namespace App\Core\EventSubscriber;
use App\Core\Event\EntityManager\EntityManagerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber;
namespace App\Core\EventSubscriber;
use App\Core\Event\Setting\NavigationSettingEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

View file

@ -0,0 +1,71 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Repository\Site\NodeRepository;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* class RequestSecurityEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
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],
];
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber;
namespace App\Core\EventSubscriber;
use App\Core\Event\Setting\SettingEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber\Site;
namespace App\Core\EventSubscriber\Site;
use App\Core\Site\SiteRequest;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

View file

@ -1,12 +1,12 @@
<?php
namespace App\Core\EventSuscriber\Site;
namespace App\Core\EventSubscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Menu;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Factory\Site\NodeFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;

View file

@ -1,11 +1,11 @@
<?php
namespace App\Core\EventSuscriber\Site;
namespace App\Core\EventSubscriber\Site;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Navigation;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Manager\EntityManager;
use App\Core\Slugify\CodeSlugify;

View file

@ -1,11 +1,11 @@
<?php
namespace App\Core\EventSuscriber\Site;
namespace App\Core\EventSubscriber\Site;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Factory\Site\NodeFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;

View file

@ -1,12 +1,12 @@
<?php
namespace App\Core\EventSuscriber\Site\Page;
namespace App\Core\EventSubscriber\Site\Page;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\FileBlock;
use App\Core\Entity\Site\Page\Page;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Form\FileUploadHandler;
use Symfony\Component\HttpFoundation\File\UploadedFile;

View file

@ -1,11 +1,11 @@
<?php
namespace App\Core\EventSuscriber\Site\Page;
namespace App\Core\EventSubscriber\Site\Page;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\Page;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Form\FileUploadHandler;
use Symfony\Component\HttpFoundation\File\UploadedFile;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber\Site;
namespace App\Core\EventSubscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
@ -8,7 +8,7 @@ use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Navigation;
use App\Core\Entity\Site\Node;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSuscriber\EntityManagerEventSubscriber;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use Symfony\Component\HttpKernel\KernelInterface;
/**

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber\Task;
namespace App\Core\EventSubscriber\Task;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Event\Task\TaskInitEvent;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Core\EventSuscriber\Task;
namespace App\Core\EventSubscriber\Task;
use App\Core\Event\Task\TaskInitEvent;
use App\Core\Event\Task\TaskRunRequestedEvent;

View file

@ -119,6 +119,41 @@ class NodeType extends AbstractType
]
);
if (count($options['roles']) > 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,
]);
}

View file

@ -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é"

View file

@ -173,6 +173,7 @@
{% endif %}
{{ form_row(form.url) }}
{{ form_row(form.code) }}
<div class="pb-3">
<details>
@ -206,9 +207,34 @@
{{ form_row(form.disableUrl) }}
{{ form_row(form.enableAnalytics) }}
{{ form_row(form.code) }}
{{ form_row(form.contentType) }}
{{ form_row(form.controller) }}
<div class="row">
<div class="col-md-6">
{{ form_row(form.controller) }}
</div>
<div class="col-md-6 pl-md-2">
{{ form_row(form.contentType) }}
</div>
</div>
{% if form.securityRoles is defined %}
<div class="pb-3">
<details>
<summary>
{{ 'Security'|trans }}
</summary>
<div class="row">
<div class="col-md-6">
{{ form_row(form.securityRoles) }}
</div>
<div class="col-md-6">
{{ form_row(form.securityOperator) }}
</div>
</div>
</details>
</div>
{% endif %}
<div class="accordion mb-3" data-collection="collection-node-parameters" id="form-node-edit-parameters-collection">
{% for item in form.parameters %}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Core\Site;
/**
* class RoleConfiguration.
*
* @author Simon Vieille <simon@deblan.fr>
*/
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;
}
}

42
core/Site/RoleLocator.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace App\Core\Site;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* class RoleLocator.
*
* @author Simon Vieille <simon@deblan.fr>
*/
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;
}
}
}

View file

@ -1,9 +1,9 @@
<?php
namespace App\EventSuscriber;
namespace App\EventSubscriber;
use App\Core\Event\Setting\NavigationSettingEvent;
use App\Core\EventSuscriber\NavigationSettingEventSubscriber as EventSubscriber;
use App\Core\EventSubscriber\NavigationSettingEventSubscriber as EventSubscriber;
use App\Core\Setting\NavigationSettingManager;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

View file

@ -1,9 +1,9 @@
<?php
namespace App\EventSuscriber;
namespace App\EventSubscriber;
use App\Core\Event\Setting\SettingEvent;
use App\Core\EventSuscriber\SettingEventSubscriber as EventSubscriber;
use App\Core\EventSubscriber\SettingEventSubscriber as EventSubscriber;
use App\Core\Setting\SettingManager;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;