mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Merge branch 'accompanying_period_voter' into 'master'
rights for reassigning and accessing confidential parcours Closes #99 and #121 See merge request Chill-Projet/chill-bundles!483
This commit is contained in:
		
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20230705-220544.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20230705-220544.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: Create a role "See Confidential Periods", separated from the "Reassign courses" | ||||
|   role | ||||
| time: 2023-07-05T22:05:44.435112463+02:00 | ||||
| custom: | ||||
|   Issue: "121" | ||||
| @@ -161,6 +161,7 @@ class TimelineActivityProvider implements TimelineProviderInterface | ||||
|  | ||||
|         // loop on reachable scopes | ||||
|         foreach ($reachableScopes as $scope) { | ||||
|             /** @phpstan-ignore-next-line  */ | ||||
|             if (in_array($scope->getId(), $scopes_ids, true)) { | ||||
|                 continue; | ||||
|             } | ||||
|   | ||||
| @@ -195,10 +195,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface | ||||
|  | ||||
|     /** | ||||
|      * Return all reachable scope for a given user, center and role. | ||||
|      * | ||||
|      * @param Center|Center[] $center | ||||
|      * | ||||
|      * @return array|Scope[] | ||||
|      */ | ||||
|     public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array | ||||
|     { | ||||
|   | ||||
| @@ -25,7 +25,8 @@ interface AuthorizationHelperForCurrentUserInterface | ||||
|     public function getReachableCenters(string $role, ?Scope $scope = null): array; | ||||
|  | ||||
|     /** | ||||
|      * @param array|Center|Center[] $center | ||||
|      * @param list<Center>|Center $center | ||||
|      * @return list<Scope> | ||||
|      */ | ||||
|     public function getReachableScopes(string $role, $center): array; | ||||
|     public function getReachableScopes(string $role, array|Center $center): array; | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,8 @@ interface AuthorizationHelperInterface | ||||
|     public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array; | ||||
|  | ||||
|     /** | ||||
|      * @param Center|list<Center> $center | ||||
|      * @param Center|array<Center> $center | ||||
|      * @return list<Scope> | ||||
|      */ | ||||
|     public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array; | ||||
| } | ||||
|   | ||||
| @@ -219,13 +219,13 @@ class AccompanyingPeriodController extends AbstractController | ||||
|         ]); | ||||
|         $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); | ||||
|  | ||||
|         $accompanyingPeriodsRaw = $this->accompanyingPeriodACLAwareRepository | ||||
|             ->findByPerson($person, AccompanyingPeriodVoter::SEE); | ||||
|         $accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository | ||||
|             ->findByPerson($person, AccompanyingPeriodVoter::SEE, ["openingDate" => "DESC", "id" => "DESC"]); | ||||
|  | ||||
|         usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() > $a->getOpeningDate()); | ||||
|         //usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() <=> $a->getOpeningDate()); | ||||
|  | ||||
|         // filter visible or not visible | ||||
|         $accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); | ||||
|         //$accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); | ||||
|  | ||||
|         return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [ | ||||
|             'accompanying_periods' => $accompanyingPeriods, | ||||
|   | ||||
| @@ -78,6 +78,7 @@ class AccompanyingPeriodRegulationListController | ||||
|                 $form['jobs']->getData(), | ||||
|                 $form['services']->getData(), | ||||
|                 $form['locations']->getData(), | ||||
|                 ['openingDate' => 'DESC', 'id' => 'DESC'], | ||||
|                 $paginator->getItemsPerPage(), | ||||
|                 $paginator->getCurrentPageFirstItemNumber() | ||||
|             ); | ||||
|   | ||||
| @@ -20,6 +20,7 @@ use Chill\MainBundle\Repository\UserRepository; | ||||
| use Chill\MainBundle\Templating\Entity\UserRender; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; | ||||
| use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\Form\CallbackTransformer; | ||||
| @@ -30,6 +31,7 @@ use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Exception\AccessDeniedException; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| @@ -85,8 +87,8 @@ class ReassignAccompanyingPeriodController extends AbstractController | ||||
|      */ | ||||
|     public function listAction(Request $request): Response | ||||
|     { | ||||
|         if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) { | ||||
|             throw new AccessDeniedException(); | ||||
|         if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) { | ||||
|             throw new AccessDeniedHttpException('no right to reassign bulk'); | ||||
|         } | ||||
|  | ||||
|         $form = $this->buildFilterForm(); | ||||
| @@ -96,7 +98,7 @@ class ReassignAccompanyingPeriodController extends AbstractController | ||||
|         $userFrom = $form['user']->getData(); | ||||
|         $postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : []; | ||||
|  | ||||
|         $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); | ||||
|         $total = $this->accompanyingPeriodACLAwareRepository->countByUserAndPostalCodesOpenedAccompanyingPeriod($userFrom, $postalCodes); | ||||
|         $paginator = $this->paginatorFactory->create($total); | ||||
|         $paginator->setItemsPerPage(50); | ||||
|         $periods = $this->accompanyingPeriodACLAwareRepository | ||||
|   | ||||
| @@ -983,11 +983,8 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac | ||||
|                     AccompanyingPeriodVoter::EDIT, | ||||
|                     AccompanyingPeriodVoter::DELETE, | ||||
|                 ], | ||||
|                 AccompanyingPeriodVoter::REASSIGN_BULK => [ | ||||
|                     AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, | ||||
|                 ], | ||||
|                 AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL => [ | ||||
|                     AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, | ||||
|                 AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL => [ | ||||
|                     AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, | ||||
|                 ], | ||||
|             ], | ||||
|         ]); | ||||
|   | ||||
| @@ -12,107 +12,93 @@ declare(strict_types=1); | ||||
| namespace Chill\PersonBundle\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Address; | ||||
| use Chill\MainBundle\Entity\Center; | ||||
| use Chill\MainBundle\Entity\Location; | ||||
| use Chill\MainBundle\Entity\PostalCode; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelper; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; | ||||
| use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; | ||||
| use DateTime; | ||||
|  | ||||
| use DateTimeImmutable; | ||||
| use Doctrine\DBAL\Types\Types; | ||||
| use Doctrine\ORM\NonUniqueResultException; | ||||
| use Doctrine\ORM\NoResultException; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Repository\AccompanyingPeriodACLAwareRepositoryTest; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use function count; | ||||
|  | ||||
| final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface | ||||
| /** | ||||
|  * @see AccompanyingPeriodACLAwareRepositoryTest | ||||
|  */ | ||||
| final readonly class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface | ||||
| { | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|  | ||||
|     private AuthorizationHelper $authorizationHelper; | ||||
|     private AuthorizationHelperForCurrentUserInterface $authorizationHelper; | ||||
|  | ||||
|     private CenterResolverDispatcherInterface $centerResolverDispatcher; | ||||
|     private CenterResolverManagerInterface $centerResolver; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     public function __construct( | ||||
|         AccompanyingPeriodRepository $accompanyingPeriodRepository, | ||||
|         Security $security, | ||||
|         AuthorizationHelper $authorizationHelper, | ||||
|         CenterResolverDispatcherInterface $centerResolverDispatcher | ||||
|         AuthorizationHelperForCurrentUserInterface $authorizationHelper, | ||||
|         CenterResolverManagerInterface $centerResolverDispatcher | ||||
|     ) { | ||||
|         $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; | ||||
|         $this->security = $security; | ||||
|         $this->authorizationHelper = $authorizationHelper; | ||||
|         $this->centerResolverDispatcher = $centerResolverDispatcher; | ||||
|         $this->centerResolver = $centerResolverDispatcher; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array|PostalCode[] | ||||
|      * | ||||
|      * @return QueryBuilder | ||||
|      */ | ||||
|     public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []) | ||||
|     public function buildQueryOpenedAccompanyingCourseByUserAndPostalCodes(?User $user, array $postalCodes = []): QueryBuilder | ||||
|     { | ||||
|         $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); | ||||
|  | ||||
|         $qb->where($qb->expr()->eq('ap.user', ':user')) | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->neq('ap.step', ':draft'), | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull('ap.closingDate'), | ||||
|                     $qb->expr()->gt('ap.closingDate', ':now') | ||||
|                 ) | ||||
|                 $qb->expr()->neq('ap.step', ':closed'), | ||||
|             ) | ||||
|             ->setParameter('user', $user) | ||||
|             ->setParameter('now', new DateTime('now')) | ||||
|             ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); | ||||
|             ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) | ||||
|             ->setParameter('closed', AccompanyingPeriod::STEP_CLOSED); | ||||
|  | ||||
|         if ([] !== $postalCodes) { | ||||
|             $qb->join('ap.locationHistories', 'location_history') | ||||
|                 ->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') | ||||
|             $qb->join('ap.locationHistories', 'location_history', Join::WITH, 'location_history.endDate IS NULL') | ||||
|                 ->leftJoin(Person\PersonCurrentAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') | ||||
|                 ->join( | ||||
|                     Address::class, | ||||
|                     'address', | ||||
|                     Join::WITH, | ||||
|                     'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id' | ||||
|                     'COALESCE(IDENTITY(person_address.address), IDENTITY(location_history.addressLocation)) = address.id' | ||||
|                 ) | ||||
|                 ->join('address.postcode', 'postcode') | ||||
|                 ->andWhere( | ||||
|                     $qb->expr()->orX( | ||||
|                         $qb->expr()->isNull('person_address'), | ||||
|                         $qb->expr()->andX( | ||||
|                             $qb->expr()->lte('person_address.validFrom', ':now'), | ||||
|                             $qb->expr()->orX( | ||||
|                                 $qb->expr()->isNull('person_address.validTo'), | ||||
|                                 $qb->expr()->lt('person_address.validTo', ':now') | ||||
|                             ) | ||||
|                         ) | ||||
|                     ) | ||||
|                     $qb->expr()->in('postcode.code', ':postal_codes') | ||||
|                 ) | ||||
|                 ->andWhere( | ||||
|                     $qb->expr()->isNull('location_history.endDate') | ||||
|                 ) | ||||
|                 ->andWhere( | ||||
|                     $qb->expr()->in('address.postcode', ':postal_codes') | ||||
|                 ) | ||||
|                 ->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE) | ||||
|                 ->setParameter('postal_codes', $postalCodes); | ||||
|                 ->setParameter('postal_codes', array_map(fn (PostalCode $postalCode) => $postalCode->getCode(), $postalCodes)); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws NonUniqueResultException | ||||
|      * @throws NoResultException | ||||
|      */ | ||||
|     public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int | ||||
|     { | ||||
|         $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); | ||||
|         $qb = $this->addACLMultiCenterOnQuery( | ||||
|             $this->buildQueryUnDispatched($jobs, $services, $administrativeLocations), | ||||
|             $this->buildCenterOnScope() | ||||
|         ); | ||||
|  | ||||
|         $qb->select('COUNT(ap)'); | ||||
|  | ||||
| @@ -125,22 +111,12 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes) | ||||
|             ->select('COUNT(ap)') | ||||
|             ->getQuery() | ||||
|             ->getSingleScalarResult(); | ||||
|     } | ||||
|         $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); | ||||
|         $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); | ||||
|  | ||||
|     public function countByUserOpenedAccompanyingPeriod(?User $user): int | ||||
|     { | ||||
|         if (null === $user) { | ||||
|             return 0; | ||||
|         } | ||||
|         $qb->select('COUNT(DISTINCT ap)'); | ||||
|  | ||||
|         return $this->buildQueryOpenedAccompanyingCourseByUser($user) | ||||
|             ->select('COUNT(ap)') | ||||
|             ->getQuery() | ||||
|             ->getSingleScalarResult(); | ||||
|         return $qb->getQuery()->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     public function findByPerson( | ||||
| @@ -152,10 +128,14 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|     ): array { | ||||
|         $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); | ||||
|         $scopes = $this->authorizationHelper | ||||
|             ->getReachableCircles( | ||||
|                 $this->security->getUser(), | ||||
|             ->getReachableScopes( | ||||
|                 $role, | ||||
|                 $this->centerResolverDispatcher->resolveCenter($person) | ||||
|                 $this->centerResolver->resolveCenters($person) | ||||
|             ); | ||||
|         $scopesCanSeeConfidential = $this->authorizationHelper | ||||
|             ->getReachableScopes( | ||||
|                 AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, | ||||
|                 $this->centerResolver->resolveCenters($person) | ||||
|             ); | ||||
|  | ||||
|         if (0 === count($scopes)) { | ||||
| @@ -165,12 +145,44 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|         $qb | ||||
|             ->join('ap.participations', 'participation') | ||||
|             ->where($qb->expr()->eq('participation.person', ':person')) | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     'ap.confidential = FALSE', | ||||
|                     $qb->expr()->eq('ap.user', ':user') | ||||
|                 ) | ||||
|             ) | ||||
|             ->setParameter('person', $person); | ||||
|  | ||||
|         $qb = $this->addACLClauses($qb, $scopes, $scopesCanSeeConfidential); | ||||
|         $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function addOrderLimitClauses(QueryBuilder $qb, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): QueryBuilder | ||||
|     { | ||||
|         if (null !== $orderBy) { | ||||
|             foreach ($orderBy as $field => $order) { | ||||
|                 $qb->addOrderBy('ap.' . $field, $order); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         if (null !== $offset) { | ||||
|             $qb->setFirstResult($offset); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add clause for scope on a query, based on no | ||||
|      * | ||||
|      * @param QueryBuilder $qb where the accompanying period have the `ap` alias | ||||
|      * @param array<Scope> $scopesCanSee | ||||
|      * @param array<Scope> $scopesCanSeeConfidential | ||||
|      * @return QueryBuilder | ||||
|      */ | ||||
|     public function addACLClauses(QueryBuilder $qb, array $scopesCanSee, array $scopesCanSeeConfidential): QueryBuilder | ||||
|     { | ||||
|         $qb | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->neq('ap.step', ':draft'), | ||||
| @@ -181,40 +193,67 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|                 ) | ||||
|             ) | ||||
|             ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) | ||||
|             ->setParameter('person', $person) | ||||
|             ->setParameter('user', $this->security->getUser()) | ||||
|             ->setParameter('creator', $this->security->getUser()); | ||||
|  | ||||
|         // add join condition for scopes | ||||
|         $orx = $qb->expr()->orX( | ||||
|             // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state | ||||
|             $qb->expr()->eq('ap.step', ':draft') | ||||
|         ); | ||||
|  | ||||
|         foreach ($scopes as $key => $scope) { | ||||
|             $orx->add($qb->expr()->orX( | ||||
|         foreach ($scopesCanSee as $key => $scope) { | ||||
|             // for each scope: | ||||
|             // - either the user is the referrer of the course | ||||
|             // - or the accompanying course is one of the reachable scopes | ||||
|             // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course | ||||
|  | ||||
|             $orOnScope = $qb->expr()->orX( | ||||
|                 $qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes'), | ||||
|                 $qb->expr()->eq('ap.user', ':user') | ||||
|             )); | ||||
|             ); | ||||
|  | ||||
|             if (in_array($scope, $scopesCanSeeConfidential, true)) { | ||||
|                 $orx->add($orOnScope); | ||||
|             } else { | ||||
|                 // we must add a condition: the course is not confidential or the user is the referrer | ||||
|                 $andXOnScope = $qb->expr()->andX( | ||||
|                     $orOnScope, | ||||
|                     $qb->expr()->orX( | ||||
|                         'ap.confidential = FALSE', | ||||
|                         $qb->expr()->eq('ap.user', ':user') | ||||
|                     ) | ||||
|                 ); | ||||
|                 $orx->add($andXOnScope); | ||||
|             } | ||||
|             $qb->setParameter('scope_' . $key, $scope); | ||||
|             $qb->setParameter('user', $this->security->getUser()); | ||||
|         } | ||||
|         $qb->andWhere($orx); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array | ||||
|     public function buildCenterOnScope(): array | ||||
|     { | ||||
|         $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); | ||||
|         $centerOnScopes = []; | ||||
|         foreach ($this->authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE) as $center) { | ||||
|             $centerOnScopes[] = [ | ||||
|                 'center' => $center, | ||||
|                 'scopeOnRole' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center), | ||||
|                 'scopeCanSeeConfidential' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center), | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return $centerOnScopes; | ||||
|     } | ||||
|  | ||||
|     public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         $qb = $this->buildQueryUnDispatched($jobs, $services, $administrativeAdministrativeLocations); | ||||
|         $qb->select('ap'); | ||||
|  | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         if (null !== $offset) { | ||||
|             $qb->setFirstResult($offset); | ||||
|         } | ||||
|         $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); | ||||
|         $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
| @@ -225,76 +264,80 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); | ||||
|  | ||||
|         $qb->setFirstResult($offset) | ||||
|             ->setMaxResults($limit); | ||||
|  | ||||
|         foreach ($orderBy as $field => $direction) { | ||||
|             $qb->addOrderBy('ap.' . $field, $direction); | ||||
|         } | ||||
|         $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); | ||||
|         $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); | ||||
|         $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array|AccompanyingPeriod[] | ||||
|      * @param QueryBuilder $qb | ||||
|      * @param list<array{center: Center, scopeOnRole: list<Scope>, scopeCanSeeConfidential: list<Scope>}> $centerScopes | ||||
|      * @param bool $allowNoCenter if true, will allow to see the periods linked to person which does not have any center. Very few edge case when some Person are not associated to a center. | ||||
|      * @return QueryBuilder | ||||
|      */ | ||||
|     public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array | ||||
|     public function addACLMultiCenterOnQuery(QueryBuilder $qb, array $centerScopes, bool $allowNoCenter = false): QueryBuilder | ||||
|     { | ||||
|         if (null === $user) { | ||||
|             return []; | ||||
|         } | ||||
|         $user = $this->security->getUser(); | ||||
|  | ||||
|         $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); | ||||
|  | ||||
|         $qb->setFirstResult($offset) | ||||
|             ->setMaxResults($limit); | ||||
|  | ||||
|         foreach ($orderBy as $field => $direction) { | ||||
|             $qb->addOrderBy('ap.' . $field, $direction); | ||||
|         } | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     private function addACLByUnDispatched(QueryBuilder $qb): QueryBuilder | ||||
|     { | ||||
|         $centers = $this->authorizationHelper->getReachableCenters( | ||||
|             $this->security->getUser(), | ||||
|             AccompanyingPeriodVoter::SEE | ||||
|         ); | ||||
|  | ||||
|         $orX = $qb->expr()->orX(); | ||||
|  | ||||
|         if (0 === count($centers)) { | ||||
|         if (0 === count($centerScopes) || !$user instanceof User) { | ||||
|             return $qb->andWhere("'FALSE' = 'TRUE'"); | ||||
|         } | ||||
|  | ||||
|         foreach ($centers as $key => $center) { | ||||
|             $scopes = $this->authorizationHelper | ||||
|                 ->getReachableCircles( | ||||
|                     $this->security->getUser(), | ||||
|                     AccompanyingPeriodVoter::SEE, | ||||
|                     $center | ||||
|                 ); | ||||
|         $orX = $qb->expr()->orX(); | ||||
|  | ||||
|         $idx = 0; | ||||
|         foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { | ||||
|             $and = $qb->expr()->andX( | ||||
|                 $qb->expr()->exists('SELECT part FROM ' . AccompanyingPeriodParticipation::class . ' part ' . | ||||
|                     "JOIN part.person p WHERE part.accompanyingPeriod = ap.id AND p.center = :center_{$key}") | ||||
|                 $qb->expr()->exists( | ||||
|                     'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . " part_{$idx} " . | ||||
|                     "JOIN part_{$idx}.person p{$idx} LEFT JOIN p{$idx}.centerCurrent centerCurrent_{$idx} " . | ||||
|                     "WHERE part_{$idx}.accompanyingPeriod = ap.id AND (centerCurrent_{$idx}.center = :center_{$idx}" | ||||
|                     . ($allowNoCenter ? " OR centerCurrent_{$idx}.id IS NULL)" : ")") | ||||
|                 ) | ||||
|             ); | ||||
|             $qb->setParameter('center_' . $key, $center); | ||||
|             $orScope = $qb->expr()->orX(); | ||||
|             $qb->setParameter('center_' . $idx, $center); | ||||
|  | ||||
|             foreach ($scopes as $skey => $scope) { | ||||
|                 $orScope->add( | ||||
|                     $qb->expr()->isMemberOf(':scope_' . $key . '_' . $skey, 'ap.scopes') | ||||
|             $orScopeInsideCenter = $qb->expr()->orX( | ||||
|                 // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state | ||||
|                 $qb->expr()->eq('ap.step', ':draft') | ||||
|             ); | ||||
|  | ||||
|             $idx++; | ||||
|             foreach ($scopes as $scope) { | ||||
|                 // for each scope: | ||||
|                 // - either the user is the referrer of the course | ||||
|                 // - or the accompanying course is one of the reachable scopes | ||||
|                 // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course | ||||
|                 $orOnScope = $qb->expr()->orX( | ||||
|                     $qb->expr()->isMemberOf(':scope_' . $idx, 'ap.scopes'), | ||||
|                     $qb->expr()->eq('ap.user', ':user_executing') | ||||
|                 ); | ||||
|                 $qb->setParameter('scope_' . $key . '_' . $skey, $scope); | ||||
|                 $qb->setParameter('user_executing', $user); | ||||
|  | ||||
|                 if (in_array($scope, $scopesCanSeeConfidential, true)) { | ||||
|                     $orScopeInsideCenter->add($orOnScope); | ||||
|                 } else { | ||||
|                     // we must add a condition: the course is not confidential or the user is the referrer | ||||
|                     $andXOnScope = $qb->expr()->andX( | ||||
|                         $orOnScope, | ||||
|                         $qb->expr()->orX( | ||||
|                             'ap.confidential = FALSE', | ||||
|                             $qb->expr()->eq('ap.user', ':user_executing') | ||||
|                         ) | ||||
|                     ); | ||||
|                     $orScopeInsideCenter->add($andXOnScope); | ||||
|                 } | ||||
|                 $qb->setParameter('scope_' . $idx, $scope); | ||||
|  | ||||
|                 $idx++; | ||||
|             } | ||||
|  | ||||
|             $and->add($orScope); | ||||
|             $and->add($orScopeInsideCenter); | ||||
|             $orX->add($and); | ||||
|  | ||||
|             $idx++; | ||||
|         } | ||||
|  | ||||
|         return $qb->andWhere($orX); | ||||
| @@ -305,7 +348,7 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|      * @param array|Scope[] $services | ||||
|      * @param array|Location[] $locations | ||||
|      */ | ||||
|     private function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder | ||||
|     public function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder | ||||
|     { | ||||
|         $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); | ||||
|  | ||||
| @@ -333,8 +376,8 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC | ||||
|             $or = $qb->expr()->orX(); | ||||
|  | ||||
|             foreach ($services as $key => $service) { | ||||
|                 $or->add($qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes')); | ||||
|                 $qb->setParameter('scope_' . $key, $service); | ||||
|                 $or->add($qb->expr()->isMemberOf(':scopef_' . $key, 'ap.scopes')); | ||||
|                 $qb->setParameter('scopef_' . $key, $service); | ||||
|             } | ||||
|             $qb->andWhere($or); | ||||
|         } | ||||
|   | ||||
| @@ -31,28 +31,28 @@ interface AccompanyingPeriodACLAwareRepositoryInterface | ||||
|      */ | ||||
|     public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int; | ||||
|  | ||||
|     public function countByUserOpenedAccompanyingPeriod(?User $user): int; | ||||
|  | ||||
|     /** | ||||
|      * @return array<AccompanyingPeriod> | ||||
|      */ | ||||
|     public function findByPerson( | ||||
|         Person $person, | ||||
|         string $role, | ||||
|         ?array $orderBy = [], | ||||
|         ?int $limit = null, | ||||
|         ?int $offset = null | ||||
|         ?int   $limit = null, | ||||
|         ?int   $offset = null | ||||
|     ): array; | ||||
|  | ||||
|     /** | ||||
|      * @param array|UserJob[] $jobs if empty, does not take this argument into account | ||||
|      * @param array|Scope[] $services if empty, does not take this argument into account | ||||
|      * | ||||
|      * @return array|AccompanyingPeriod[] | ||||
|      * @return list<AccompanyingPeriod> | ||||
|      */ | ||||
|     public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; | ||||
|     public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; | ||||
|  | ||||
|     /** | ||||
|      * @param array|PostalCode[] $postalCodes | ||||
|      * @return list<AccompanyingPeriod> | ||||
|      */ | ||||
|     public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array; | ||||
|  | ||||
|     public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; | ||||
| } | ||||
|   | ||||
| @@ -42,11 +42,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|         self::RE_OPEN_COURSE, | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
|      * Give the ability to see all confidential courses. | ||||
|      */ | ||||
|     public const CONFIDENTIAL_CRUD = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CRUD_CONFIDENTIAL'; | ||||
|  | ||||
|     public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE'; | ||||
|  | ||||
|     /** | ||||
| @@ -107,6 +102,11 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|      */ | ||||
|     public const TOGGLE_INTENSITY = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_INTENSITY'; | ||||
|  | ||||
|     /** | ||||
|      * Right to see confidential period even if not referrer | ||||
|      */ | ||||
|     public const SEE_CONFIDENTIAL_ALL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL'; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private VoterHelperInterface $voterHelper; | ||||
| @@ -131,7 +131,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|         return [ | ||||
|             self::SEE, | ||||
|             self::SEE_DETAILS, | ||||
|             self::CONFIDENTIAL_CRUD, | ||||
|             self::CREATE, | ||||
|             self::EDIT, | ||||
|             self::DELETE, | ||||
| @@ -139,6 +138,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|             self::TOGGLE_CONFIDENTIAL_ALL, | ||||
|             self::REASSIGN_BULK, | ||||
|             self::STATS, | ||||
|             self::SEE_CONFIDENTIAL_ALL, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @@ -149,7 +149,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|  | ||||
|     public function getRolesWithoutScope(): array | ||||
|     { | ||||
|         return [self::REASSIGN_BULK]; | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     protected function supports($attribute, $subject) | ||||
| @@ -216,7 +216,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH | ||||
|  | ||||
|             // if confidential, only the referent can see it | ||||
|             if ($subject->isConfidential()) { | ||||
|                 if ($this->voterHelper->voteOnAttribute(self::CONFIDENTIAL_CRUD, $subject, $token)) { | ||||
|                 if ($this->voterHelper->voteOnAttribute(self::SEE_CONFIDENTIAL_ALL, $subject, $token)) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -360,10 +360,12 @@ final class PersonContext implements PersonContextInterface | ||||
|  | ||||
|     private function isScopeNecessary(Person $person): bool | ||||
|     { | ||||
|         if ($this->showScopes && 1 < $this->authorizationHelper->getReachableScopes( | ||||
|             $this->security->getUser(), | ||||
|             PersonDocumentVoter::CREATE, | ||||
|             $this->centerResolverManager->resolveCenters($person) | ||||
|         if ($this->showScopes && 1 < count( | ||||
|             $this->authorizationHelper->getReachableScopes( | ||||
|                 $this->security->getUser(), | ||||
|                 PersonDocumentVoter::CREATE, | ||||
|                 $this->centerResolverManager->resolveCenters($person) | ||||
|             ) | ||||
|         )) { | ||||
|             return true; | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,517 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Center; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\CenterRepositoryInterface; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository; | ||||
| use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; | ||||
| use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Workflow\Registry; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|  | ||||
|     private CenterResolverManagerInterface $centerResolverManager; | ||||
|  | ||||
|     private CenterRepositoryInterface $centerRepository; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private ScopeRepositoryInterface $scopeRepository; | ||||
|  | ||||
|     private Registry $registry; | ||||
|  | ||||
|     private static array $periodsIdsToDelete = []; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); | ||||
|         $this->centerRepository = self::$container->get(CenterRepositoryInterface::class); | ||||
|         $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); | ||||
|         $this->entityManager = self::$container->get(EntityManagerInterface::class); | ||||
|         $this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class); | ||||
|         $this->registry = self::$container->get(Registry::class); | ||||
|     } | ||||
|  | ||||
|     public static function tearDownAfterClass(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $em = self::$container->get(EntityManagerInterface::class); | ||||
|         $repository = self::$container->get(AccompanyingPeriodRepository::class); | ||||
|  | ||||
|         foreach (self::$periodsIdsToDelete as $id) { | ||||
|             if (null === $period = $repository->find($id)) { | ||||
|                 throw new \RuntimeException("period not found while trying to delete it"); | ||||
|             } | ||||
|  | ||||
|             foreach ($period->getParticipations() as $participation) { | ||||
|                 $em->remove($participation); | ||||
|             } | ||||
|             $em->remove($period); | ||||
|         } | ||||
|  | ||||
|         //$em->flush(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod | ||||
|      * @param list<array{center: Center, scopeOnRole: list<Scope>, scopeCanSeeConfidential: list<Scope>}> $centerScopes | ||||
|      * @param list<AccompanyingPeriod> $expectedContains | ||||
|      * @param list<AccompanyingPeriod> $expectedNotContains | ||||
|      */ | ||||
|     public function testFindByUserAndPostalCodesOpenedAccompanyingPeriod(User $user, User $searched, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $centers = []; | ||||
|  | ||||
|         foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { | ||||
|             $centers[spl_object_hash($center)] = $center; | ||||
|             $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center) | ||||
|                 ->willReturn($scopes); | ||||
|             $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center) | ||||
|                 ->willReturn($scopesCanSeeConfidential); | ||||
|         } | ||||
|         $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers)); | ||||
|  | ||||
|         $repository = new AccompanyingPeriodACLAwareRepository( | ||||
|             $this->accompanyingPeriodRepository, | ||||
|             $security->reveal(), | ||||
|             $authorizationHelper->reveal(), | ||||
|             $this->centerResolverManager | ||||
|         ); | ||||
|  | ||||
|         $actual = array_map( | ||||
|             fn (AccompanyingPeriod $period) => $period->getId(), | ||||
|             $repository->findByUserAndPostalCodesOpenedAccompanyingPeriod($searched, [], ['id' => 'DESC'], 20, 0) | ||||
|         ); | ||||
|  | ||||
|         foreach ($expectedContains as $expected) { | ||||
|             self::assertContains($expected->getId(), $actual, $message); | ||||
|         } | ||||
|         foreach ($expectedNotContains as $expected) { | ||||
|             self::assertNotContains($expected->getId(), $actual, $message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) | ||||
|             ->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         /** @var Person $person */ | ||||
|         [$person, $anotherPerson, $person2, $person3] = $this->entityManager | ||||
|             ->createQuery("SELECT p FROM " . Person::class . " p JOIN p.centerCurrent current_center") | ||||
|             ->setMaxResults(4) | ||||
|             ->getResult(); | ||||
|  | ||||
|         if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { | ||||
|             throw new \RuntimeException("no person found"); | ||||
|         } | ||||
|  | ||||
|         $scopes = $this->scopeRepository->findAll(); | ||||
|  | ||||
|         if (3 > count($scopes)) { | ||||
|             throw new \RuntimeException("not enough scopes for this test"); | ||||
|         } | ||||
|         $scopesCanSee = [ $scopes[0] ]; | ||||
|         $scopesGroup2 = [ $scopes[1] ]; | ||||
|  | ||||
|         $centers = $this->centerRepository->findActive(); | ||||
|         $aCenterNotAssociatedToPerson = array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0]; | ||||
|  | ||||
|         if (2 > count($centers)) { | ||||
|             throw new \RuntimeException("not enough centers for this test"); | ||||
|         } | ||||
|  | ||||
|         $period = $this->buildPeriod($person, $scopesCanSee, $user, true); | ||||
|         $period->setUser($user); | ||||
|  | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             $user, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [$period], | ||||
|             [], | ||||
|             "period should be visible with expected scopes", | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             $user, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesGroup2, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [], | ||||
|             [$period], | ||||
|             "period should not be visible without expected scopes", | ||||
|         ]; | ||||
|  | ||||
|         yield  [ | ||||
|             $anotherUser, | ||||
|             $user, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesGroup2, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|                 [ | ||||
|                     'center' => $aCenterNotAssociatedToPerson, | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [], | ||||
|             [$period], | ||||
|             "period should not be visible for user having right in another scope (with multiple centers)" | ||||
|         ]; | ||||
|  | ||||
|         $period = $this->buildPeriod($person, $scopesCanSee, $user, true); | ||||
|         $period->setUser($user); | ||||
|         $period->setConfidential(true); | ||||
|  | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             $user, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [], | ||||
|             [$period], | ||||
|             "period confidential should not be visible", | ||||
|         ]; | ||||
|  | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             $user, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => $scopesCanSee, | ||||
|                 ], | ||||
|             ], | ||||
|             [$period], | ||||
|             [], | ||||
|             "period confidential be visible if user has required scopes", | ||||
|         ]; | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByUndispatched | ||||
|      * @param list<array{center: Center, scopeOnRole: list<Scope>, scopeCanSeeConfidential: list<Scope>}> $centerScopes | ||||
|      * @param list<AccompanyingPeriod> $expectedContains | ||||
|      * @param list<AccompanyingPeriod> $expectedNotContains | ||||
|      */ | ||||
|     public function testFindByUndispatched(User $user, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $centers = []; | ||||
|  | ||||
|         foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { | ||||
|             $centers[spl_object_hash($center)] = $center; | ||||
|             $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center) | ||||
|                 ->willReturn($scopes); | ||||
|             $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center) | ||||
|                 ->willReturn($scopesCanSeeConfidential); | ||||
|         } | ||||
|         $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers)); | ||||
|  | ||||
|         $repository = new AccompanyingPeriodACLAwareRepository( | ||||
|             $this->accompanyingPeriodRepository, | ||||
|             $security->reveal(), | ||||
|             $authorizationHelper->reveal(), | ||||
|             $this->centerResolverManager | ||||
|         ); | ||||
|  | ||||
|         $actual = array_map( | ||||
|             fn (AccompanyingPeriod $period) => $period->getId(), | ||||
|             $repository->findByUnDispatched([], [], [], ['id' => 'DESC'], 20, 0) | ||||
|         ); | ||||
|  | ||||
|         foreach ($expectedContains as $expected) { | ||||
|             self::assertContains($expected->getId(), $actual, $message); | ||||
|         } | ||||
|         foreach ($expectedNotContains as $expected) { | ||||
|             self::assertNotContains($expected->getId(), $actual, $message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByUndispatched(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) | ||||
|             ->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         /** @var Person $person */ | ||||
|         [$person, $anotherPerson, $person2, $person3] = $this->entityManager | ||||
|             ->createQuery("SELECT p FROM " . Person::class . " p ") | ||||
|             ->setMaxResults(4) | ||||
|             ->getResult(); | ||||
|  | ||||
|         if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { | ||||
|             throw new \RuntimeException("no person found"); | ||||
|         } | ||||
|  | ||||
|         $scopes = $this->scopeRepository->findAll(); | ||||
|  | ||||
|         if (3 > count($scopes)) { | ||||
|             throw new \RuntimeException("not enough scopes for this test"); | ||||
|         } | ||||
|         $scopesCanSee = [ $scopes[0] ]; | ||||
|         $scopesGroup2 = [ $scopes[1] ]; | ||||
|  | ||||
|         $centers = $this->centerRepository->findActive(); | ||||
|  | ||||
|         if (2 > count($centers)) { | ||||
|             throw new \RuntimeException("not enough centers for this test"); | ||||
|         } | ||||
|  | ||||
|         $period = $this->buildPeriod($person, $scopesCanSee, $user, true); | ||||
|  | ||||
|  | ||||
|         // expected scope: can see the period | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [$period], | ||||
|             [], | ||||
|             "period should be visible with expected scopes", | ||||
|         ]; | ||||
|  | ||||
|         // no scope visible | ||||
|         yield [ | ||||
|             $anotherUser, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesGroup2, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [], | ||||
|             [$period], | ||||
|             "period should not be visible without expected scopes", | ||||
|         ]; | ||||
|  | ||||
|         // another center | ||||
|         yield  [ | ||||
|             $anotherUser, | ||||
|             [ | ||||
|                 [ | ||||
|                     'center' => $person->getCenter(), | ||||
|                     'scopeOnRole' => $scopesGroup2, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|                 [ | ||||
|                     'center' => array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0], | ||||
|                     'scopeOnRole' => $scopesCanSee, | ||||
|                     'scopeCanSeeConfidential' => [], | ||||
|                 ], | ||||
|             ], | ||||
|             [], | ||||
|             [$period], | ||||
|             "period should not be visible for user having right in another scope (with multiple centers)" | ||||
|         ]; | ||||
|  | ||||
|         $this->entityManager->flush(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * For testing this method, we mock the authorization helper to return different Scope that a user | ||||
|      * can see, or that a user can see confidential periods. | ||||
|      * | ||||
|      * @param array<Scope> $scopeUserCanSee | ||||
|      * @param array<Scope> $scopeUserCanSeeConfidential | ||||
|      * @param array<AccompanyingPeriod> $expectedPeriod | ||||
|      * @dataProvider provideDataForFindByPerson | ||||
|      */ | ||||
|     public function testFindByPersonTestUser(User $user, Person $person, array $scopeUserCanSee, array $scopeUserCanSeeConfidential, array $expectedPeriod, string $message): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, Argument::any()) | ||||
|             ->willReturn($scopeUserCanSee); | ||||
|         $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::any()) | ||||
|             ->willReturn($scopeUserCanSeeConfidential); | ||||
|  | ||||
|         $repository = new AccompanyingPeriodACLAwareRepository( | ||||
|             $this->accompanyingPeriodRepository, | ||||
|             $security->reveal(), | ||||
|             $authorizationHelper->reveal(), | ||||
|             $this->centerResolverManager | ||||
|         ); | ||||
|  | ||||
|         $actuals = $repository->findByPerson($person, AccompanyingPeriodVoter::SEE); | ||||
|         $expectedIds = array_map(fn (AccompanyingPeriod $period) => $period->getId(), $expectedPeriod); | ||||
|  | ||||
|         self::assertCount(count($expectedPeriod), $actuals, $message); | ||||
|         foreach ($actuals as $actual) { | ||||
|             self::assertContains($actual->getId(), $expectedIds); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function provideDataForFindByPerson(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) | ||||
|             ->setMaxResults(1)->getSingleResult()) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         [$person, $anotherPerson, $person2, $person3] = $this->entityManager | ||||
|             ->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) = 0") | ||||
|             ->setMaxResults(4) | ||||
|             ->getResult(); | ||||
|  | ||||
|         if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { | ||||
|             throw new \RuntimeException("no person found"); | ||||
|         } | ||||
|  | ||||
|         $scopes = $this->scopeRepository->findAll(); | ||||
|  | ||||
|         if (3 > count($scopes)) { | ||||
|             throw new \RuntimeException("not enough scopes for this test"); | ||||
|         } | ||||
|         $scopesCanSee = [ $scopes[0] ]; | ||||
|         $scopesGroup2 = [ $scopes[1] ]; | ||||
|  | ||||
|         // case: a period is in draft state | ||||
|         $period = $this->buildPeriod($person, $scopesCanSee, $user, false); | ||||
|  | ||||
|         yield [$user, $person, $scopesCanSee, [], [$period], "a user can see his period during draft state"]; | ||||
|  | ||||
|         // another user is not allowed to see this period, because it is in DRAFT state | ||||
|         yield [$anotherUser, $person, $scopesCanSee, [], [], "another user is not allowed to see the period of someone else in draft state"]; | ||||
|  | ||||
|         // the period is confirmed | ||||
|         $period = $this->buildPeriod($anotherPerson, $scopesCanSee, $user, true); | ||||
|  | ||||
|         // the other user can now see it | ||||
|         yield [$user, $anotherPerson, $scopesCanSee, [], [$period], "a user see his period when confirmed"]; | ||||
|         yield [$anotherUser, $anotherPerson, $scopesCanSee, [], [$period], "another user with required scopes is allowed to see the period when not draft"]; | ||||
|         yield [$anotherUser, $anotherPerson, $scopesGroup2, [], [], "another user without the required scopes is not allowed to see the period when not draft"]; | ||||
|  | ||||
|         // this period will be confidential | ||||
|         $period = $this->buildPeriod($person2, $scopesCanSee, $user, true); | ||||
|         $period->setConfidential(true)->setUser($user, true); | ||||
|  | ||||
|         yield [$user, $person2, $scopesCanSee, [], [$period], "a user see his period when confirmed and confidential with required scopes"]; | ||||
|         yield [$user, $person2, $scopesGroup2, [], [$period], "a user see his period when confirmed and confidential without required scopes"]; | ||||
|         yield [$anotherUser, $person2, $scopesCanSee, [], [], "a user don't see a confidential period, even if he has required scopes"]; | ||||
|         yield [$anotherUser, $person2, $scopesCanSee, $scopesCanSee, [$period], "a user see the period when confirmed and confidential if he has required scope to see the period"]; | ||||
|  | ||||
|         // period draft with creator = null | ||||
|         $period = $this->buildPeriod($person3, $scopesCanSee, null, false); | ||||
|         yield [$user, $person3, $scopesCanSee, [], [$period], "a user see a period when draft if no creator on the period"]; | ||||
|         $this->entityManager->flush(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param Person $person | ||||
|      * @param array<Scope> $scopes | ||||
|      * @return AccompanyingPeriod | ||||
|      */ | ||||
|     private function buildPeriod(Person $person, array $scopes, User|null $creator, bool $confirm): AccompanyingPeriod | ||||
|     { | ||||
|         $period = new AccompanyingPeriod(); | ||||
|         $period->addPerson($person); | ||||
|         if (null !== $creator) { | ||||
|             $period->setCreatedBy($creator); | ||||
|         } | ||||
|  | ||||
|         foreach ($scopes as $scope) { | ||||
|             $period->addScope($scope); | ||||
|         } | ||||
|  | ||||
|         $this->entityManager->persist($period); | ||||
|         self::$periodsIdsToDelete[] = $period->getId(); | ||||
|  | ||||
|         if ($confirm) { | ||||
|             $workflow = $this->registry->get($period, 'accompanying_period_lifecycle'); | ||||
|             $workflow->apply($period, 'confirm'); | ||||
|         } | ||||
|  | ||||
|         return $period; | ||||
|     } | ||||
| } | ||||
| @@ -329,8 +329,9 @@ CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE: Créer un parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE: Modifier un parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_FULL: Voir les détails, créer, supprimer et mettre à jour un parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_COURSE_REASSIGN_BULK: Réassigner les parcours en lot | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails  d'un parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails d'un parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_STATS: Statistiques sur les parcours d'accompagnement | ||||
| CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL: Voir les parcours confidentiels | ||||
|  | ||||
| CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE: Créer une action d'accompagnement | ||||
| CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE: Supprimer une action d'accompagnement | ||||
|   | ||||
		Reference in New Issue
	
	Block a user