From 8ac41a15b6cb5c771a13962f4a974f11e54e2ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 30 Jun 2015 14:38:08 +0200 Subject: [PATCH] Merge branch add_acl ref #263 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit e1236655e1514fd207818aeb57789eca0d949453 Merge: c0b349b fb15bd3 Author: Julien Fastré Date: Tue Jun 30 09:51:35 2015 +0200 Merge remote-tracking branch 'origin/master' into add_acl In order to prepare merging of add_acl to master Conflicts: composer.json commit c0b349bb5f31fe79c84f82d4dd6658c9e90ef728 Author: Julien Fastré Date: Tue Jun 30 00:40:13 2015 +0200 fix infos in composer.json [ci skip] commit 106bbf56a5060efd2a89232f278692eeb57e3092 Author: Julien Fastré Date: Mon Jun 29 22:58:48 2015 +0200 add username and password to client auth options [ci skip] commit c4990972711850616aa1426394884223d63b504f Author: Julien Fastré Date: Mon Jun 29 22:22:20 2015 +0200 fix quoting in timelinebuilder commit 1db7cbea5a0fb0e8d396f8c9d8dc01240b47e96f Author: Julien Fastré Date: Thu Jun 25 22:43:46 2015 +0200 remove data_class to allow edit form commit 7c999279310b5e2b9ecef8a9b5001c71910a822d Author: Julien Fastré Date: Wed Jun 24 22:33:42 2015 +0200 fix doc for AppendScopeChoiceTypeTrait commit 839d4c43bf6f463e705b47d5dbc0cdf7853db0b6 Author: Julien Fastré Date: Wed Jun 24 22:30:13 2015 +0200 refactor: move scope field to a trait Example usage : ``` class AbcType extends Symfony\Component\Form\AbstractType { use AppendScopeChoiceTypeTrait; protected $authorizationHelper; protected $translatableStringHelper; protected $user; public function __construct(AuthorizationHelper $helper, TokenStorageInterface $tokenStorage, TranslatableStringHelper $translatableStringHelper) { $this->authorizationHelper = $helper; $this->user = $tokenStorage->getToken()->getUser(); $this->translatableStringHelper = $translatableStringHelper; } public function buildForm(FormBuilder $builder, array $options) { // ... add your form there // append the scope using FormEvents: PRE_SET_DATA $this->appendScopeChoices($builder, $options['role'], $options['center'], $this->user, $this->authorizationHelper, $this->translatableStringHelper); } public function configureOptions(OptionsResolver $resolver) { // ... add your options // add an option 'role' and 'center' to your form (optional) $this->appendScopeChoicesOptions($resolver); } } ``` [ci skip] commit a1ac530f343146eee12b5982e4e6fceb6dc1da66 Author: Julien Fastré Date: Wed Jun 24 00:24:30 2015 +0200 remove unused statements commit 74f0a4ce5dfdfa4f8fc39ce1cfe726d945e9bdec Author: Julien Fastré Date: Wed Jun 24 00:09:47 2015 +0200 add missing unused statement commit b3a49f2de8758b51c57af6ff0437f30b2fdef72e Author: Julien Fastré Date: Tue Jun 23 23:58:57 2015 +0200 remove ScopeType strategy and fix autorization helper commit aaa70b5eeae76b0950110f51b8274e08bef10576 Author: Julien Fastré Date: Tue Jun 23 21:35:06 2015 +0200 create scope type commit 8f5b2b23c9448b8c8e752e46bdd7f9f721054a98 Author: Julien Fastré Date: Fri Jun 19 18:13:54 2015 +0200 add getREachableCenter method + test on Autho.Helper commit ab2ccb8c287f9aef12912ea9b7f5dc4998209d77 Author: Julien Fastré Date: Fri Jun 19 17:27:22 2015 +0200 remove debug information [ci skip] commit 9d481c07966a5d769d1418bb366bd6b1ea9d4f76 Author: Julien Fastré Date: Fri Jun 19 17:24:57 2015 +0200 fix role hierarchy Now we test effectively that a user has access, not that a role may grant access commit f4b17d0ae398fd6c26d4907377761e9f15d36a90 Author: Julien Fastré Date: Thu Jun 18 02:08:55 2015 +0200 fix trait conflicts traits does not share the same instance of prophet any more commit baac8ce97ba0acd4c4fe6f03278f7a0d30da28ad Author: Julien Fastré Date: Thu Jun 18 01:56:39 2015 +0200 try to fix trait error in zend strict mode (used by travis) commit 7b9fa4b14bde72c7036a29c03a0c6cca3c7e6c74 Author: Julien Fastré Date: Thu Jun 18 01:24:09 2015 +0200 fix error on trait hierarchy (should be) commit f8b3451089f7017653bf8d280b640356df6a1841 Author: Julien Fastré Date: Thu Jun 18 01:03:04 2015 +0200 add userHasAccess method to AuthorizationHelper This method may be used in voter to check access. It supports both hasCenterInterface and HasScopeInterface and check all required permission. commit 9ad9f624a0aa73ca2e639fd68fe4d5559da2bbd7 Author: Julien Fastré Date: Thu Jun 18 01:01:16 2015 +0200 add utilities to generate prophesized entities entities - User (with permissions) - Center - Scope may be generated by trait/methods This ease test writing about acl commit 16008b9e64bb7f551319abea494db1c1c5a12b82 Author: Julien Fastré Date: Fri Jun 12 22:42:46 2015 +0200 add test to CenterType commit 55e2c64aba9714caf09df8dbc9595980826b296a Author: Julien Fastré Date: Fri Jun 12 20:50:05 2015 +0200 first implementation of test on CenterType [TRANSFER][ci skip] commit 548fb24927cc470794a51d81f1ba6af52237363f Author: Julien Fastré Date: Wed Jun 10 21:15:52 2015 +0200 add center type the center type is hidden if the current user can reach only one center, and is `entity` type if the user can reach multiple centers commit 024e3ef8d969d25560406864d86768a260ef4402 Author: Julien Fastré Date: Mon Jun 8 00:28:51 2015 +0200 add first impl of Access Model - first classes and interfaces - authorizationHelper + test - rewrite loadUser to have multi-center commit bc5ae70c83c39a0a738e78313e33020fc284f456 Author: Julien Fastré Date: Sun Jun 7 22:11:13 2015 +0200 make deprecations message not fail tests commit ab9308ed62e45171e9d355ec033b0862b9274e07 Author: Julien Fastré Date: Sat Jun 6 10:19:19 2015 +0200 introducting phpunit-bridge to handle deprecation warnings commit 5b7a43c4d058af58578c30120670c5be5b11cfd8 Author: Julien Fastré Date: Fri Jun 5 12:49:23 2015 +0200 fix options resolver deprecation commit a5b4e5743f790c16dfd04e5068c5ecca9c2b1583 Author: Julien Fastré Date: Fri Jun 5 12:36:17 2015 +0200 remove warning about deprecation in phpunit commit 56621767936df1ea1293c49ae06380d7735c8d6c Author: Julien Fastré Date: Fri Jun 5 11:59:50 2015 +0200 fix pattern deprecation in routing/test pattern=> path commit 17d40fc5294b245d7377572a8c918abab2484985 Author: Julien Fastré Date: Fri Jun 5 11:40:59 2015 +0200 fix deprecation of pattern in routing pattern => path in routing commit 6a33752c6439bd9dbc707df010ce1d8765eeb5fe Author: Julien Fastré Date: Fri Jun 5 11:21:18 2015 +0200 fix twig.form.resources deprecation the new key is twig.form_themes commit adf03eb819f2d443f87d7b744f59a5adf51d2b31 Author: Julien Fastré Date: Fri Jun 5 10:56:51 2015 +0200 switch to symfony 2.7 [ci-skip] --- DataFixtures/ORM/LoadUsers.php | 61 ++- DependencyInjection/ChillMainExtension.php | 3 +- DependencyInjection/TimelineCompilerClass.php | 1 - Entity/Center.php | 5 + Entity/Country.php | 2 - Entity/HasCenterInterface.php | 36 ++ Entity/HasScopeInterface.php | 35 ++ Form/Type/AppendScopeChoiceTypeTrait.php | 153 ++++++ Form/Type/CenterType.php | 138 ++++++ .../DataTransformer/CenterTransformer.php | 70 +++ .../Type/DataTransformer/ScopeTransformer.php | 77 +++ Form/Type/Select2ChoiceType.php | 6 +- Form/Type/Select2CountryType.php | 1 - Form/Type/Select2LanguageType.php | 1 - Resources/config/routing.yml | 12 +- Resources/config/services.yml | 18 + Routing/MenuComposer.php | 1 - Security/Authorization/AbstractChillVoter.php | 36 ++ .../Authorization/AuthorizationHelper.php | 209 ++++++++ .../Authorization/ChillVoterInterface.php | 30 ++ Test/PrepareCenterTrait.php | 59 +++ Test/PrepareScopeTrait.php | 57 +++ Test/PrepareUserTrait.php | 85 ++++ Test/ProphecyTrait.php | 53 +++ Tests/Fixtures/App/config/config_test.yml | 2 + Tests/Fixtures/App/config/routing.yml | 6 +- Tests/Form/Type/CenterTypeTest.php | 153 ++++++ .../Authorization/AuthorizationHelperTest.php | 448 ++++++++++++++++++ Tests/TestHelper.php | 7 +- Timeline/TimelineBuilder.php | 4 +- composer.json | 19 +- phpunit.xml.dist | 1 + 32 files changed, 1739 insertions(+), 50 deletions(-) create mode 100644 Entity/HasCenterInterface.php create mode 100644 Entity/HasScopeInterface.php create mode 100644 Form/Type/AppendScopeChoiceTypeTrait.php create mode 100644 Form/Type/CenterType.php create mode 100644 Form/Type/DataTransformer/CenterTransformer.php create mode 100644 Form/Type/DataTransformer/ScopeTransformer.php create mode 100644 Security/Authorization/AbstractChillVoter.php create mode 100644 Security/Authorization/AuthorizationHelper.php create mode 100644 Security/Authorization/ChillVoterInterface.php create mode 100644 Test/PrepareCenterTrait.php create mode 100644 Test/PrepareScopeTrait.php create mode 100644 Test/PrepareUserTrait.php create mode 100644 Test/ProphecyTrait.php create mode 100644 Tests/Form/Type/CenterTypeTest.php create mode 100644 Tests/Security/Authorization/AuthorizationHelperTest.php diff --git a/DataFixtures/ORM/LoadUsers.php b/DataFixtures/ORM/LoadUsers.php index 6bed52f5e..16d02ba4a 100644 --- a/DataFixtures/ORM/LoadUsers.php +++ b/DataFixtures/ORM/LoadUsers.php @@ -32,31 +32,54 @@ class LoadUsers extends AbstractFixture implements OrderedFixtureInterface, Cont return 1000; } - public static $refs = array(); + public static $refs = array( + 'center a_social' => array( + 'groupCenterRefs' => ['centerA_permission_group_social'] + ), + 'center a_administrative' => array( + 'groupCenterRefs' => ['centerA_permission_group_administrative'] + ), + 'center a_direction' => array( + 'groupCenterRefs' => ['centerA_permission_group_direction'] + ), + 'center b_social' => array( + 'groupCenterRefs' => ['centerB_permission_group_social'] + ), + 'center b_administrative' => array( + 'groupCenterRefs' => ['centerB_permission_group_administrative'] + ), + 'center b_direction' => array( + 'groupCenterRefs' => ['centerB_permission_group_direction'] + ), + 'multi_center' => array( + 'groupCenterRefs' => ['centerA_permission_group_social', + 'centerB_permission_group_social'] + ) + + ); public function load(ObjectManager $manager) { - foreach(LoadCenters::$refs as $centerRef) { - foreach(LoadPermissionsGroup::$refs as $permissionGroupRef) { - $user = new User(); - - $permissionGroup = $this->getReference($permissionGroupRef); - $center = $this->getReference($centerRef); - $username = strtolower($center->getName().'_'.$permissionGroup->getName()); - - $user->setUsername($username) - ->setPassword($this->container->get('security.encoder_factory') + foreach (self::$refs as $username => $params) { + + $user = new User(); + + $user->setUsername($username) + ->setPassword( + $this->container->get('security.encoder_factory') ->getEncoder($user) - ->encodePassword('password', $user->getSalt())); - $user->addGroupCenter($this->getReference($centerRef.'_'.$permissionGroupRef)); - - $manager->persist($user); - $this->addReference($username, $user); - static::$refs[] = $user->getUsername(); - echo "Creating user with username '".$user->getUsername()."' and password 'password'.. \n"; + ->encodePassword('password', $user->getSalt()) + ); + + foreach ($params['groupCenterRefs'] as $groupCenterRef) { + $user->addGroupCenter($this->getReference($groupCenterRef)); } + + echo 'Creating user ' . $username ."... \n"; + $manager->persist($user); + $this->addReference($username, $user); } - + $manager->flush(); } diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php index 2a310b38e..b66425ec4 100644 --- a/DependencyInjection/ChillMainExtension.php +++ b/DependencyInjection/ChillMainExtension.php @@ -58,8 +58,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface 'name' => $config['installation_name']), 'available_languages' => $config['available_languages'] ), - 'form' => array( - 'resources' => array('ChillMainBundle:Form:fields.html.twig')) + 'form_themes' => array('ChillMainBundle:Form:fields.html.twig') ); $container->prependExtensionConfig('twig', $twigConfig); diff --git a/DependencyInjection/TimelineCompilerClass.php b/DependencyInjection/TimelineCompilerClass.php index 1cb06e0c7..92aaaa003 100644 --- a/DependencyInjection/TimelineCompilerClass.php +++ b/DependencyInjection/TimelineCompilerClass.php @@ -21,7 +21,6 @@ namespace Chill\MainBundle\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; /** * Add services taggued with `name: chill.timeline` to diff --git a/Entity/Center.php b/Entity/Center.php index fddb54441..a526c2468 100644 --- a/Entity/Center.php +++ b/Entity/Center.php @@ -76,6 +76,11 @@ class Center $this->groupCenters->add($groupCenter); return $this; } + + public function __toString() + { + return $this->getName(); + } } diff --git a/Entity/Country.php b/Entity/Country.php index 96e50b4e3..016e6ad09 100644 --- a/Entity/Country.php +++ b/Entity/Country.php @@ -2,8 +2,6 @@ namespace Chill\MainBundle\Entity; -use Doctrine\ORM\Mapping as ORM; - /** * Country */ diff --git a/Entity/HasCenterInterface.php b/Entity/HasCenterInterface.php new file mode 100644 index 000000000..8d6d541d8 --- /dev/null +++ b/Entity/HasCenterInterface.php @@ -0,0 +1,36 @@ + + * + * 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\MainBundle\Entity; + +/** + * Interface for entities which may be linked to a center + * + * + * @author Julien Fastré + */ +interface HasCenterInterface +{ + /** + * the linked center + * + * @return Center + */ + public function getCenter(); +} diff --git a/Entity/HasScopeInterface.php b/Entity/HasScopeInterface.php new file mode 100644 index 000000000..344d58557 --- /dev/null +++ b/Entity/HasScopeInterface.php @@ -0,0 +1,35 @@ + + * + * 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\MainBundle\Entity; + +/** + * Interface for entities which have a scop + * + * @author Julien Fastré + */ +interface HasScopeInterface +{ + /** + * Return the linked scope + * + * @return Scope + */ + public function getScope(); +} diff --git a/Form/Type/AppendScopeChoiceTypeTrait.php b/Form/Type/AppendScopeChoiceTypeTrait.php new file mode 100644 index 000000000..ddf13a7ba --- /dev/null +++ b/Form/Type/AppendScopeChoiceTypeTrait.php @@ -0,0 +1,153 @@ + + * + * 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\MainBundle\Form\Type; + +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\FormBuilderInterface; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormEvent; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Center; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Form\Type\DataTransformer\ScopeTransformer; + +/** + * Trait to add an input with reachable scope for a given center and role. + * + * Example usage : + * + * ``` + * class AbcType extends Symfony\Component\Form\AbstractType + * { + * use AppendScopeChoiceTypeTrait; + * protected $authorizationHelper; + * protected $translatableStringHelper; + * protected $user; + * protected $om; + * + * public function __construct(AuthorizationHelper $helper, + * TokenStorageInterface $tokenStorage, + * TranslatableStringHelper $translatableStringHelper, + * ObjectManager $om) + * { + * $this->authorizationHelper = $helper; + * $this->user = $tokenStorage->getToken()->getUser(); + * $this->translatableStringHelper = $translatableStringHelper; + * $this->om = $om; + * } + * + * public function buildForm(FormBuilder $builder, array $options) + * { + * // ... add your form there + * + * // append the scope using FormEvents: PRE_SET_DATA + * $this->appendScopeChoices($builder, $options['role'], + * $options['center'], $this->user, $this->authorizationHelper, + * $this->translatableStringHelper, $this->om); + * } + * + * public function configureOptions(OptionsResolver $resolver) + * { + * // ... add your options + * + * // add an option 'role' and 'center' to your form (optional) + * $this->appendScopeChoicesOptions($resolver); + * } + * + * } + * ``` + * + * @author Julien Fastré + * @author Champs Libres + */ +trait AppendScopeChoiceTypeTrait +{ + /** + * Append a scope choice field, with the scopes reachable by given + * user for the given role and center. + * + * The field is added on event FormEvents::PRE_SET_DATA + * + * @param FormBuilderInterface $builder + * @param Role $role + * @param Center $center + * @param User $user + * @param AuthorizationHelper $authorizationHelper + * @param TranslatableStringHelper $translatableStringHelper + * @param string $name + */ + protected function appendScopeChoices(FormBuilderInterface $builder, + Role $role, Center $center, User $user, + AuthorizationHelper $authorizationHelper, + TranslatableStringHelper $translatableStringHelper, + ObjectManager $om, $name = 'scope') + { + $reachableScopes = $authorizationHelper + ->getReachableScopes($user, $role, $center); + + $choices = array(); + foreach($reachableScopes as $scope) { + $choices[$scope->getId()] = $translatableStringHelper + ->localize($scope->getName()); + } + + $dataTransformer = new ScopeTransformer($om); + + $builder->addEventListener(FormEvents::PRE_SET_DATA, + function (FormEvent $event) use ($choices, $name, $dataTransformer, $builder) { + $form = $event->getForm(); + $form->add( + $builder + ->create($name, 'choice', array( + 'choices' => $choices, + 'auto_initialize' => false + ) + ) + ->addModelTransformer($dataTransformer) + ->getForm() + ); + }); + } + + /** + * Append a `role` and `center` option to the form. + * + * The allowed types are : + * - Chill\MainBundle\Entity\Center for center + * - Symfony\Component\Security\Core\Role\Role for role + * + * @param OptionsResolver $resolver + */ + public function appendScopeChoicesOptions(OptionsResolver $resolver) + { + $resolver + ->setRequired(array('center', 'role')) + ->setAllowedTypes(array( + 'center' => 'Chill\MainBundle\Entity\Center', + 'role' => 'Symfony\Component\Security\Core\Role\Role' + )) + ; + } + +} diff --git a/Form/Type/CenterType.php b/Form/Type/CenterType.php new file mode 100644 index 000000000..19ca1f41d --- /dev/null +++ b/Form/Type/CenterType.php @@ -0,0 +1,138 @@ + + * + * 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\MainBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer; + +/** + * + * + * @author Julien Fastré + */ +class CenterType extends AbstractType +{ + /** + * The user linked with this type. + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + /** + * associative array where keys are center.id and + * value are center objects + * + * @var Center[] + */ + protected $reachableCenters = array(); + + /** + * + * @var CenterTransformer + */ + protected $transformer; + + public function __construct(TokenStorage $tokenStorage, + CenterTransformer $transformer) + { + $this->user = $tokenStorage->getToken()->getUser(); + $this->transformer = $transformer; + $this->prepareReachableCenterByUser(); + } + + public function getName() + { + return 'center'; + } + + /** + * return a 'hidden' field if only one center is available. + * + * Return a 'choice' field if more than one center is available. + * + * @return string + * @throws \RuntimeException if the user is not associated with any center + */ + public function getParent() + { + $nbReachableCenters = count($this->reachableCenters); + + if ($nbReachableCenters === 0) { + throw new \RuntimeException("The user is not associated with " + . "any center. Associate user with a center"); + } elseif ($nbReachableCenters === 1) { + return 'hidden'; + } else { + return 'entity'; + } + } + + /** + * configure default options, i.e. add choices if user can reach multiple + * centers. + * + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + if (count($this->reachableCenters) > 1) { + $resolver->setDefault('class', 'Chill\MainBundle\Entity\Center'); + } + } + + /** + * add a data transformer if user can reach only one center + * + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($this->getParent() === 'hidden') { + $builder->addModelTransformer($this->transformer); + } + } + + /** + * populate reachableCenters as an associative array where + * keys are center.id and value are center entities. + * + */ + private function prepareReachableCenterByUser() + { + $groupCenters = $this->user->getGroupCenters(); + + foreach ($groupCenters as $groupCenter) { + + $center = $groupCenter->getCenter(); + + if (!array_key_exists($center->getId(), + $this->reachableCenters)) { + $this->reachableCenters[$center->getId()] = $center; + } + } + } + +} diff --git a/Form/Type/DataTransformer/CenterTransformer.php b/Form/Type/DataTransformer/CenterTransformer.php new file mode 100644 index 000000000..a1930cf85 --- /dev/null +++ b/Form/Type/DataTransformer/CenterTransformer.php @@ -0,0 +1,70 @@ + + * + * 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\MainBundle\Form\Type\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Transform a center object to his id, and vice-versa + * + * @author Julien Fastré + */ +class CenterTransformer implements DataTransformerInterface +{ + /** + * + * @var ObjectManager + */ + private $om; + + public function __construct(ObjectManager $om) + { + $this->om = $om; + } + + public function reverseTransform($id) + { + if ($id === NULL) { + return NULL; + } + + $center = $this->om->getRepository('ChillMainBundle:Center') + ->find($id); + + if ($center === NULL) { + throw new TransformationFailedException(sprintf( + 'No center found with id %d', $id)); + } + + return $center; + } + + public function transform($center) + { + if ($center === NULL) { + return ''; + } + + return $center->getId(); + } + +} diff --git a/Form/Type/DataTransformer/ScopeTransformer.php b/Form/Type/DataTransformer/ScopeTransformer.php new file mode 100644 index 000000000..ca53b85b0 --- /dev/null +++ b/Form/Type/DataTransformer/ScopeTransformer.php @@ -0,0 +1,77 @@ + + * + * 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\MainBundle\Form\Type\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Chill\MainBundle\Templating\TranslatableStringHelper; + +/** + * + * + * @author Julien Fastré + */ +class ScopeTransformer implements DataTransformerInterface +{ + /** + * + * @var ObjectManager + */ + protected $om; + + /** + * + * @var TranslatableStringHelper + */ + protected $helper; + + public function __construct(ObjectManager $om) + { + $this->om = $om; + } + + public function transform($scope) + { + if ($scope === NULL) { + return NULL; + } + + return $scope->getId(); + } + + public function reverseTransform($id) + { + if ($id == NULL) { + return NULL; + } + + $scope = $this->om->getRepository('ChillMainBundle:Scope') + ->find($id); + + if ($scope === NULL) { + throw new TransformationFailedException(sprintf("The scope with id " + . "'%d' were not found", $id)); + } + + return $scope; + } + +} diff --git a/Form/Type/Select2ChoiceType.php b/Form/Type/Select2ChoiceType.php index 263a6f24c..3c1903539 100644 --- a/Form/Type/Select2ChoiceType.php +++ b/Form/Type/Select2ChoiceType.php @@ -21,7 +21,7 @@ namespace Chill\MainBundle\Form\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** * Extends choice to allow adding select2 library on widget @@ -40,9 +40,9 @@ class Select2ChoiceType extends AbstractType return 'choice'; } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { - $resolver->replaceDefaults( + $resolver->setDefaults( array('attr' => array('class' => 'select2 ')) ); } diff --git a/Form/Type/Select2CountryType.php b/Form/Type/Select2CountryType.php index cdd449739..b29fbbcb6 100644 --- a/Form/Type/Select2CountryType.php +++ b/Form/Type/Select2CountryType.php @@ -22,7 +22,6 @@ namespace Chill\MainBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Chill\MainBundle\Entity\Country; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer; diff --git a/Form/Type/Select2LanguageType.php b/Form/Type/Select2LanguageType.php index 6b0b88ce5..51edc44ca 100644 --- a/Form/Type/Select2LanguageType.php +++ b/Form/Type/Select2LanguageType.php @@ -22,7 +22,6 @@ namespace Chill\MainBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Chill\MainBundle\Entity\Language; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Form\Type\DataTransformer\MultipleObjectsToIdTransformer; diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index b59abfb66..438c7cccf 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,16 +1,16 @@ root: - pattern: / + path: / defaults: _controller: FrameworkBundle:Redirect:urlRedirect path: /homepage permanent: true chill_main_homepage_without_locale: - pattern: /homepage + path: /homepage defaults: { _controller: ChillMainBundle:Default:indexWithoutLocale } chill_main_homepage: - pattern: /{_locale}/homepage + path: /{_locale}/homepage defaults: { _controller: ChillMainBundle:Default:index } options: menus: @@ -20,7 +20,7 @@ chill_main_homepage: icons: [home] chill_main_export_index: - pattern: /{_locale}/export + path: /{_locale}/export defaults: { _controller: ChillMainBundle:Export:index } options: menus: @@ -30,7 +30,7 @@ chill_main_export_index: icons: [upload] chill_main_admin_central: - pattern: /{_locale}/admin + path: /{_locale}/admin defaults: { _controller: ChillMainBundle:Admin:index } options: menus: @@ -40,7 +40,7 @@ chill_main_admin_central: icons: [gears] chill_main_search: - pattern: /{_locale}/search + path: /{_locale}/search defaults: { _controller: ChillMainBundle:Search:search } login: diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 87523bd26..6f1d6fb73 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -81,6 +81,19 @@ services: - "@doctrine.orm.entity_manager" tags: - { name: form.type, alias: select2_chill_language } + + chill.main.form.type.center: + class: Chill\MainBundle\Form\Type\CenterType + arguments: + - "@security.token_storage" + - "@chill.main.form.data_transformer.center_transformer" + tags: + - { name: form.type, alias: center } + + chill.main.form.data_transformer.center_transformer: + class: Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer + arguments: + - "@doctrine.orm.entity_manager" chill.main.search_provider: class: Chill\MainBundle\Search\SearchProvider @@ -91,3 +104,8 @@ services: - "@doctrine.orm.entity_manager" calls: - [ setContainer, ["@service_container"]] + + chill.main.security.authorization.helper: + class: Chill\MainBundle\Security\Authorization\AuthorizationHelper + arguments: + - "@security.role_hierarchy" diff --git a/Routing/MenuComposer.php b/Routing/MenuComposer.php index 0df3e9551..4143231ad 100644 --- a/Routing/MenuComposer.php +++ b/Routing/MenuComposer.php @@ -2,7 +2,6 @@ namespace Chill\MainBundle\Routing; -use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/Security/Authorization/AbstractChillVoter.php b/Security/Authorization/AbstractChillVoter.php new file mode 100644 index 000000000..ac291c673 --- /dev/null +++ b/Security/Authorization/AbstractChillVoter.php @@ -0,0 +1,36 @@ + + * + * 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\MainBundle\Security\Authorization; + +use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; + +/** + * Voter for Chill software. + * + * This abstract Voter provide generic methods to handle object specific to Chill + * + * + * @author Julien Fastré + */ +abstract class AbstractChillVoter extends AbstractVoter implements ChillVoterInterface +{ + + +} diff --git a/Security/Authorization/AuthorizationHelper.php b/Security/Authorization/AuthorizationHelper.php new file mode 100644 index 000000000..51e2e196a --- /dev/null +++ b/Security/Authorization/AuthorizationHelper.php @@ -0,0 +1,209 @@ + + * + * 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\MainBundle\Security\Authorization; + +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\HasCenterInterface; +use Chill\MainBundle\Entity\HasScopeInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Entity\Scope; + +/** + * Helper for authorizations. + * + * Provides methods for user and entities information. + * + * @author Julien Fastré + */ +class AuthorizationHelper +{ + /** + * + * @var RoleHierarchyInterface + */ + protected $roleHierarchy; + + protected $existingRoles = array('CHILL_MASTER_ROLE', 'CHILL_PERSON_SEE', + 'CHILL_PERSON_UPDATE',); + + public function __construct(RoleHierarchyInterface $roleHierarchy) + { + $this->roleHierarchy = $roleHierarchy; + } + + /** + * Determines if a user is active on this center + * + * @param User $user + * @param Center $center + * @return bool + */ + public function userCanReachCenter(User $user, Center $center) + { + foreach ($user->getGroupCenters() as $groupCenter) { + if ($center->getId() === $groupCenter->getCenter()->getId()) { + + return true; + } + } + + return false; + } + + /** + * + * Determines if the user has access to the given entity. + * + * if the entity implements Chill\MainBundle\Entity\HasScopeInterface, + * the scope is taken into account. + * + * @param User $user + * @param HasCenterInterface $entity the entity may also implement HasScopeInterface + * @param string|Role $attribute + * @return boolean true if the user has access + */ + public function userHasAccess(User $user, HasCenterInterface $entity, $attribute) + { + + $center = $entity->getCenter(); + + if (!$this->userCanReachCenter($user, $center)) { + return false; + } + + $role = ($attribute instanceof Role) ? $attribute : new Role($attribute); + + foreach ($user->getGroupCenters() as $groupCenter){ + //filter on center + if ($groupCenter->getCenter()->getId() === $entity->getCenter()->getId()) { + //iterate on permissionGroup + foreach($groupCenter->getPermissionGroups() as $permissionGroup) { + //iterate on roleScopes + foreach($permissionGroup->getRoleScopes() as $roleScope) { + //check that the role allow to reach the required role + if ($this->isRoleReached($role, + new Role($roleScope->getRole()))){ + //if yes, we have a right on something... + // perform check on scope if necessary + if ($entity instanceof HasScopeInterface) { + $scope = $entity->getScope(); + if ($scope->getId() === $roleScope + ->getScope()->getId()) { + return true; + } + } else { + return true; + } + } + } + } + } + } + + return false; + } + + /** + * Get reachable Centers for the given user, role, + * and optionnaly Scope + * + * @param User $user + * @param Role $role + * @param null|Scope $scope + * @return Center[] + */ + public function getReachableCenters(User $user, Role $role, Scope $scope = null) + { + $centers = array(); + + foreach ($user->getGroupCenters() as $groupCenter){ + //iterate on permissionGroup + foreach($groupCenter->getPermissionGroups() as $permissionGroup) { + //iterate on roleScopes + foreach($permissionGroup->getRoleScopes() as $roleScope) { + //check that the role is in the reachable roles + if ($this->isRoleReached($role, + new Role($roleScope->getRole()))) { + if ($scope === null) { + $centers[] = $groupCenter->getCenter(); + break 2; + } else { + if ($scope->getId() == $roleScope->getScope()->getId()){ + $centers[] = $groupCenter->getCenter(); + break 2; + } + } + } + } + } + } + + return $centers; + } + + /** + * Return all reachable scope for a given user, center and role + * + * @param User $user + * @param Role $role + * @param Center $center + * @return Scope[] + */ + public function getReachableScopes(User $user, Role $role, Center $center) + { + $scopes = array(); + + foreach ($user->getGroupCenters() as $groupCenter){ + if ($center->getId() === $groupCenter->getCenter()->getId()) { + //iterate on permissionGroup + foreach($groupCenter->getPermissionGroups() as $permissionGroup) { + //iterate on roleScopes + foreach($permissionGroup->getRoleScopes() as $roleScope) { + //check that the role is in the reachable roles + if ($this->isRoleReached($role, + new Role($roleScope->getRole()))) { + + $scopes[] = $roleScope->getScope(); + } + } + } + } + } + + return $scopes; + } + + /** + * Test if a parent role may give access to a given child role + * + * @param Role $childRole The role we want to test if he is reachable + * @param Role $parentRole The role which should give access to $childRole + * @return boolean true if the child role is granted by parent role + */ + protected function isRoleReached(Role $childRole, Role $parentRole) + { + $reachableRoles = $this->roleHierarchy + ->getReachableRoles([$parentRole]); + + return in_array($childRole, $reachableRoles); + } +} diff --git a/Security/Authorization/ChillVoterInterface.php b/Security/Authorization/ChillVoterInterface.php new file mode 100644 index 000000000..3e89d2595 --- /dev/null +++ b/Security/Authorization/ChillVoterInterface.php @@ -0,0 +1,30 @@ + + * + * 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\MainBundle\Security\Authorization; + +/** + * Provides methods for compiling voter and build admin role fields. + * + * @author Julien Fastré + */ +interface ChillVoterInterface +{ + +} diff --git a/Test/PrepareCenterTrait.php b/Test/PrepareCenterTrait.php new file mode 100644 index 000000000..39d530a29 --- /dev/null +++ b/Test/PrepareCenterTrait.php @@ -0,0 +1,59 @@ + + * + * 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\MainBundle\Test; + + +/** + * A trait to prepare center + * + * **Usage :** You must set up trait with `setUpTrait` before use + * and use tearDownTrait after usage. + * + * @author Julien Fastré + * @codeCoverageIgnore + */ +trait PrepareCenterTrait +{ + + private $centerProphet; + + /** + * prepare a mocked center, with and id and name given + * + * @param int $id + * @param string $name + * @return \Chill\MainBundle\Entity\Center + */ + protected function prepareCenter($id, $name) + { + + if ($this->centerProphet === NULL) { + $this->centerProphet = new \Prophecy\Prophet(); + } + + $center = $this->centerProphet->prophesize(); + $center->willExtend('\Chill\MainBundle\Entity\Center'); + $center->getId()->willReturn($id); + $center->getName()->willReturn($name); + + return $center->reveal(); + } + +} diff --git a/Test/PrepareScopeTrait.php b/Test/PrepareScopeTrait.php new file mode 100644 index 000000000..288e0c39b --- /dev/null +++ b/Test/PrepareScopeTrait.php @@ -0,0 +1,57 @@ + + * + * 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\MainBundle\Test; + +/** + * A trait to prepare center + * + * **Usage :** You must set up trait with `setUpTrait` before use + * and use tearDownTrait after usage. + * + * + * @author Julien Fastré + * @codeCoverageIgnore + */ +trait PrepareScopeTrait +{ + private $scopeProphet; + + /** + * prepare a mocked center, with and id and name given + * + * @param int $id + * @param string $name + * @return \Chill\MainBundle\Entity\Center + */ + protected function prepareScope($id, $name) + { + + if ($this->scopeProphet === NULL) { + $this->scopeProphet = new \Prophecy\Prophet(); + } + + $scope = $this->scopeProphet->prophesize(); + $scope->willExtend('\Chill\MainBundle\Entity\Scope'); + $scope->getId()->willReturn($id); + $scope->getName()->willReturn($name); + + return $scope->reveal(); + } +} diff --git a/Test/PrepareUserTrait.php b/Test/PrepareUserTrait.php new file mode 100644 index 000000000..83a18d2b2 --- /dev/null +++ b/Test/PrepareUserTrait.php @@ -0,0 +1,85 @@ + + * + * 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\MainBundle\Test; + +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\GroupCenter; +use Chill\MainBundle\Entity\RoleScope; +use Chill\MainBundle\Entity\PermissionsGroup; + +/** + * A trait to prepare user with permission. May be used + * within tests. + * + * **Usage : ** You must set up trait with `setUpTrait` before use + * and use tearDownTrait after usage. + * + * + * @author Julien Fastré + * @codeCoverageIgnore + */ +trait PrepareUserTrait +{ + + /** + * prepare a user with correct permissions + * + * Example of permissions: + * ``` + * array( + * array( 'center' => $centerA, 'permissionsGroup' => array( + * [ 'role' => 'CHILL_REPORT_SEE', 'scope' => $scopeA] + * ), + * array( 'center' => $centerB, 'permissionsGroup' => array( + * [ 'role' => 'CHILL_ACTIVITY_UPDATE', 'scope' => $scopeB] + * ) + * ) + * ``` + * Scope must be an int. Scope created have this int as id, and the + * int converted to string as name. + * + * + * @param array $permissions an array of permissions, with key 'center' for the center and key 'attrs' for an array of ['role' => (string), 'scope' => (int)] + * @return User + * @throws \LogicException if the trait is not set up + */ + protected function prepareUser(array $permissions) + { + $user = new User(); + + foreach ($permissions as $permission) { + $groupCenter = (new GroupCenter()) + ->setCenter($permission['center']); + $permissionGroup = new PermissionsGroup(); + foreach ($permission['permissionsGroup'] as $pg) { + + $roleScope = (new RoleScope()) + ->setRole($pg['role']) + ->setScope($pg['scope']); + ; + $permissionGroup->addRoleScope($roleScope); + $groupCenter->addPermissionGroup($permissionGroup); + } + $user->addGroupCenter($groupCenter); + } + + return $user; + } +} diff --git a/Test/ProphecyTrait.php b/Test/ProphecyTrait.php new file mode 100644 index 000000000..d471dd624 --- /dev/null +++ b/Test/ProphecyTrait.php @@ -0,0 +1,53 @@ + + * + * 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\MainBundle\Test; + +/** + * a trait to prepare prophecy + * + * **Usage : ** You must set up trait with `setUpTrait` before use + * and use tearDownTrait after usage. + * + * @author Julien Fastré + * @codeCoverageIgnore + */ +trait ProphecyTrait +{ + + /** + * + * @var \Prophecy\Prophet() + */ + private $prophet; + + /** + * + * @return \Prophecy\Prophet + */ + public function getProphet() + { + if ($this->prophet === NULL) { + $this->prophet = new \Prophecy\Prophet(); + } + + return $this->prophet; + } + +} diff --git a/Tests/Fixtures/App/config/config_test.yml b/Tests/Fixtures/App/config/config_test.yml index 652a26975..18856b3ac 100644 --- a/Tests/Fixtures/App/config/config_test.yml +++ b/Tests/Fixtures/App/config/config_test.yml @@ -7,6 +7,8 @@ framework: storage_id: session.storage.filesystem security: + role_hierarchy: + CHILL_MASTER_ROLE: [CHILL_INHERITED_ROLE_1] providers: users: entity: diff --git a/Tests/Fixtures/App/config/routing.yml b/Tests/Fixtures/App/config/routing.yml index 2333ea6e3..2da2876a4 100644 --- a/Tests/Fixtures/App/config/routing.yml +++ b/Tests/Fixtures/App/config/routing.yml @@ -2,7 +2,7 @@ hello_bundle: resource: "@ChillMainBundle/Resources/config/routing.yml" chill_main_dummy_0: - pattern: /dummy + path: /dummy defaults: { _controller: ChillMainBundle:Default:index } options: menus: @@ -15,7 +15,7 @@ chill_main_dummy_0: label: test dummy 1 chill_main_dummy_1: - pattern: /dummy1 + path: /dummy1 defaults: { _controller: ChillMainBundle:Default:index } options: menus: @@ -25,7 +25,7 @@ chill_main_dummy_1: helper: 'great helper' chill_main_dummy_2: - pattern: /dummy2/{param} + path: /dummy2/{param} defaults: {_controller: ChillMainBundle:Default:index } options: menus: diff --git a/Tests/Form/Type/CenterTypeTest.php b/Tests/Form/Type/CenterTypeTest.php new file mode 100644 index 000000000..8c3d2f290 --- /dev/null +++ b/Tests/Form/Type/CenterTypeTest.php @@ -0,0 +1,153 @@ + + * + * 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\MainBundle\Form\Type; + +use Symfony\Component\Form\Test\TypeTestCase; +use Chill\MainBundle\Form\Type\CenterType; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\GroupCenter; + + +/** + * + * + * @author Julien Fastré + */ +class CenterTypeTest extends TypeTestCase +{ + /** + * Test that a user which can reach only one center + * render as an hidden field + */ + public function testUserCanReachSingleCenter() + { + //prepare user + $center = $this->prepareCenter(1, 'center'); + $groupCenter = (new GroupCenter()) + ->setCenter($center) + ; + $user = (new User()) + ->addGroupCenter($groupCenter); + + $type = $this->prepareType($user); + + $this->assertEquals('hidden', $type->getParent()); + } + + /** + * Test that a user which can reach only one center + * render as an hidden field + */ + public function testUserCanReachMultipleSameCenter() + { + //prepare user + $center = $this->prepareCenter(1, 'center'); + $groupCenterA = (new GroupCenter()) + ->setCenter($center) + ; + $groupCenterB = (new GroupCenter()) + ->setCenter($center) + ; + $user = (new User()) + ->addGroupCenter($groupCenterA) + ->addGroupCenter($groupCenterB); + + $type = $this->prepareType($user); + + $this->assertEquals('hidden', $type->getParent()); + } + + /** + * Test that a user which can reach multiple center + * make CenterType render as "entity" type. + */ + public function testUserCanReachMultipleCenters() + { + //prepare user + $centerA = $this->prepareCenter(1, 'centerA'); + $centerB = $this->prepareCenter(2, 'centerB'); + $groupCenterA = (new GroupCenter()) + ->setCenter($centerA) + ; + $groupCenterB = (new GroupCenter()) + ->setCenter($centerB) + ; + $user = (new User()) + ->addGroupCenter($groupCenterA) + ->addGroupCenter($groupCenterB) + ; + + $type = $this->prepareType($user); + + $this->assertEquals('entity', $type->getParent()); + } + + /** + * prepare a mocked center, with and id and name given + * + * @param int $id + * @param string $name + * @return \Chill\MainBundle\Entity\Center + */ + private function prepareCenter($id, $name) + { + $prophet = new \Prophecy\Prophet; + + $prophecyCenter = $prophet->prophesize(); + $prophecyCenter->willExtend('\Chill\MainBundle\Entity\Center'); + $prophecyCenter->getId()->willReturn($id); + $prophecyCenter->getName()->willReturn($name); + + return $prophecyCenter->reveal(); + } + + + /** + * prepare the type with mocked center transformer and token storage + * + * @param User $user the user for wich the form will be prepared + * @return CenterType + */ + private function prepareType(User $user) + { + $prophet = new \Prophecy\Prophet; + + //create a center transformer + $centerTransformerProphecy = $prophet->prophesize(); + $centerTransformerProphecy + ->willExtend('Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer'); + $transformer = $centerTransformerProphecy->reveal(); + + $tokenProphecy = $prophet->prophesize(); + $tokenProphecy + ->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $tokenProphecy->getUser()->willReturn($user); + $token = $tokenProphecy->reveal(); + + $tokenStorageProphecy = $prophet->prophesize(); + $tokenStorageProphecy + ->willExtend('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage'); + $tokenStorageProphecy->getToken()->willReturn($token); + $tokenStorage = $tokenStorageProphecy->reveal(); + + return new CenterType($tokenStorage, $transformer); + } + +} diff --git a/Tests/Security/Authorization/AuthorizationHelperTest.php b/Tests/Security/Authorization/AuthorizationHelperTest.php new file mode 100644 index 000000000..abfc3c754 --- /dev/null +++ b/Tests/Security/Authorization/AuthorizationHelperTest.php @@ -0,0 +1,448 @@ + + * + * 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\MainBundle\Tests\Security\Authorization; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Chill\MainBundle\Test\PrepareUserTrait; +use Chill\MainBundle\Test\PrepareCenterTrait; +use Chill\MainBundle\Test\PrepareScopeTrait; +use Chill\MainBundle\Test\ProphecyTrait; +use Chill\MainBundle\Entity\User; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Entity\Center; + +/** + * + * + * @author Julien Fastré + */ +class AuthorizationHelperTest extends KernelTestCase +{ + + use PrepareUserTrait, PrepareCenterTrait, PrepareScopeTrait, ProphecyTrait; + + public function setUp() + { + static::bootKernel(); + } + + /** + * + * @return \Chill\MainBundle\Security\Authorization\AuthorizationHelper + */ + private function getAuthorizationHelper() + { + return static::$kernel->getContainer() + ->get('chill.main.security.authorization.helper') + ; + } + + /** + * Test function userCanReach of helper. + * + * A user can reach center => the function should return true. + */ + public function testUserCanReachCenter_UserShouldReach() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'ANY_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + + $this->assertTrue($helper->userCanReachCenter($user, $center)); + } + + /** + * Test function userCanReach of helper + * + * A user can not reachcenter =>W the function should return false + */ + public function testUserCanReachCenter_UserShouldNotReach() + { + $centerA = $this->prepareCenter(1, 'center'); + $centerB = $this->prepareCenter(2, 'centerB'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $centerA, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'ANY_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + + $this->assertFalse($helper->userCanReachCenter($user, $centerB)); + + } + + public function testUserHasAccess_shouldHaveAccess_EntityWithoutScope() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->getCenter()->willReturn($center); + + $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), + 'CHILL_ROLE')); + } + + public function testUserHasAccess_ShouldHaveAccessWithInheritance_EntityWithoutScope() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_MASTER_ROLE'] + ) + ) + )); + + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->getCenter()->willReturn($center); + + $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), + 'CHILL_INHERITED_ROLE_1')); + } + + + public function testuserHasAccess_UserHasNoRole_EntityWithoutScope() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'ANY_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->getCenter()->willReturn($center); + + $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE')); + } + + /** + * test that a user has no access on a entity, but is granted on the same role + * on another center + */ + public function testUserHasAccess_userHasNoRole_UserHasRoleOnAnotherCenter_EntityWithoutScope() + { + $centerA = $this->prepareCenter(1, 'center'); + $centerB = $this->prepareCenter(2, 'centerB'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $centerA, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'ANY_ROLE'] + ), + array( + 'centerB' => $centerB, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'ANY_ROLE'], + ['scope' => $scope, 'role' => 'CHILL_ROLE'] + ) + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->getCenter()->willReturn($centerA); + + $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE')); + } + + public function testtestUserHasAccess_UserShouldHaveAccess_EntityWithScope() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface'); + $entity->getCenter()->willReturn($center); + $entity->getScope()->willReturn($scope); + + $this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE')); + } + + public function testUserHasAccess_UserHasNoRole_EntityWithScope() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface'); + $entity->getCenter()->willReturn($center); + $entity->getScope()->willReturn($scope); + + $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'ANOTHER_ROLE')); + } + + public function testUserHasAccess_UserHasNoCenter_EntityWithScope() + { + $centerA = $this->prepareCenter(1, 'center'); //the user will have this center + $centerB = $this->prepareCenter(2, 'centerB'); //the entity will have another center + $scope = $this->prepareScope(1, 'default'); + $user = $this->prepareUser(array( + array( + 'center' => $centerA, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface'); + $entity->getCenter()->willReturn($centerB); + $entity->getScope()->willReturn($scope); + + $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE')); + } + + public function testUserHasAccess_UserHasNoScope_EntityWithScope() + { + $center = $this->prepareCenter(1, 'center'); + $scopeA = $this->prepareScope(1, 'default'); //the entity will have this scope + $scopeB = $this->prepareScope(2, 'other'); //the user will be granted this scope + $user = $this->prepareUser(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scopeB, 'role' => 'CHILL_ROLE'] + ) + ) + )); + $helper = $this->getAuthorizationHelper(); + $entity = $this->getProphet()->prophesize(); + $entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface'); + $entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface'); + $entity->getCenter()->willReturn($center); + $entity->getScope()->willReturn($scopeA); + + $this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE')); + } + + /** + * + * @dataProvider dataProvider_getReachableCenters + * @param Center $shouldHaveCenter + * @param User $user + * @param Role $role + * @param Scope $scope + */ + public function testGetReachableCenters($test, $result, $msg) + { + $this->assertEquals($test, $result, $msg); + } + + public function dataProvider_getReachableCenters() + { + $this->setUp(); + $centerA = $this->prepareCenter(1, 'center A'); + $centerB = $this->prepareCenter(2, 'center B'); + $scopeA = $this->prepareScope(1, 'scope default'); + $scopeB = $this->prepareScope(2, 'scope B'); + $scopeC = $this->prepareScope(3, 'scope C'); + + $userA = $this->prepareUser(array( + array( + 'center' => $centerA, + 'permissionsGroup' => array( + ['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'], + ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'] + ) + ), + array( + 'center' => $centerB, + 'permissionsGroup' => array( + ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'], + ['scope' => $scopeC, 'role' => 'CHILL_ROLE_2'] + ) + ) + + )); + + $ah = $this->getAuthorizationHelper(); + + return array( + // without scopes + array( + true, + in_array($centerA, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_1'), null)), + 'center A should be available for userA, with role 1 ' + ), + array( + true, + in_array($centerA, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_2'), null)), + 'center A should be available for userA, with role 2 ' + ), + array( + true, + in_array($centerB, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_2'), null)), + 'center A should be available for userA, with role 2 ' + ), + array( + false, + in_array($centerB, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_1'), null)), + 'center B should NOT be available for userA, with role 1 ' + ), + // with scope + array( + true, + in_array($centerA, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_1'), $scopeB)), + 'center A should be available for userA, with role 1, scopeC ' + ), + array( + false, + in_array($centerA, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_2'), $scopeC)), + 'center A should NOT be available for userA, with role 2, scopeA ' + ), + array( + true, + in_array($centerB, $ah->getReachableCenters($userA, + new Role('CHILL_ROLE_2'), $scopeA)), + 'center B should be available for userA, with role 2, scopeA ' + ), + ); + + } + + /** + * + * @dataProvider dataProvider_getReachableScopes + * @param boolean $expectedResult + * @param Scope $testedScope + * @param User $user + * @param Role $role + * @param Center $center + * @param string $message + */ + public function testGetReachableScopes($expectedResult, Scope $testedScope, + User $user, Role $role, Center $center, $message) + { + $reachableScopes = $this->getAuthorizationHelper() + ->getReachableScopes($user, $role, $center); + + $this->assertEquals($expectedResult, in_array($testedScope, $reachableScopes), + $message); + } + + public function dataProvider_getReachableScopes() + { + $centerA = $this->prepareCenter(1, 'center A'); + $centerB = $this->prepareCenter(2, 'center B'); + $scopeA = $this->prepareScope(1, 'scope default'); + $scopeB = $this->prepareScope(2, 'scope B'); + $scopeC = $this->prepareScope(3, 'scope C'); + + $userA = $this->prepareUser(array( + array( + 'center' => $centerA, + 'permissionsGroup' => array( + ['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'], + ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'] + ) + ), + array( + 'center' => $centerB, + 'permissionsGroup' => array( + ['scope' => $scopeA, 'role' => 'CHILL_ROLE_2'], + ['scope' => $scopeC, 'role' => 'CHILL_ROLE_2'], + ['scope' => $scopeB, 'role' => 'CHILL_ROLE_2'] + ) + ) + + )); + + return array( + array( + true, + $scopeA, + $userA, + new Role('CHILL_ROLE_2'), + $centerA, + "Assert that a scope is found within accessible scopes" + ), + array( + false, + $scopeB, + $userA, + new Role('CHILL_ROLE_2'), + $centerA, + "Assert that a scope not reachable is NOT found within accessible scopes" + ), + array( + false, + $scopeB, + $userA, + new Role('CHILL_ROLE_1'), + $centerB, + "Assert that a scope not reachable is not found within accessible scopes." + . " Trying on filter centering" + ) + ); + } + + + +} diff --git a/Tests/TestHelper.php b/Tests/TestHelper.php index a9ca90e71..a969eeeb3 100644 --- a/Tests/TestHelper.php +++ b/Tests/TestHelper.php @@ -36,11 +36,12 @@ class TestHelper * @param WebTestCase $testCase * @return \Symfony\Component\BrowserKit\Client authenticated client */ - public static function getAuthenticatedClientOptions() + public static function getAuthenticatedClientOptions($username = 'center a_social', + $password = 'password') { return array( - 'PHP_AUTH_USER' => 'center a_social', - 'PHP_AUTH_PW' => 'password', + 'PHP_AUTH_USER' => $username, + 'PHP_AUTH_PW' => $password, ); } } diff --git a/Timeline/TimelineBuilder.php b/Timeline/TimelineBuilder.php index 2923c13a1..3e534a280 100644 --- a/Timeline/TimelineBuilder.php +++ b/Timeline/TimelineBuilder.php @@ -150,8 +150,8 @@ class TimelineBuilder implements ContainerAwareInterface $data = $provider->fetchQuery($context, $args); return sprintf( - 'SELECT "%s" AS id, ' - . '"%s" AS "date", ' + 'SELECT %s AS id, ' + . '%s AS "date", ' . "'%s' AS type " . 'FROM %s ' . 'WHERE %s', diff --git a/composer.json b/composer.json index 5d22ebbf1..b88eb9799 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,14 @@ "license": "AGPL-3.0", "type": "symfony-bundle", "description": "The main bundle for the Chill App", - "keywords" : ["chill", "social work"], - "homepage" : "https://github.com/Chill-project/Main", + "keywords" : ["chill", "social work", "software for social service"], + "homepage" : "http://chill.social", + "support": { + "email": "dev@lists.chill.social", + "issues": "https://redmine.champs-libres.coop/projects/chillmain/issues", + "sources": "https://github.com/chill-project/main", + "docs": "http://chill.readthedocs.org" + }, "autoload": { "psr-4": { "Chill\\MainBundle\\": "" } }, @@ -20,9 +26,9 @@ "twig/extensions": "~1.0", "symfony/assetic-bundle": "~2.3", "symfony/monolog-bundle": "~2.4", - "symfony/framework-bundle": "2.5.*", - "symfony/yaml": "2.5.*", - "symfony/symfony": "2.5.*", + "symfony/framework-bundle": "~2.7", + "symfony/yaml": "~2.7", + "symfony/symfony": "~2.7", "doctrine/dbal": "~2.5", "doctrine/orm": "~2.4", "doctrine/common": "~2.4", @@ -34,7 +40,8 @@ "require-dev": { "symfony/dom-crawler": "2.5", "doctrine/doctrine-fixtures-bundle": "~2.2", - "symfony/security": "~2.5" + "symfony/security": "~2.5", + "symfony/phpunit-bridge": "^2.7" }, "scripts": { "post-install-cmd": [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3358647e3..360d89ccd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,5 +18,6 @@ +