diff --git a/Controller/PersonController.php b/Controller/PersonController.php index 82b85caa3..7657b43cd 100644 --- a/Controller/PersonController.php +++ b/Controller/PersonController.php @@ -51,9 +51,13 @@ class PersonController extends Controller $person = $this->_getPerson($person_id); if ($person === null) { - return $this->createNotFoundException("Person with id $person_id not found on this server"); + return $this->createNotFoundException("Person with id $person_id not" + . " found on this server"); } + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person, + "You are not allowed to see this person."); + return $this->render('ChillPersonBundle:Person:view.html.twig', array("person" => $person, "cFGroup" => $this->getCFGroup())); @@ -67,6 +71,9 @@ class PersonController extends Controller return $this->createNotFoundException(); } + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + 'You are not allowed to edit this person'); + $form = $this->createForm(new PersonType(), $person, array( "action" => $this->generateUrl('chill_person_general_update', @@ -87,6 +94,9 @@ class PersonController extends Controller return $this->createNotFoundException(); } + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + 'You are not allowed to edit this person'); + $form = $this->createForm(new PersonType(), $person, array("cFGroup" => $this->getCFGroup())); @@ -120,64 +130,6 @@ class PersonController extends Controller } } - public function searchAction() - { - $q = $this->getRequest()->query->getAlnum('q', ''); - $q = trim($q); - - if ( $q === '' ) { - $this->get('session') - ->getFlashBag() - ->add('info', - $this->get('translator') - ->trans('Your query is empty. Be more explicive') - ); - } - - $em = $this->getDoctrine()->getManager(); - - $offset = $this->getRequest()->query->getInt('offet', 0); - $limit = $this->getRequest()->query->getInt('limit', 30); - - $dql = 'SELECT p FROM ChillPersonBundle:Person p' - . ' WHERE' - . ' LOWER(p.firstName) like LOWER(:q)' - . ' OR LOWER(p.lastName) like LOWER(:q)'; - - if ($this->container->getParameter('cl_chill_person.search.use_double_metaphone')) { - $dql .= ' OR DOUBLEMETAPHONE(p.lastName) = DOUBLEMETAPHONE(:qabsolute)'; - } - - - $query = $em->createQuery($dql) - ->setParameter('q', '%'.$q.'%'); - if ($this->container->getParameter('cl_chill_person.search.use_double_metaphone')) { - $query->setParameter('qabsolute', $q); - } - - // ->setOffset($offset) - // ->setLimit($limit) - $persons = $query->getResult() ; - - - if (count($persons) === 0 ){ - $this->get('session') - ->getFlashBag() - ->add('info', - $this->get('translator') - ->trans('Your query %q% gives no results', array( - '%q%' => $q - )) - ); - } - - return $this->render('ChillPersonBundle:Person:list.html.twig', - array( - 'persons' => $persons, - 'pattern' => $q - )); - } - /** * Return a csv file with all the persons * @@ -199,10 +151,22 @@ class PersonController extends Controller } public function newAction() - { + { + // this is a dummy default center. + $defaultCenter = $this->get('security.token_storage') + ->getToken() + ->getUser() + ->getGroupCenters()[0] + ->getCenter(); + + $person = (new Person()) + ->setCenter($defaultCenter); + $form = $this->createForm( new CreationPersonType(CreationPersonType::FORM_NOT_REVIEWED), - null, array('action' => $this->generateUrl('chill_person_review'))); + array('creation_date' => new \DateTime(), 'center' => $defaultCenter), + array('action' => $this->generateUrl('chill_person_review')) + ); return $this->_renderNewForm($form); } @@ -232,6 +196,7 @@ class PersonController extends Controller ->setLastName($form['lastName']->getData()) ->setGenre($form['genre']->getData()) ->setDateOfBirth($form['dateOfBirth']->getData()) + ->setCenter($form['center']->getData()) ; return $person; @@ -360,6 +325,9 @@ class PersonController extends Controller $errors = $this->_validatePersonAndAccompanyingPeriod($person); + $this->denyAccessUnlessGranted('CHILL_PERSON_CREATE', $person, + 'You are not allowed to create this person'); + if ($errors->count() === 0) { $em = $this->getDoctrine()->getManager(); diff --git a/Controller/TimelinePersonController.php b/Controller/TimelinePersonController.php index 0fb0f1015..95c91df8e 100644 --- a/Controller/TimelinePersonController.php +++ b/Controller/TimelinePersonController.php @@ -40,6 +40,8 @@ class TimelinePersonController extends Controller if ($person === NULL) { throw $this->createNotFoundException(); } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); return $this->render('ChillPersonBundle:Timeline:index.html.twig', array ( diff --git a/DataFixtures/ORM/LoadPeople.php b/DataFixtures/ORM/LoadPeople.php index 5a143af54..24a88a114 100644 --- a/DataFixtures/ORM/LoadPeople.php +++ b/DataFixtures/ORM/LoadPeople.php @@ -118,7 +118,8 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'FirstName' => $firstName, 'LastName' => $lastName, 'Genre' => $sex, - 'Nationality' => (rand(0,100) > 50) ? NULL: 'BE' + 'Nationality' => (rand(0,100) > 50) ? NULL: 'BE', + 'center' => (rand(0,1) == 0) ? 'centerA': 'centerB' ); $this->addAPerson($this->fillWithDefault($person), $manager); @@ -151,18 +152,19 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con foreach ($person as $key => $value) { switch ($key) { case 'CountryOfBirth': - $p->setCountryOfBirth($this->getCountry($value)); + $value = $this->getCountry($value); break; case 'Nationality': - $p->setNationality($this->getCountry($value)); + $value = $this->getCountry($value); break; case 'DateOfBirth': $value = new \DateTime($value); - - - default: - call_user_func(array($p, 'set'.$key), $value); + break; + case 'center': + $value = $this->getReference($value); + break; } + call_user_func(array($p, 'set'.$key), $value); } $manager->persist($p); @@ -210,7 +212,8 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'PlaceOfBirth' => "Châteauroux", 'Genre' => Person::GENRE_MAN, 'CountryOfBirth' => 'FR', - 'Nationality' => 'RU' + 'Nationality' => 'RU', + 'center' => 'centerA' ), array( //to have a person with same firstname as Gérard Depardieu @@ -218,24 +221,28 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con 'LastName' => "Jean", 'DateOfBirth' => "1960-10-12", 'CountryOfBirth' => 'FR', - 'Nationality' => 'FR' + 'Nationality' => 'FR', + 'center' => 'centerA' ), array( //to have a person with same birthdate of Gérard Depardieu 'FirstName' => 'Van Snick', 'LastName' => 'Bart', - 'DateOfBirth' => '1948-12-27' + 'DateOfBirth' => '1948-12-27', + 'center' => 'centerA' ), array( //to have a woman with Depardieu as FirstName 'FirstName' => 'Depardieu', 'LastName' => 'Charline', - 'Genre' => Person::GENRE_WOMAN + 'Genre' => Person::GENRE_WOMAN, + 'center' => 'centerA' ), array( //to have a special character in lastName 'FirstName' => 'Manço', - 'LastName' => 'Étienne' + 'LastName' => 'Étienne', + 'center' => 'centerA' ) ); } diff --git a/DataFixtures/ORM/LoadPersonACL.php b/DataFixtures/ORM/LoadPersonACL.php new file mode 100644 index 000000000..1f4163deb --- /dev/null +++ b/DataFixtures/ORM/LoadPersonACL.php @@ -0,0 +1,79 @@ + + * + * 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\PersonBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Common\Persistence\ObjectManager; +use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; + +/** + * Add a role CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE for all groups except administrative, + * and a role CHILL_PERSON_SEE for administrative + * + * @author Julien Fastré + */ +class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 9600; + } + + + public function load(ObjectManager $manager) + { + foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { + $permissionsGroup = $this->getReference($permissionsGroupRef); + $scope = $this->getReference('scope_all'); + + //create permission group + switch ($permissionsGroup->getName()) { + case 'social': + case 'direction': + printf("Adding CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE to %s permission group \n", $permissionsGroup->getName()); + $roleScopeUpdate = (new RoleScope()) + ->setRole('CHILL_PERSON_UPDATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeUpdate); + $roleScopeCreate = (new RoleScope()) + ->setRole('CHILL_PERSON_CREATE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeCreate); + $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeCreate); + break; + case 'administrative': + printf("Adding CHILL_PERSON_SEE to %s permission group \n", $permissionsGroup->getName()); + $roleScopeSee = (new RoleScope()) + ->setRole('CHILL_PERSON_SEE') + ->setScope($scope); + $permissionsGroup->addRoleScope($roleScopeSee); + $manager->persist($roleScopeSee); + break; + } + + } + + $manager->flush(); + } + +} diff --git a/DependencyInjection/ChillPersonExtension.php b/DependencyInjection/ChillPersonExtension.php index 1dafadcd0..548f3536f 100644 --- a/DependencyInjection/ChillPersonExtension.php +++ b/DependencyInjection/ChillPersonExtension.php @@ -49,6 +49,8 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac public function prepend(ContainerBuilder $container) { + $this->prependRoleHierarchy($container); + $bundles = $container->getParameter('kernel.bundles'); //add ChillMain to assetic-enabled bundles if (!isset($bundles['AsseticBundle'])) { @@ -71,4 +73,14 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac ) )); } + + protected function prependRoleHierarchy(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + 'CHILL_PERSON_UPDATE' => array('CHILL_PERSON_SEE'), + 'CHILL_PERSON_CREATE' => array('CHILL_PERSON_SEE') + ) + )); + } } diff --git a/Entity/Person.php b/Entity/Person.php index 25b0a4804..1d748ffe1 100644 --- a/Entity/Person.php +++ b/Entity/Person.php @@ -25,11 +25,12 @@ namespace Chill\PersonBundle\Entity; use Symfony\Component\Validator\ExecutionContextInterface; use Chill\MainBundle\Entity\Country; use Doctrine\Common\Collections\ArrayCollection; +use Chill\MainBundle\Entity\HasCenterInterface; /** * Person */ -class Person { +class Person implements HasCenterInterface { /** * @var integer */ @@ -60,6 +61,12 @@ class Person { */ private $genre; + /** + * + * @var \Chill\MainBundle\Entity\Center + */ + private $center; + const GENRE_MAN = 'man'; const GENRE_WOMAN = 'woman'; @@ -486,7 +493,30 @@ class Person { public function getLabel() { return $this->getFirstName()." ".$this->getLastName(); } + + /** + * Get center + * + * @return \Chill\MainBundle\Entity\Center + */ + public function getCenter() + { + return $this->center; + } + /** + * Set the center + * + * @param \Chill\MainBundle\Entity\Center $center + * @return \Chill\PersonBundle\Entity\Person + */ + public function setCenter(\Chill\MainBundle\Entity\Center $center) + { + $this->center = $center; + return $this; + } + + /** * Set cFData * diff --git a/Form/CreationPersonType.php b/Form/CreationPersonType.php index 2459e0126..9e878d73a 100644 --- a/Form/CreationPersonType.php +++ b/Form/CreationPersonType.php @@ -66,6 +66,7 @@ class CreationPersonType extends AbstractType ->addModelTransformer($dateToStringTransformer) ) ->add('form_status', 'hidden') + ->add('center', 'center') ; } else { $builder @@ -80,6 +81,7 @@ class CreationPersonType extends AbstractType 'widget' => 'single_text', 'format' => 'dd-MM-yyyy', 'data' => new \DateTime())) ->add('form_status', 'hidden', array('data' => $this->form_status)) + ->add('center', 'center') ; } } diff --git a/Resources/config/doctrine/Person.orm.yml b/Resources/config/doctrine/Person.orm.yml index 1c11cb4cd..bfaab4376 100644 --- a/Resources/config/doctrine/Person.orm.yml +++ b/Resources/config/doctrine/Person.orm.yml @@ -51,6 +51,9 @@ Chill\PersonBundle\Entity\Person: targetEntity: Chill\MainBundle\Entity\Country inversedBy: nationals nullable: true + center: + targetEntity: Chill\MainBundle\Entity\Center + nullable: false oneToMany: accompanyingPeriods: targetEntity: AccompanyingPeriod diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index aba288ba7..b4e825dc0 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -1,5 +1,5 @@ chill_person_view: - pattern: /{_locale}/person/{person_id}/general + path: /{_locale}/person/{person_id}/general defaults: { _controller: ChillPersonBundle:Person:view } options: menus: @@ -8,15 +8,15 @@ chill_person_view: label: Person details chill_person_general_edit: - pattern: /{_locale}/person/{person_id}/general/edit + path: /{_locale}/person/{person_id}/general/edit defaults: {_controller: ChillPersonBundle:Person:edit } chill_person_general_update: - pattern: /{_locale}/person/{person_id}/general/update + path: /{_locale}/person/{person_id}/general/update defaults: {_controller: ChillPersonBundle:Person:update } chill_person_new: - pattern: /{_locale}/person/new + path: /{_locale}/person/new defaults: {_controller: ChillPersonBundle:Person:new } options: menus: @@ -29,15 +29,15 @@ chill_person_new: icons: [plus, male, female] chill_person_review: - pattern: /{_locale}/person/review + path: /{_locale}/person/review defaults: {_controller: ChillPersonBundle:Person:review } chill_person_create: - pattern: /{_locale}/person/create + path: /{_locale}/person/create defaults: {_controller: ChillPersonBundle:Person:create } chill_person_search: - pattern: /{_locale}/person/search + path: /{_locale}/person/search defaults: { _controller: ChillPersonBundle:Person:search } options: menus: @@ -46,7 +46,7 @@ chill_person_search: label: Search within persons chill_person_accompanying_period_list: - pattern: /{_locale}/person/{person_id}/accompanying-period + path: /{_locale}/person/{person_id}/accompanying-period defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:list } # options: # menus: @@ -55,23 +55,23 @@ chill_person_accompanying_period_list: # label: menu.person.history chill_person_accompanying_period_create: - pattern: /{_locale}/person/{person_id}/accompanying-period/create + path: /{_locale}/person/{person_id}/accompanying-period/create defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:create } chill_person_accompanying_period_update: - pattern: /{_locale}/person/{person_id}/accompanying-period/{period_id}/update + path: /{_locale}/person/{person_id}/accompanying-period/{period_id}/update defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:update } chill_person_accompanying_period_close: - pattern: /{_locale}/person/{person_id}/accompanying-period/close + path: /{_locale}/person/{person_id}/accompanying-period/close defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:close } chill_person_accompanying_period_open: - pattern: /{_locale}/person/{person_id}/accompanying-period/open + path: /{_locale}/person/{person_id}/accompanying-period/open defaults: { _controller: ChillPersonBundle:AccompanyingPeriod:open } chill_person_admin: - pattern: /{_locale}/admin/person + path: /{_locale}/admin/person defaults: { _controller: ChillPersonBundle:Admin:index } options: menus: @@ -81,7 +81,7 @@ chill_person_admin: helper: menu.person.admin.helper chill_person_export: - pattern: /{_locale}/person/export/ + path: /{_locale}/person/export/ defaults: { _controller: ChillPersonBundle:Person:export } options: menus: @@ -90,7 +90,7 @@ chill_person_export: label: Export persons chill_person_timeline: - pattern: /{_locale}/person/{person_id}/timeline + path: /{_locale}/person/{person_id}/timeline defaults: { _controller: ChillPersonBundle:TimelinePerson:person } options: menus: diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 9f7f61f99..b97cfefd4 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -14,6 +14,8 @@ services: class: Chill\PersonBundle\Search\PersonSearch arguments: - "@doctrine.orm.entity_manager" + - "@security.token_storage" + - "@chill.main.security.authorization.helper" calls: - ['setContainer', ["@service_container"]] tags: @@ -32,3 +34,10 @@ services: - "@doctrine.orm.entity_manager" tags: - { name: chill.timeline, context: 'person' } + + chill.person.security.authorization.person: + class: Chill\PersonBundle\Security\Authorization\PersonVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } diff --git a/Resources/migrations/Version20150607231010.php b/Resources/migrations/Version20150607231010.php new file mode 100644 index 000000000..38abb3e47 --- /dev/null +++ b/Resources/migrations/Version20150607231010.php @@ -0,0 +1,100 @@ +container = $container; + } + + public function getDescription() + { + return 'Add a center on the person entity. The default center is the first ' + . 'recorded.'; + } + + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + // retrieve center for setting a default center + $centers = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:Center') + ->findAll(); + + + if (count($centers) > 0) { + $defaultCenterId = $centers[0]->getId(); + } else { // if no center, performs other checks + //check if there are data in person table + $nbPeople = $this->container->get('doctrine.orm.entity_manager') + ->createQuery('SELECT count(p) FROM ChillPersonBundle:Person p') + ->getSingleScalarResult(); + + if ($nbPeople > 0) { + // we have data ! We have to create a center ! + $center = new Center(); + $center->setName('Auto-created center'); + $this->container->get('doctrine.orm.entity_manager') + ->persist($center) + ->flush(); + $defaultCenterId = $center->getId(); + } + } + + + $this->addSql('ALTER TABLE person ADD center_id INT'); + + if (isset($defaultCenterId)) { + $this->addSql('UPDATE person SET center_id = :id', array('id' => $defaultCenterId)); + } + + $this->addSql('ALTER TABLE person ' + . 'ADD CONSTRAINT FK_person_center FOREIGN KEY (center_id) ' + . 'REFERENCES centers (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE person ALTER center_id SET NOT NULL'); + $this->addSql('CREATE INDEX IDX_person_center ON person (center_id)'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE Person DROP CONSTRAINT FK_person_center'); + $this->addSql('DROP INDEX IDX_person_center'); + $this->addSql('ALTER TABLE Person DROP center_id'); + } + + + +} diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index 56896403b..13d3e0c3e 100644 --- a/Resources/views/Person/view.html.twig +++ b/Resources/views/Person/view.html.twig @@ -35,8 +35,9 @@ This view should receive those arguments: {% block personcontent %} - +{% if is_granted('CHILL_PERSON_UPDATE', person) %} {{ include(edit_tmp_name, edit_tmp_args) }} +{% endif %}

{{ 'General information'|trans }}

@@ -83,7 +84,9 @@ This view should receive those arguments:
+{% if is_granted('CHILL_PERSON_UPDATE', person) %} {{ include(edit_tmp_name, edit_tmp_args) }} +{% endif %}

{{ 'Administrative information'|trans }}

@@ -112,7 +115,9 @@ This view should receive those arguments:
+{% if is_granted('CHILL_PERSON_UPDATE', person) %} {{ include(edit_tmp_name, edit_tmp_args) }} +{% endif %}

{{ 'Contact information'|trans }}

@@ -128,7 +133,9 @@ This view should receive those arguments:
+{% if is_granted('CHILL_PERSON_UPDATE', person) %} {{ include(edit_tmp_name, edit_tmp_args) }} +{% endif %} {% if cFGroup %}
@@ -142,7 +149,9 @@ This view should receive those arguments: {% endfor %}
- {{ include(edit_tmp_name, edit_tmp_args) }} +{% if is_granted('CHILL_PERSON_UPDATE', person) %} +{{ include(edit_tmp_name, edit_tmp_args) }} +{% endif %} {% endif %} diff --git a/Search/PersonSearch.php b/Search/PersonSearch.php index dda98721c..5d1e69ca7 100644 --- a/Search/PersonSearch.php +++ b/Search/PersonSearch.php @@ -23,11 +23,14 @@ use Chill\MainBundle\Search\AbstractSearch; use Doctrine\ORM\EntityManagerInterface; use Chill\PersonBundle\Entity\Person; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Chill\MainBundle\Search\ParsingException; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Symfony\Component\Security\Core\Role\Role; -class PersonSearch extends AbstractSearch +class PersonSearch extends AbstractSearch implements ContainerAwareInterface { use ContainerAwareTrait; @@ -37,10 +40,31 @@ class PersonSearch extends AbstractSearch */ private $em; + /** + * + * @var \Chill\MainBundle\Entity\User + */ + private $user; - public function __construct(EntityManagerInterface $em) + /** + * + * @var AuthorizationHelper + */ + private $helper; + + + public function __construct(EntityManagerInterface $em, + TokenStorage $tokenStorage, AuthorizationHelper $helper) { $this->em = $em; + $this->user = $tokenStorage->getToken()->getUser(); + $this->helper = $helper; + + // throw an error if user is not a valid user + if (!$this->user instanceof \Chill\MainBundle\Entity\User) { + throw new \LogicException('The user provided must be an instance' + . ' of Chill\MainBundle\Entity\User'); + } } /* @@ -189,6 +213,14 @@ class PersonSearch extends AbstractSearch } } + //restraint center for security + $reachableCenters = $this->helper->getReachableCenters($this->user, + new Role('CHILL_PERSON_SEE')); + $qb->andWhere($qb->expr() + ->in('p.center', ':centers')) + ->setParameter('centers', $reachableCenters) + ; + $this->_cacheQuery[$cacheKey] = $qb; return $qb; diff --git a/Security/Authorization/PersonVoter.php b/Security/Authorization/PersonVoter.php new file mode 100644 index 000000000..e373c34a8 --- /dev/null +++ b/Security/Authorization/PersonVoter.php @@ -0,0 +1,67 @@ + + * + * 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\PersonBundle\Security\Authorization; + +use Chill\MainBundle\Security\Authorization\AbstractChillVoter; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; + +/** + * + * + * @author Julien Fastré + */ +class PersonVoter extends AbstractChillVoter +{ + const CREATE = 'CHILL_PERSON_CREATE'; + const UPDATE = 'CHILL_PERSON_UPDATE'; + const SEE = 'CHILL_PERSON_SEE'; + + /** + * + * @var AuthorizationHelper + */ + protected $helper; + + public function __construct(AuthorizationHelper $helper) + { + $this->helper = $helper; + } + + protected function getSupportedAttributes() + { + return array(self::CREATE, self::UPDATE, self::SEE); + } + + protected function getSupportedClasses() + { + return array('Chill\PersonBundle\Entity\Person'); + } + + protected function isGranted($attribute, $person, $user = null) + { + if (!$user instanceof User) { + return false; + } + + return $this->helper->userHasAccess($user, $person, $attribute); + + } +} diff --git a/Tests/Controller/AccompanyingPeriodControllerTest.php b/Tests/Controller/AccompanyingPeriodControllerTest.php index df13e282c..6f31c0e29 100644 --- a/Tests/Controller/AccompanyingPeriodControllerTest.php +++ b/Tests/Controller/AccompanyingPeriodControllerTest.php @@ -77,9 +77,13 @@ class AccompanyingPeriodControllerTest extends WebTestCase 'PHP_AUTH_PW' => 'password', )); + $center = static::$em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + $this->person = (new Person(new \DateTime('2015-01-05'))) ->setFirstName('Roland') ->setLastName('Gallorime') + ->setCenter($center) ->setGenre(Person::GENRE_MAN); static::$em->persist($this->person); diff --git a/Tests/Controller/PersonControllerCreateTest.php b/Tests/Controller/PersonControllerCreateTest.php index 0496d68c2..8808f5c76 100644 --- a/Tests/Controller/PersonControllerCreateTest.php +++ b/Tests/Controller/PersonControllerCreateTest.php @@ -36,6 +36,8 @@ class PersonControllerCreateTest extends WebTestCase const GENRE_INPUT = "chill_personbundle_person_creation[genre]"; const DATEOFBIRTH_INPUT = "chill_personbundle_person_creation[dateOfBirth]"; const CREATEDATE_INPUT = "chill_personbundle_person_creation[creation_date]"; + const CENTER_INPUT = "chill_personbundle_person_creation[center]"; + const LONG_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosq."; /** @@ -43,10 +45,10 @@ class PersonControllerCreateTest extends WebTestCase * * @return \Symfony\Component\BrowserKit\Client */ - private function getAuthenticatedClient() + private function getAuthenticatedClient($username = 'center a_social') { return static::createClient(array(), array( - 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_USER' => $username, 'PHP_AUTH_PW' => 'password', )); } @@ -55,10 +57,11 @@ class PersonControllerCreateTest extends WebTestCase * * @param Form $creationForm */ - private function fillAValidCreationForm(Form &$creationForm) + private function fillAValidCreationForm(Form &$creationForm, + $firstname = 'God', $lastname = 'Jesus') { - $creationForm->get(self::FIRSTNAME_INPUT)->setValue("God"); - $creationForm->get(self::LASTNAME_INPUT)->setValue("Jesus"); + $creationForm->get(self::FIRSTNAME_INPUT)->setValue($firstname); + $creationForm->get(self::LASTNAME_INPUT)->setValue($lastname); $creationForm->get(self::GENRE_INPUT)->select("man"); $date = new \DateTime('1947-02-01'); $creationForm->get(self::DATEOFBIRTH_INPUT)->setValue($date->format('d-m-Y')); @@ -203,13 +206,57 @@ class PersonControllerCreateTest extends WebTestCase . "/{_locale}/person/{personID}/general"); } + /** + * test adding a person with a user with multi center + * is valid + */ + public function testValidFormWithMultiCenterUser() + { + $client = $this->getAuthenticatedClient('multi_center'); + + $crawler = $client->request('GET', '/fr/person/new'); + + $this->assertTrue($client->getResponse()->isSuccessful(), + "The page is accessible at the URL /{_locale}/person/new"); + $form = $crawler->selectButton("Ajouter la personne")->form(); + + $this->fillAValidCreationForm($form, 'roger', 'rabbit'); + + $this->assertTrue($form->has(self::CENTER_INPUT), + 'The page contains a "center" input'); + $centerInput = $form->get(self::CENTER_INPUT); + $availableValues = $centerInput->availableOptionValues(); + $lastCenterInputValue = end($availableValues); + $centerInput->setValue($lastCenterInputValue); + + $client->submit($form); + + $this->assertTrue($client->getResponse()->isRedirect(), + "a valid form redirect to url /{_locale}/person/{personId}/general/edit"); + $client->followRedirect(); + $this->assertRegExp('|/fr/person/[1-9][0-9]*/general/edit$|', + $client->getHistory()->current()->getUri(), + "a valid form redirect to url /{_locale}/person/{personId}/general/edit"); + + } + public static function tearDownAfterClass() { static::bootKernel(); $em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager'); + + //remove two people created during test $jesus = $em->getRepository('ChillPersonBundle:Person') ->findOneBy(array('firstName' => 'God')); - $em->remove($jesus); + if ($jesus !== NULL) { + $em->remove($jesus); + } + + $jesus2 = $em->getRepository('ChillPersonBundle:Person') + ->findOneBy(array('firstName' => 'roger')); + if ($jesus2 !== NULL) { + $em->remove($jesus2); + } $em->flush(); } } diff --git a/Tests/Controller/PersonControllerUpdateTest.php b/Tests/Controller/PersonControllerUpdateTest.php index 3756c8a38..ef933e5e6 100644 --- a/Tests/Controller/PersonControllerUpdateTest.php +++ b/Tests/Controller/PersonControllerUpdateTest.php @@ -57,13 +57,18 @@ class PersonControllerUpdateTest extends WebTestCase { static::bootKernel(); + $this->em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = $this->em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + $this->person = (new Person()) ->setLastName("My Beloved") ->setFirstName("Jesus") + ->setCenter($center) ->setGenre(Person::GENRE_MAN); - $this->em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager'); - $this->em->persist($this->person); $this->em->flush(); @@ -76,10 +81,6 @@ class PersonControllerUpdateTest extends WebTestCase )); } - /** - * - * @return Person - */ protected function refreshPerson() { $this->person = $this->em->getRepository('ChillPersonBundle:Person') @@ -97,6 +98,30 @@ class PersonControllerUpdateTest extends WebTestCase "The person edit form is accessible"); } + public function testEditPageDeniedForUnauthorized_OutsideCenter() + { + $client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center b_social', + 'PHP_AUTH_PW' => 'password', + )); + + $client->request('GET', $this->editUrl); + + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + } + + public function testEditPageDeniedForUnauthorized_InsideCenter() + { + $client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center a_administrative', + 'PHP_AUTH_PW' => 'password', + )); + + $client->request('GET', $this->editUrl); + + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + } + /** * test the edition of a field * diff --git a/Tests/Controller/PersonControllerViewTest.php b/Tests/Controller/PersonControllerViewTest.php new file mode 100644 index 000000000..cffa45506 --- /dev/null +++ b/Tests/Controller/PersonControllerViewTest.php @@ -0,0 +1,104 @@ + + * + * 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\PersonBundle\Tests\Controller; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Chill\PersonBundle\Entity\Person; + +/** + * + * + * @author Julien Fastré + */ +class PersonControllerViewTest extends WebTestCase +{ + + /** + * + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * + * @var Person + */ + private $person; + + public function setUp() + { + static::bootKernel(); + + $this->em = static::$kernel->getContainer() + ->get('doctrine.orm.entity_manager'); + + $center = $this->em->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + + $this->person = (new Person()) + ->setLastName("Tested Person") + ->setFirstName("Réginald") + ->setCenter($center) + ->setGenre(Person::GENRE_MAN); + + $this->em->persist($this->person); + $this->em->flush(); + + $this->seeUrl = '/en/person/'.$this->person->getId().'/general'; + } + + public function testViewPerson() + { + $client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_PW' => 'password', + )); + + $client->request('GET', $this->seeUrl); + + $this->assertTrue($client->getResponse()->isSuccessful()); + } + + public function testViewPersonAccessDeniedForUnauthorized() + { + $client = static::createClient(array(), array( + 'PHP_AUTH_USER' => 'center b_social', + 'PHP_AUTH_PW' => 'password', + )); + + $client->request('GET', $this->seeUrl); + + $this->assertEquals(403, $client->getResponse()->getStatusCode()); + } + + protected function refreshPerson() + { + $this->person = $this->em->getRepository('ChillPersonBundle:Person') + ->find($this->person->getId()); + } + + public function tearDown() + { + $this->refreshPerson(); + $this->em->remove($this->person); + $this->em->flush(); + } + +} diff --git a/Tests/Search/PersonSearchTest.php b/Tests/Search/PersonSearchTest.php index d8f57b9db..eba78d4be 100644 --- a/Tests/Search/PersonSearchTest.php +++ b/Tests/Search/PersonSearchTest.php @@ -194,9 +194,25 @@ class PersonSearchTest extends WebTestCase $this->assertRegExp('/Étienne/', $crawlerNoSpecial->text()); } - private function generateCrawlerForSearch($pattern) + /** + * test that person which a user cannot see are not displayed in results + */ + public function testSearchWithAuthorization() { - $client = $this->getAuthenticatedClient(); + $crawlerCanSee = $this->generateCrawlerForSearch('Gérard', 'center a_social'); + $crawlerCannotSee = $this->generateCrawlerForSearch('Gérard', 'center b_social'); + + $this->assertRegExp('/Gérard/', $crawlerCanSee->text(), + 'center a_social may see "Gérard" in center a'); + $this->assertRegExp('/Aucune personne ne correspond aux termes de recherche "gerard"/', + $crawlerCannotSee->text(), + 'center b_social may not see any "Gérard" associated to center b'); + + } + + private function generateCrawlerForSearch($pattern, $username = 'center a_social') + { + $client = $this->getAuthenticatedClient($username); $crawler = $client->request('GET', '/fr/search', array( 'q' => $pattern @@ -211,10 +227,10 @@ class PersonSearchTest extends WebTestCase * * @return \Symfony\Component\BrowserKit\Client */ - private function getAuthenticatedClient() + private function getAuthenticatedClient($username = 'center a_social') { return static::createClient(array(), array( - 'PHP_AUTH_USER' => 'center a_social', + 'PHP_AUTH_USER' => $username, 'PHP_AUTH_PW' => 'password', )); } diff --git a/Tests/Security/Authorization/PersonVoterTest.php b/Tests/Security/Authorization/PersonVoterTest.php new file mode 100644 index 000000000..474fbc103 --- /dev/null +++ b/Tests/Security/Authorization/PersonVoterTest.php @@ -0,0 +1,184 @@ + + * + * 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\PersonBundle\Tests\Security\Authorization; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\PermissionsGroup; +use Chill\MainBundle\Entity\GroupCenter; +use Chill\MainBundle\Entity\RoleScope; +use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Test\PrepareUserTrait; +use Chill\MainBundle\Test\PrepareCenterTrait; +use Chill\MainBundle\Test\PrepareScopeTrait; +use Chill\MainBundle\Test\ProphecyTrait; + +/** + * Test PersonVoter + * + * @author Julien Fastré + * @author Champs Libres + */ +class PersonVoterTest extends KernelTestCase +{ + + use PrepareUserTrait, PrepareCenterTrait, PrepareScopeTrait; + + /** + * + * @var \Chill\PersonBundle\Security\Authorization\PersonVoter + */ + protected $voter; + + /** + * + * @var \Prophecy\Prophet + */ + protected $prophet; + + public function setUp() + { + static::bootKernel(); + $this->voter = static::$kernel->getContainer() + ->get('chill.person.security.authorization.person'); + $this->prophet = new \Prophecy\Prophet(); + } + + public function testNullUser() + { + $token = $this->prepareToken(); + $center = $this->prepareCenter(1, 'center'); + $person = $this->preparePerson($center); + + $this->assertEquals( + VoterInterface::ACCESS_DENIED, + $this->voter->vote($token, $person, array('CHILL_PERSON_SEE')), + "assert that a null user is not allowed to see" + ); + } + + public function testUserCanNotReachCenter() + { + $centerA = $this->prepareCenter(1, 'centera'); + $centerB = $this->prepareCenter(2, 'centerb'); + $scope = $this->prepareScope(1, 'default'); + $token = $this->prepareToken(array( + array( + 'center' => $centerA, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_PERSON_UPDATE'] + ) + ) + )); + $person = $this->preparePerson($centerB); + + $this->assertEquals( + VoterInterface::ACCESS_DENIED, + $this->voter->vote($token, $person, array('CHILL_PERSON_UPDATE')), + 'assert that a user with right not in the good center has access denied' + ); + } + + /** + * test a user with sufficient right may see the person + */ + public function testUserAllowed() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $token = $this->prepareToken(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_PERSON_SEE'] + ) + ) + )); + $person = $this->preparePerson($center); + + $this->assertEquals( + VoterInterface::ACCESS_GRANTED, + $this->voter->vote($token, $person, array('CHILL_PERSON_SEE')), + 'assert that a user with correct rights may is granted access' + ); + } + + /** + * test a user with sufficient right may see the person. + * hierarchy between role is required + */ + public function testUserAllowedWithInheritance() + { + $center = $this->prepareCenter(1, 'center'); + $scope = $this->prepareScope(1, 'default'); + $token = $this->prepareToken(array( + array( + 'center' => $center, 'permissionsGroup' => array( + ['scope' => $scope, 'role' => 'CHILL_PERSON_UPDATE'] + ) + ) + )); + $person = $this->preparePerson($center); + $this->assertEquals( + VoterInterface::ACCESS_GRANTED, + $this->voter->vote($token, $person, array('CHILL_PERSON_SEE')), + 'assert that a user with correct role is granted on inherited roles' + ); + } + + /** + * prepare a person + * + * The only properties set is the center, others properties are ignored. + * + * @param Center $center + * @return Person + */ + protected function preparePerson(Center $center) + { + return (new Person()) + ->setCenter($center) + ; + } + + /** + * prepare a token interface with correct rights + * + * if $permissions = null, user will be null (no user associated with token + * + * @param array $permissions an array of permissions, with key 'center' for the center and 'permissions' for an array of permissions + * @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface + */ + protected function prepareToken(array $permissions = null) + { + $token = $this->prophet->prophesize(); + $token + ->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + if ($permissions === NULL) { + $token->getUser()->willReturn(null); + } else { + $token->getUser()->willReturn($this->prepareUser($permissions)); + } + + return $token->reveal(); + } +} diff --git a/composer.json b/composer.json index 8210fcd75..5b884c8e4 100644 --- a/composer.json +++ b/composer.json @@ -20,15 +20,15 @@ "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", "doctrine/doctrine-bundle": "~1.2", - "chill-project/main": "*@dev", - "chill-project/custom-fields": "*@dev", + "chill-project/main": "dev-add_acl@dev", + "chill-project/custom-fields": "dev-add_acl@dev", "doctrine/doctrine-fixtures-bundle": "~2.2", "champs-libres/composer-bundle-migration": "~1.0", "doctrine/doctrine-migrations-bundle": "dev-master@dev", @@ -36,7 +36,8 @@ }, "require-dev": { "symfony/dom-crawler": "2.5", - "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 c9d5669ce..c13f031a9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,5 +19,6 @@ + - \ No newline at end of file +