diff --git a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php index e35a9619b..d0336d254 100644 --- a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php +++ b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Form\CalendarType; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface; +use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Pagination\PaginatorFactory; @@ -146,6 +147,8 @@ class CalendarController extends AbstractController */ public function editAction(Calendar $entity, Request $request): Response { + $this->denyAccessUnlessGranted(CalendarVoter::EDIT, $entity); + if (!$this->remoteCalendarConnector->isReady()) { return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri()); } @@ -207,6 +210,8 @@ class CalendarController extends AbstractController */ public function listActionByCourse(AccompanyingPeriod $accompanyingPeriod): Response { + $this->denyAccessUnlessGranted(CalendarVoter::SEE, $accompanyingPeriod); + $filterOrder = $this->buildListFilterOrder(); ['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate'); @@ -239,6 +244,8 @@ class CalendarController extends AbstractController */ public function listActionByPerson(Person $person): Response { + $this->denyAccessUnlessGranted(CalendarVoter::SEE, $person); + $filterOrder = $this->buildListFilterOrder(); ['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate'); @@ -308,7 +315,7 @@ class CalendarController extends AbstractController $view = '@ChillCalendar/Calendar/newByAccompanyingCourse.html.twig'; $entity->setAccompanyingPeriod($accompanyingPeriod); $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]); - } elseif ($person) { + } elseif (null !== $person) { $view = '@ChillCalendar/Calendar/newByPerson.html.twig'; $entity->setPerson($person)->addPerson($person); $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]); @@ -318,6 +325,8 @@ class CalendarController extends AbstractController $entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser'))); } + $this->denyAccessUnlessGranted(CalendarVoter::CREATE, $entity); + $form = $this->createForm(CalendarType::class, $entity) ->add('save', SubmitType::class); @@ -437,6 +446,8 @@ class CalendarController extends AbstractController */ public function toActivity(Request $request, Calendar $calendar): RedirectResponse { + $this->denyAccessUnlessGranted(CalendarVoter::SEE, $calendar); + $personsId = array_map( static fn (Person $p): int => $p->getId(), $calendar->getPersons()->toArray() diff --git a/src/Bundle/ChillCalendarBundle/DependencyInjection/ChillCalendarExtension.php b/src/Bundle/ChillCalendarBundle/DependencyInjection/ChillCalendarExtension.php index 08890912b..c848366bb 100644 --- a/src/Bundle/ChillCalendarBundle/DependencyInjection/ChillCalendarExtension.php +++ b/src/Bundle/ChillCalendarBundle/DependencyInjection/ChillCalendarExtension.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\DependencyInjection; +use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -52,9 +53,10 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf { $this->preprendRoutes($container); $this->prependCruds($container); + $this->prependRoleHierarchy($container); } - protected function prependCruds(ContainerBuilder $container) + private function prependCruds(ContainerBuilder $container) { $container->prependExtensionConfig('chill_main', [ 'cruds' => [ @@ -130,7 +132,18 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf ]); } - protected function preprendRoutes(ContainerBuilder $container) + private function prependRoleHierarchy(ContainerBuilder $container): void + { + $container->prependExtensionConfig('security', [ + 'role_hierarchy' => [ + CalendarVoter::CREATE => [CalendarVoter::SEE], + CalendarVoter::EDIT => [CalendarVoter::SEE], + CalendarVoter::DELETE => [CalendarVoter::SEE], + ], + ]); + } + + private function preprendRoutes(ContainerBuilder $container) { $container->prependExtensionConfig('chill_main', [ 'routing' => [ diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index 351954492..0f112cf36 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -18,6 +18,7 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable; +use Chill\MainBundle\Entity\HasCentersInterface; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -48,7 +49,7 @@ use function in_array; * "chill_calendar_calendar": Calendar::class * }) */ -class Calendar implements TrackCreationInterface, TrackUpdateInterface +class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCentersInterface { use RemoteCalendarTrait; @@ -312,6 +313,20 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this->cancelReason; } + public function getCenters(): ?iterable + { + switch ($this->getContext()) { + case 'person': + return [$this->getPerson()->getCenter()]; + + case 'accompanying_period': + return $this->getAccompanyingPeriod()->getCenters(); + + default: + throw new LogicException('context not supported: ' . $this->getContext()); + } + } + public function getComment(): CommentEmbeddable { return $this->comment; diff --git a/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarDocVoter.php b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarDocVoter.php new file mode 100644 index 000000000..452286e85 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarDocVoter.php @@ -0,0 +1,61 @@ +security = $security; + } + + protected function supports($attribute, $subject): bool + { + return in_array($attribute, self::ALL, true) && $subject instanceof CalendarDoc; + } + + /** + * @param CalendarDoc $subject + * @param mixed $attribute + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + { + switch ($attribute) { + case self::EDIT: + return $this->security->isGranted(CalendarVoter::EDIT, $subject->getCalendar()); + + case self::SEE: + return $this->security->isGranted(CalendarVoter::SEE, $subject->getCalendar()); + + default: + throw new UnexpectedValueException('Attribute not supported: ' . $attribute); + } + } +} diff --git a/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php index 996b5ebfb..0e2be1d0f 100644 --- a/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php +++ b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php @@ -20,13 +20,14 @@ namespace Chill\CalendarBundle\Security\Voter; use Chill\CalendarBundle\Entity\Calendar; use Chill\MainBundle\Security\Authorization\AbstractChillVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface; use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; +use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use Chill\PersonBundle\Security\Authorization\PersonVoter; use LogicException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -41,6 +42,10 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn public const SEE = 'CHILL_CALENDAR_CALENDAR_SEE'; + private AuthorizationHelperInterface $authorizationHelper; + + private CenterResolverManagerInterface $centerResolverManager; + private Security $security; private VoterHelperInterface $voterHelper; @@ -52,8 +57,8 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn $this->security = $security; $this->voterHelper = $voterHelperFactory ->generate(self::class) - ->addCheckFor(AccompanyingPeriod::class, [self::SEE]) - ->addCheckFor(Person::class, [self::SEE]) + ->addCheckFor(AccompanyingPeriod::class, [self::SEE, self::CREATE]) + ->addCheckFor(Person::class, [self::SEE, self::CREATE]) ->addCheckFor(Calendar::class, [self::SEE, self::CREATE, self::EDIT, self::DELETE]) ->build(); } @@ -93,48 +98,37 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn if ($subject instanceof AccompanyingPeriod) { switch ($attribute) { case self::SEE: + case self::CREATE: if ($subject->getStep() === AccompanyingPeriod::STEP_DRAFT) { return false; } // we first check here that the user has read access to the period - return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject); + if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject)) { + return false; + } - default: - throw new LogicException('subject not implemented'); + // There is no scope on Calendar, but there are some on accompanying period + // so, to ignore AccompanyingPeriod's scopes, we create a blank Calendar + // linked with an accompanying period. + return $this->voterHelper->voteOnAttribute($attribute, (new Calendar())->setAccompanyingPeriod($subject), $token); } } elseif ($subject instanceof Person) { switch ($attribute) { case self::SEE: - return $this->security->isGranted(PersonVoter::SEE, $subject); - - default: - throw new LogicException('subject not implemented'); + case self::CREATE: + return $this->voterHelper->voteOnAttribute($attribute, $subject, $token); } } elseif ($subject instanceof Calendar) { - if (null !== $period = $subject->getAccompanyingPeriod()) { - switch ($attribute) { - case self::SEE: - case self::EDIT: - case self::CREATE: - return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $period); - - case self::DELETE: - return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period); - } - } elseif (null !== $person = $subject->getPerson()) { - switch ($attribute) { - case self::SEE: - case self::EDIT: - case self::CREATE: - return $this->security->isGranted(PersonVoter::SEE, $person); - - case self::DELETE: - return $this->security->isGranted(PersonVoter::UPDATE, $person); - } + switch ($attribute) { + case self::SEE: + case self::EDIT: + case self::CREATE: + case self::DELETE: + return $this->voterHelper->voteOnAttribute($attribute, $subject, $token); } } - throw new LogicException('attribute not implemented'); + throw new LogicException('attribute or not implemented'); } } diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index 930a2c4d1..33e9a3db6 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -309,7 +309,7 @@ class AuthorizationHelper implements AuthorizationHelperInterface if ($this->isRoleReached($attribute, $roleScope->getRole())) { //if yes, we have a right on something... // perform check on scope if necessary - if ($this->scopeResolverDispatcher->isConcerned($entity)) { + if ($this->scopeResolverDispatcher->isConcerned($entity)) {// here, we should also check that the role need a scope $scope = $this->scopeResolverDispatcher->resolveScope($entity); if (null === $scope) {