diff --git a/DataFixtures/ORM/LoadRolesACL.php b/DataFixtures/ORM/LoadRolesACL.php new file mode 100644 index 000000000..c0236d25c --- /dev/null +++ b/DataFixtures/ORM/LoadRolesACL.php @@ -0,0 +1,84 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace Chill\EventBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; +use Chill\MainBundle\DataFixtures\ORM\LoadScopes; +use Doctrine\Common\Persistence\ObjectManager; + +/** + * Add roles to existing groups + * + * @author Julien Fastré + * @author Champs Libres + */ +class LoadRolesACL extends AbstractFixture implements OrderedFixtureInterface +{ + public function load(ObjectManager $manager) + { + foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { + $permissionsGroup = $this->getReference($permissionsGroupRef); + foreach (LoadScopes::$references as $scopeRef){ + $scope = $this->getReference($scopeRef); + //create permission group + switch ($permissionsGroup->getName()) { + case 'social': + if ($scope->getName()['en'] === 'administrative') { + break 2; // we do not want any power on administrative + } + break; + case 'administrative': + case 'direction': + if (in_array($scope->getName()['en'], array('administrative', 'social'))) { + break 2; // we do not want any power on social or administrative + } + break; + } + + printf("Adding CHILL_EVENT_UPDATE & CHILL_EVENT_CREATE to %s " + . "permission group, scope '%s' \n", + $permissionsGroup->getName(), $scope->getName()['en']); + $roleScopeUpdate = (new RoleScope()) + ->setRole('CHILL_EVENT_UPDATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeUpdate); + $roleScopeCreate = (new RoleScope()) + ->setRole('CHILL_EVENT_CREATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeCreate); + $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeCreate); + } + + } + + $manager->flush(); + } + + public function getOrder() + { + return 30011; + } + +} diff --git a/DependencyInjection/ChillEventExtension.php b/DependencyInjection/ChillEventExtension.php index 1035659fd..5933488f1 100644 --- a/DependencyInjection/ChillEventExtension.php +++ b/DependencyInjection/ChillEventExtension.php @@ -7,6 +7,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Chill\EventBundle\Security\Authorization\EventVoter; /** * This is the class that loads and manages your bundle configuration @@ -23,8 +24,11 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.yml'); + $loader = new Loader\YamlFileLoader($container, + new FileLocator(__DIR__.'/../Resources/config/services')); + $loader->load('repositories.yml'); + $loader->load('search.yml'); + $loader->load('authorization.yml'); } /* (non-PHPdoc) @@ -32,7 +36,17 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface */ public function prepend(ContainerBuilder $container) { - + $this->prependAuthorization($container); + $this->prependRoute($container); + } + + /** + * add route to route loader for chill + * + * @param ContainerBuilder $container + */ + protected function prependRoute(ContainerBuilder $container) + { //add routes for custom bundle $container->prependExtensionConfig('chill_main', array( 'routing' => array( @@ -42,4 +56,20 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface ) )); } + + /** + * add authorization hierarchy + * + * @param ContainerBuilder $container + */ + protected function prependAuthorization(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + EventVoter::SEE_DETAILS => array(EventVoter::SEE), + EventVoter::UPDATE => array(EventVoter::SEE_DETAILS), + EventVoter::CREATE => array(EventVoter::SEE_DETAILS) + ) + )); + } } diff --git a/Resources/config/services.yml b/Resources/config/services.yml deleted file mode 100644 index d2cfb62f0..000000000 --- a/Resources/config/services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: -# chill_event.example: -# class: Chill\EventBundle\Example -# arguments: [@service_id, "plain_value", %parameter%] diff --git a/Resources/config/services/authorization.yml b/Resources/config/services/authorization.yml new file mode 100644 index 000000000..9adb5b087 --- /dev/null +++ b/Resources/config/services/authorization.yml @@ -0,0 +1,8 @@ +services: + chill_event.event_voter: + class: Chill\EventBundle\Security\Authorization\EventVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: chill.role } + - { name: security.voter } diff --git a/Resources/config/services/repositories.yml b/Resources/config/services/repositories.yml new file mode 100644 index 000000000..44e653cc0 --- /dev/null +++ b/Resources/config/services/repositories.yml @@ -0,0 +1,6 @@ +services: + chill_group.repository.event: + class: Doctrine\ORM\EntityRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\EventBundle\Entity\Event' \ No newline at end of file diff --git a/Resources/config/services/search.yml b/Resources/config/services/search.yml new file mode 100644 index 000000000..99b455bae --- /dev/null +++ b/Resources/config/services/search.yml @@ -0,0 +1,11 @@ +services: + chill_event.search_events: + class: Chill\EventBundle\Search\EventSearch + arguments: + - "@security.token_storage" + - "@chill_group.repository.event" + - "@chill.main.security.authorization.helper" + - "@templating" + tags: + - { name: chill.search, alias: 'event_regular' } + diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml new file mode 100644 index 000000000..d579fd3a2 --- /dev/null +++ b/Resources/translations/messages.fr.yml @@ -0,0 +1,11 @@ +#events +Name: Nom +Date: Date +Event type : Type d'événement +See: Voir + + + +#search +Event search: Recherche d'événements +'%total% events match the search %pattern%' : '{0} Aucun événement ne correspond aux termes de recherche "%pattern%" | {1} Un événement a été trouvé par la recherche "%pattern%" | ]1,Inf] %total% événements correspondent aux termes de recherche "%pattern%".' diff --git a/Resources/views/Event/list.html.twig b/Resources/views/Event/list.html.twig new file mode 100644 index 000000000..9e158b285 --- /dev/null +++ b/Resources/views/Event/list.html.twig @@ -0,0 +1,43 @@ +

{{ 'Event search'|trans }}

+ +

{% transchoice total with { '%pattern%' : pattern } %}%total% events match the search %pattern%{% endtranschoice %}

+ + +{% if events|length > 0 %} + + + + + + + + + + + {% for event in events %} + + + + + + + {% endfor %} + +
{{ 'Name'|trans }}{{ 'Date'|trans }}{{ 'Event type'|trans }} 
{{ event.label }}{{ event.date|localizeddate('long', 'none') }}{{ event.type.label|localize_translatable_string }} + +
+ +{% endif %} \ No newline at end of file diff --git a/Search/EventSearch.php b/Search/EventSearch.php new file mode 100644 index 000000000..c2b4e4e80 --- /dev/null +++ b/Search/EventSearch.php @@ -0,0 +1,143 @@ + + * @author Champs Libres + */ +class EventSearch extends AbstractSearch +{ + + /** + * + * @var EntityRepository + */ + private $er; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + private $user; + + /** + * + * @var AuthorizationHelper + */ + private $helper; + + /** + * + * @var TemplatingEngine + */ + private $templating; + + + public function __construct( + TokenStorageInterface $tokenStorage, + EntityRepository $eventRepository, + AuthorizationHelper $authorizationHelper, + TemplatingEngine $templating + ) + { + $this->user = $tokenStorage->getToken()->getUser(); + $this->er = $eventRepository; + $this->helper = $authorizationHelper; + $this->templating = $templating; + } + + public function supports($domain) + { + return 'event' === $domain or 'events' === $domain; + } + + public function isActiveByDefault() + { + return false; + } + + public function getOrder() + { + return 3000; + } + + public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array()) + { + return $this->templating->render('ChillEventBundle:Event:list.html.twig', + array( + 'events' => $this->search($terms, $start, $limit, $options), + 'pattern' => $this->recomposePattern($terms, array(), $terms['_domain']), + 'total' => $this->count($terms) + )); + } + + protected function search(array $terms, $start, $limit, $options) + { + $qb = $this->er->createQueryBuilder('e'); + $qb->select('e'); + $this->composeQuery($qb, $terms) + ->setMaxResults($limit) + ->setFirstResult($start) + ->orderBy('e.date', 'DESC') + ; + + return $qb->getQuery()->getResult(); + } + + protected function count(array $terms) + { + $qb = $this->er->createQueryBuilder('e'); + $qb->select('COUNT(e)'); + $this->composeQuery($qb, $terms) + ; + + return $qb->getQuery()->getSingleScalarResult(); + } + + protected function composeQuery(QueryBuilder &$qb, $terms) + { + + // add security clauses + $reachableCenters = $this->helper + ->getReachableCenters($this->user, new Role('CHILL_EVENT_SEE')); + + if (count($reachableCenters) === 0) { + // add a clause to block all events + $where = $qb->expr()->isNull('e.center'); + $qb->andWhere($where); + } else { + + $n = 0; + $orWhere = $qb->expr()->orX(); + foreach ($reachableCenters as $center) { + $circles = $this->helper->getReachableScopes($this->user, + new Role('CHILL_EVENT_SEE'), $center); + $where = $qb->expr()->andX( + $qb->expr()->eq('e.center', ':center_'.$n), + $qb->expr()->in('e.circle', ':circle_'.$n) + ); + $qb->setParameter('center_'.$n, $center); + $qb->setParameter('circle_'.$n, $circles); + $orWhere->add($where); + } + + $qb->andWhere($orWhere); + } + + + + return $qb; + } +} diff --git a/Security/Authorization/EventVoter.php b/Security/Authorization/EventVoter.php new file mode 100644 index 000000000..23fcccf28 --- /dev/null +++ b/Security/Authorization/EventVoter.php @@ -0,0 +1,63 @@ + + * @author Champs Libres + */ +class EventVoter extends AbstractChillVoter implements ProvideRoleInterface +{ + + const SEE = 'CHILL_EVENT_SEE'; + const SEE_DETAILS = 'CHILL_EVENT_SEE_DETAILS'; + const CREATE = 'CHILL_EVENT_CREATE'; + const UPDATE = 'CHILL_EVENT_UPDATE'; + + protected $authorizationHelper; + + public function __construct(AuthorizationHelper $helper) + { + $this->authorizationHelper = $helper; + } + + protected function getSupportedAttributes() + { + return array(self::SEE, self::SEE_DETAILS, + self::CREATE, self::UPDATE); + } + + protected function getSupportedClasses() + { + return array(Event::class); + } + + protected function isGranted($attribute, $event, $user = null) + { + if (!$user instanceof User) { + return false; + } + + return $this->helper->userHasAccess($user, $event, $attribute); + } + + public function getRoles() + { + return $this->getSupportedAttributes(); + } + + public function getRolesWithoutScope() + { + return null; + } + +}