apply new role on accompanying period

This commit is contained in:
Julien Fastré 2021-09-17 13:57:45 +02:00
parent cf40f38463
commit 74598ee926
19 changed files with 281 additions and 195 deletions

View File

@ -146,14 +146,7 @@ class ScopePickerType extends AbstractType
->setParameter('center', $center->getId())
// role constraints
->andWhere($qb->expr()->in('rs.role', ':roles'))
->setParameter(
'roles', \array_map(
function (Role $role) {
return $role->getRole();
},
$roles
)
)
->setParameter('roles', $roles)
// user contraint
->andWhere(':user MEMBER OF gc.users')
->setParameter('user', $this->tokenStorage->getToken()->getUser());

View File

@ -269,16 +269,12 @@ class AuthorizationHelper
/**
*
* @param Role $role
* @param Center $center
* @param Scope $circle
* @return Users
* @return User[]
*/
public function findUsersReaching(Role $role, Center $center, Scope $circle = null)
public function findUsersReaching(string $role, Center $center, Scope $circle = null): array
{
$parents = $this->getParentRoles($role);
$parents[] = $role;
$parentRolesString = \array_map(function(Role $r) { return $r->getRole(); }, $parents);
$qb = $this->em->createQueryBuilder();
$qb
@ -288,7 +284,7 @@ class AuthorizationHelper
->join('gc.permissionsGroup', 'pg')
->join('pg.roleScopes', 'rs')
->where('gc.center = :center')
->andWhere($qb->expr()->in('rs.role', $parentRolesString))
->andWhere($qb->expr()->in('rs.role', $parents))
;
$qb->setParameter('center', $center);
@ -322,21 +318,16 @@ class AuthorizationHelper
* which are registered into Chill are taken into account.
*
* @param Role $role
* @return Role[] the role which give access to the given $role
* @return string[] the role which give access to the given $role
*/
public function getParentRoles(Role $role)
public function getParentRoles($role): array
{
$parentRoles = [];
// transform the roles from role hierarchy from string to Role
$roles = \array_map(
function($string) {
return new Role($string);
},
\array_keys($this->hierarchy)
);
$roles = \array_keys($this->hierarchy);
foreach ($roles as $r) {
$childRoles = $this->roleHierarchy->getReachableRoleNames([$r->getRole()]);
$childRoles = $this->roleHierarchy->getReachableRoleNames([$r]);
if (\in_array($role, $childRoles)) {
$parentRoles[] = $r;

View File

@ -5,7 +5,7 @@ namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
class DefaultVoterHelper implements VoterHelperInterface
final class DefaultVoterHelper implements VoterHelperInterface
{
protected AuthorizationHelper $authorizationHelper;

View File

@ -18,7 +18,7 @@ final class DefaultVoterHelperGenerator implements VoterGeneratorInterface
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function addCheckFor($subject, $attributes): self
public function addCheckFor(?string $subject, array $attributes): self
{
$this->configuration[] = [$attributes, $subject];

View File

@ -9,7 +9,7 @@ interface VoterGeneratorInterface
* @param array $attributes an array of attributes
* @return $this
*/
public function addCheckFor(string $class, array $attributes): self;
public function addCheckFor(?string $class, array $attributes): self;
public function build(): VoterHelperInterface;
}

View File

@ -446,16 +446,9 @@ class AuthorizationHelperTest extends KernelTestCase
public function testGetParentRoles()
{
$parentRoles = $this->getAuthorizationHelper()
->getParentRoles(new Role('CHILL_INHERITED_ROLE_1'));
->getParentRoles('CHILL_INHERITED_ROLE_1');
$this->assertContains(
'CHILL_MASTER_ROLE',
\array_map(
function(Role $role) {
return $role->getRole();
},
$parentRoles
),
$this->assertContains('CHILL_MASTER_ROLE', $parentRoles,
"Assert that `CHILL_MASTER_ROLE` is a parent of `CHILL_INHERITED_ROLE_1`");
}

View File

@ -19,11 +19,11 @@ services:
autowire: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Authorization\DefaultVoterFactory:
Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory:
autowire: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Authorization\VoterFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterFactory'
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
chill.main.security.authorization.helper:
class: Chill\MainBundle\Security\Authorization\AuthorizationHelper

View File

@ -73,7 +73,7 @@ class AccompanyingCourseController extends Controller
}
}
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $period);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
$em->persist($period);
$em->flush();
@ -92,6 +92,8 @@ class AccompanyingCourseController extends Controller
*/
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
// compute some warnings
// get persons without household
$withoutHousehold = [];
@ -131,6 +133,8 @@ class AccompanyingCourseController extends Controller
*/
public function editAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/edit.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);
@ -146,6 +150,8 @@ class AccompanyingCourseController extends Controller
*/
public function historyAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/history.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);

View File

@ -23,7 +23,10 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\AccompanyingPeriodType;
@ -53,21 +56,24 @@ class AccompanyingPeriodController extends AbstractController
*/
protected $validator;
/**
* AccompanyingPeriodController constructor.
*
* @param EventDispatcherInterface $eventDispatcher
* @param ValidatorInterface $validator
*/
public function __construct(EventDispatcherInterface $eventDispatcher, ValidatorInterface $validator)
{
protected AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository;
public function __construct(
AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository,
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator
) {
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
}
public function listAction(int $person_id): Response
/**
* @ParamConverter("person", options={"id"="person_id"})
*/
public function listAction(Person $person): Response
{
$person = $this->_getPerson($person_id);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $person);
$event = new PrivacyEvent($person, [
'element_class' => AccompanyingPeriod::class,
@ -75,9 +81,10 @@ class AccompanyingPeriodController extends AbstractController
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
$accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository
->findByPerson($person, AccompanyingPeriodVoter::SEE);
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [
'accompanying_periods' => $accompanyingPeriods,
'person' => $person
]);

View File

@ -18,6 +18,7 @@
namespace Chill\PersonBundle\DependencyInjection;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -258,14 +259,26 @@ 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'),
PersonVoter::LISTS => [ ChillExportVoter::EXPORT ],
PersonVoter::STATS => [ ChillExportVoter::EXPORT ]
)
));
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
PersonVoter::UPDATE => [PersonVoter::SEE],
PersonVoter::CREATE => [PersonVoter::SEE],
PersonVoter::LISTS => [ChillExportVoter::EXPORT],
PersonVoter::STATS => [ChillExportVoter::EXPORT],
// accompanying period
AccompanyingPeriodVoter::SEE_DETAILS => [AccompanyingPeriodVoter::SEE],
AccompanyingPeriodVoter::CREATE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::DELETE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::EDIT => [AccompanyingPeriodVoter::SEE_DETAILS],
// give all ACL for FULL
AccompanyingPeriodVoter::FULL => [
AccompanyingPeriodVoter::SEE_DETAILS,
AccompanyingPeriodVoter::CREATE,
AccompanyingPeriodVoter::EDIT,
AccompanyingPeriodVoter::DELETE
]
]
]);
}
/**

View File

@ -18,7 +18,10 @@
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -44,11 +47,15 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
*/
protected $translator;
private Security $security;
public function __construct(
$showAccompanyingPeriod,
ParameterBagInterface $parameterBag,
Security $security,
TranslatorInterface $translator
) {
$this->showAccompanyingPeriod = $showAccompanyingPeriod;
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
$this->security = $security;
$this->translator = $translator;
}
@ -84,7 +91,9 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
'order' => 99999
]);
if ($this->showAccompanyingPeriod === 'visible') {
if ($this->showAccompanyingPeriod === 'visible'
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])
) {
$menu->addChild($this->translator->trans('Accompanying period list'), [
'route' => 'chill_person_accompanying_period_list',
'routeParameters' => [

View File

@ -0,0 +1,58 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security;
final class AccompanyingPeriodACLAwareRepository
{
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private Security $security;
private AuthorizationHelper $authorizationHelper;
private CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, AuthorizationHelper $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher)
{
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function findByPerson(
Person $person,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array {
dump(__METHOD__);
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
$scopes = $this->authorizationHelper
->getReachableCircles($this->security->getUser(), $role,
$this->centerResolverDispatcher->resolveCenter($person));
if (0 === count($scopes)) {
return [];
}
$qb
->join('ap.participations', 'participation')
->where($qb->expr()->eq('participation.person', ':person'))
->setParameter('person', $person)
;
// add join condition for scopes
$orx = $qb->expr()->orX();
foreach ($scopes as $key => $scope) {
$orx->add($qb->expr()->in('ap.scopes', ':scope_'.$key));
$qb->setParameter('scope_'.$key, $scope);
}
$qb->andWhere($orx);
return $qb->getQuery()->getResult();
}
}

View File

@ -59,6 +59,11 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->findOneBy($criteria);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function getClassName()
{
return AccompanyingPeriod::class;

View File

@ -60,7 +60,7 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getQueryResult($qb, $simplify, $limit, $start);
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
}
/**
@ -119,7 +119,7 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getCountQueryResult($qb);
return $this->getCountQueryResult($qb,'p');
}
/**

View File

@ -4,63 +4,92 @@ namespace Chill\PersonBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\Center;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
protected AuthorizationHelper $helper;
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
/**
* details are for seeing:
*
* * SocialIssues
*/
public const SEE_DETAILS = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS';
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
public const EDIT = 'CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE';
public const DELETE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_DELETE';
/**
* @param AuthorizationHelper $helper
* Give all the right above
*/
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
public const FULL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_FULL';
public const ALL = [
self::SEE,
self::SEE_DETAILS,
self::CREATE,
self::EDIT,
self::DELETE,
self::FULL,
];
private VoterHelperInterface $voterHelper;
private Security $security;
public function __construct(
Security $security,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->security = $security;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::CREATE])
->addCheckFor(AccompanyingPeriod::class, self::ALL)
->addCheckFor(Person::class, [self::SEE])
->build();
}
protected function supports($attribute, $subject)
{
return $subject instanceof AccompanyingPeriod;
return $this->voterHelper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
// TODO take scopes into account
if (count($subject->getPersons()) === 0) {
return true;
}
foreach ($subject->getPersons() as $person) {
// give access as soon as on center is reachable
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {
if ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
// only creator can see, edit, delete, etc.
if ($subject->getCreatedBy() === $token->getUser()
|| NULL === $subject->getCreatedBy()) {
return true;
}
return false;
}
// if confidential, only the referent can see it
if ($subject->isConfidential()) {
return $token->getUser() === $subject->getUser();
}
}
private function getAttributes()
{
return [
self::SEE
];
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
public function getRoles()
{
return $this->getAttributes();
return self::ALL;
}
public function getRolesWithoutScope()
@ -70,7 +99,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRole
public function getRolesWithHierarchy()
{
return [ 'Person' => $this->getRoles() ];
return [ 'Accompanying period' => $this->getRoles() ];
}
}

View File

@ -40,19 +40,11 @@ class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte
const LISTS = 'CHILL_PERSON_LISTS';
const DUPLICATE = 'CHILL_PERSON_DUPLICATE';
protected AuthorizationHelper $helper;
protected CenterResolverDispatcher $centerResolverDispatcher;
protected VoterHelperInterface $voter;
public function __construct(
AuthorizationHelper $helper,
CenterResolverDispatcher $centerResolverDispatcher,
VoterHelperFactoryInterface $voterFactory
) {
$this->helper = $helper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->voter = $voterFactory
->generate(self::class)
->addCheckFor(Center::class, [self::STATS, self::LISTS, self::DUPLICATE])

View File

@ -12,9 +12,8 @@ services:
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingPeriodController:
arguments:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
autowire: true
autoconfigure: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\PersonAddressController:

View File

@ -19,14 +19,7 @@ services:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
arguments:
$showAccompanyingPeriod: '%chill_person.accompanying_period%'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
autowire: true
autoconfigure: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
# arguments:
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }

View File

@ -7,8 +7,7 @@ services:
- { name: chill.role }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
arguments:
- "@chill.main.security.authorization.helper"
autowire: true
tags:
- { name: security.voter }
- { name: chill.role }