upgrade voter and acl for activities and implement autoconfiguration for

ChillProvideRole interface
This commit is contained in:
Julien Fastré 2021-09-20 13:03:59 +02:00
parent b6c58a5c31
commit 120f7d8026
11 changed files with 236 additions and 101 deletions

View File

@ -22,6 +22,9 @@
namespace Chill\ActivityBundle\Controller; namespace Chill\ActivityBundle\Controller;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@ -53,12 +56,16 @@ class ActivityController extends AbstractController
protected SerializerInterface $serializer; protected SerializerInterface $serializer;
protected ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
public function __construct( public function __construct(
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
EventDispatcherInterface $eventDispatcher, EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper, AuthorizationHelper $authorizationHelper,
LoggerInterface $logger, LoggerInterface $logger,
SerializerInterface $serializer SerializerInterface $serializer
) { ) {
$this->activityACLAwareRepository = $activityACLAwareRepository;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->logger = $logger; $this->logger = $logger;
@ -77,13 +84,9 @@ class ActivityController extends AbstractController
[$person, $accompanyingPeriod] = $this->getEntity($request); [$person, $accompanyingPeriod] = $this->getEntity($request);
if ($person instanceof Person) { if ($person instanceof Person) {
$reachableScopes = $this->authorizationHelper $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
->getReachableCircles($this->getUser(), new Role('CHILL_ACTIVITY_SEE'), $activities = $this->activityACLAwareRepository
$person->getCenter()); ->findByPerson($person, ActivityVoter::SEE, 0, null);
$activities = $em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes)
;
$event = new PrivacyEvent($person, array( $event = new PrivacyEvent($person, array(
'element_class' => Activity::class, 'element_class' => Activity::class,
@ -93,10 +96,10 @@ class ActivityController extends AbstractController
$view = 'ChillActivityBundle:Activity:listPerson.html.twig'; $view = 'ChillActivityBundle:Activity:listPerson.html.twig';
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
$activities = $em->getRepository('ChillActivityBundle:Activity')->findBy( $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
['accompanyingPeriod' => $accompanyingPeriod],
['date' => 'DESC'], $activities = $this->activityACLAwareRepository
); ->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE);
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
} }

View File

@ -23,6 +23,8 @@
namespace Chill\ActivityBundle\Repository; namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
@ -33,9 +35,10 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
final class ActivityACLAwareRepository final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
{ {
private AuthorizationHelper $authorizationHelper; private AuthorizationHelper $authorizationHelper;
@ -45,16 +48,63 @@ final class ActivityACLAwareRepository
private EntityManagerInterface $em; private EntityManagerInterface $em;
private Security $security;
private CenterResolverDispatcher $centerResolverDispatcher;
public function __construct( public function __construct(
AuthorizationHelper $authorizationHelper, AuthorizationHelper $authorizationHelper,
CenterResolverDispatcher $centerResolverDispatcher,
TokenStorageInterface $tokenStorage, TokenStorageInterface $tokenStorage,
ActivityRepository $repository, ActivityRepository $repository,
EntityManagerInterface $em EntityManagerInterface $em,
Security $security
) { ) {
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
$this->repository = $repository; $this->repository = $repository;
$this->em = $em; $this->em = $em;
$this->security = $security;
}
/**
* @param Person $person
* @param string $role
* @param int|null $start
* @param int|null $limit
* @param array $orderBy
* @return array|Activity[]
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($person);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
}
$reachableScopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
return $this->em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start);
;
}
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($period);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
}
$scopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
return $this->em->getRepository(Activity::class)
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
} }
public function queryTimelineIndexer(string $context, array $args = []): array public function queryTimelineIndexer(string $context, array $args = []): array

View File

@ -0,0 +1,19 @@
<?php
namespace Chill\ActivityBundle\Repository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
interface ActivityACLAwareRepositoryInterface
{
/**
* @return array|Activity[]
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
/**
* @return array|Activity[]
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
}

View File

@ -23,6 +23,8 @@
namespace Chill\ActivityBundle\Repository; namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@ -39,15 +41,22 @@ class ActivityRepository extends ServiceEntityRepository
parent::__construct($registry, Activity::class); parent::__construct($registry, Activity::class);
} }
public function findByPersonImplied($person, array $scopes, $orderBy = [ 'date' => 'DESC'], $limit = 100, $offset = 0) /**
* @param $person
* @param array $scopes
* @param string[] $orderBy
* @param int $limit
* @param int $offset
* @return array|Activity[]
*/
public function findByPersonImplied(Person $person, array $scopes, ?array $orderBy = [ 'date' => 'DESC'], ?int $limit = 100, ?int $offset = 0): array
{ {
$qb = $this->createQueryBuilder('a'); $qb = $this->createQueryBuilder('a');
$qb->select('a'); $qb->select('a');
$qb $qb
// TODO add acl ->where($qb->expr()->in('a.scope', ':scopes'))
//->where($qb->expr()->in('a.scope', ':scopes')) ->setParameter('scopes', $scopes)
//->setParameter('scopes', $scopes)
->andWhere( ->andWhere(
$qb->expr()->orX( $qb->expr()->orX(
$qb->expr()->eq('a.person', ':person'), $qb->expr()->eq('a.person', ':person'),
@ -61,6 +70,55 @@ class ActivityRepository extends ServiceEntityRepository
$qb->addOrderBy('a.'.$k, $dir); $qb->addOrderBy('a.'.$k, $dir);
} }
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()
->getResult();
}
/**
* @param AccompanyingPeriod $period
* @param array $scopes
* @param int|null $limit
* @param int|null $offset
* @param array|string[] $orderBy
* @return array|Activity[]
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, array $scopes, ?bool $allowNullScope = false, ?int $limit = 100, ?int $offset = 0, array $orderBy = ['date' => 'desc']): array
{
$qb = $this->createQueryBuilder('a');
$qb->select('a');
if (!$allowNullScope) {
$qb
->where($qb->expr()->in('a.scope', ':scopes'))
->setParameter('scopes', $scopes)
;
} else {
$qb
->where(
$qb->expr()->orX(
$qb->expr()->in('a.scope', ':scopes'),
$qb->expr()->isNull('a.scope')
)
)
->setParameter('scopes', $scopes)
;
}
$qb
->andWhere(
$qb->expr()->eq('a.accompanyingPeriod', ':period')
)
->setParameter('period', $period)
;
foreach ($orderBy as $k => $dir) {
$qb->addOrderBy('a.'.$k, $dir);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery() return $qb->getQuery()
->getResult(); ->getResult();
} }

View File

@ -19,6 +19,11 @@
namespace Chill\ActivityBundle\Security\Authorization; namespace Chill\ActivityBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter; use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
@ -28,11 +33,10 @@ use Chill\MainBundle\Entity\User;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
/** /**
* * Voter for Activity class
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/ */
class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{ {
@ -41,30 +45,37 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
const SEE_DETAILS = 'CHILL_ACTIVITY_SEE_DETAILS'; const SEE_DETAILS = 'CHILL_ACTIVITY_SEE_DETAILS';
const UPDATE = 'CHILL_ACTIVITY_UPDATE'; const UPDATE = 'CHILL_ACTIVITY_UPDATE';
const DELETE = 'CHILL_ACTIVITY_DELETE'; const DELETE = 'CHILL_ACTIVITY_DELETE';
const FULL = 'CHILL_ACTIVITY_FULL';
/** private const ALL = [
* self::CREATE,
* @var AuthorizationHelper self::SEE,
*/ self::UPDATE,
protected $helper; self::DELETE,
self::SEE_DETAILS,
self::FULL
];
public function __construct(AuthorizationHelper $helper) protected VoterHelperInterface $voterHelper;
{
$this->helper = $helper; protected Security $security;
public function __construct(
Security $security,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->security = $security;
$this->voterHelper = $voterHelperFactory->generate(self::class)
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(AccompanyingPeriod::class, [self::SEE, self::CREATE])
->addCheckFor(Activity::class, self::ALL)
->build();
} }
protected function supports($attribute, $subject) protected function supports($attribute, $subject)
{ {
if ($subject instanceof Activity) { return $this->voterHelper->supports($attribute, $subject);
return \in_array($attribute, $this->getAttributes());
} elseif ($subject instanceof Person) {
return $attribute === self::SEE
||
$attribute === self::CREATE;
} else {
return false;
}
} }
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
@ -73,31 +84,33 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
return false; return false;
} }
if ($subject instanceof Person) { if ($subject instanceof Activity) {
$centers = $this->helper->getReachableCenters($token->getUser(), new Role($attribute)); if ($subject->getPerson() instanceof Person) {
// the context is person: we must have the right to see the person
return \in_array($subject->getCenter(), $centers); if (!$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) {
return false;
}
} elseif ($subject->getAccompanyingPeriod() instanceof AccompanyingPeriod) {
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject->getAccompanyingPeriod())) {
return false;
}
} else {
throw new \RuntimeException("could not determine context of activity");
}
} }
/* @var $subject Activity */ return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
return $this->helper->userHasAccess($token->getUser(), $subject, $attribute);
}
private function getAttributes()
{
return [ self::CREATE, self::SEE, self::UPDATE, self::DELETE,
self::SEE_DETAILS ];
} }
public function getRoles() public function getRoles()
{ {
return $this->getAttributes(); return self::ALL;
} }
public function getRolesWithoutScope() public function getRolesWithoutScope()
{ {
return array(); return [];
} }

View File

@ -1,20 +1,4 @@
services: services:
chill.activity.security.authorization.activity_voter:
class: Chill\ActivityBundle\Security\Authorization\ActivityVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.activity.security.authorization.activity_stats_voter:
class: Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.activity.timeline: chill.activity.timeline:
class: Chill\ActivityBundle\Timeline\TimelineActivityProvider class: Chill\ActivityBundle\Timeline\TimelineActivityProvider
@ -38,3 +22,8 @@ services:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
resource: '../Notification' resource: '../Notification'
Chill\ActivityBundle\Security\Authorization\:
resource: '../Security/Authorization/'
autowire: true
autoconfigure: true

View File

@ -1,8 +1,4 @@
services: services:
Chill\ActivityBundle\Controller\ActivityController: Chill\ActivityBundle\Controller\ActivityController:
arguments: autowire: true
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$logger: '@chill.main.logger'
$serializer: '@Symfony\Component\Serializer\SerializerInterface'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']

View File

@ -24,9 +24,7 @@ services:
- '@Doctrine\Persistence\ManagerRegistry' - '@Doctrine\Persistence\ManagerRegistry'
Chill\ActivityBundle\Repository\ActivityACLAwareRepository: Chill\ActivityBundle\Repository\ActivityACLAwareRepository:
arguments: autowire: true
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' autoconfigure: true
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface: '@Chill\ActivityBundle\Repository\ActivityACLAwareRepository'
$repository: '@Chill\ActivityBundle\Repository\ActivityRepository'
$em: '@Doctrine\ORM\EntityManagerInterface'

View File

@ -4,6 +4,8 @@ namespace Chill\MainBundle;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchInterface; use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverInterface; use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface; use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -30,6 +32,8 @@ class ChillMainBundle extends Bundle
$container->registerForAutoconfiguration(LocalMenuBuilderInterface::class) $container->registerForAutoconfiguration(LocalMenuBuilderInterface::class)
->addTag('chill.menu_builder'); ->addTag('chill.menu_builder');
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
->addTag('chill.role');
$container->registerForAutoconfiguration(CenterResolverInterface::class) $container->registerForAutoconfiguration(CenterResolverInterface::class)
->addTag('chill_main.center_resolver'); ->addTag('chill_main.center_resolver');
$container->registerForAutoconfiguration(ScopeResolverInterface::class) $container->registerForAutoconfiguration(ScopeResolverInterface::class)

View File

@ -265,11 +265,11 @@ class AuthorizationHelper
* @deprecated Use getReachableCircles * @deprecated Use getReachableCircles
* *
* @param User $user * @param User $user
* @param Role $role * @param string role
* @param Center $center * @param Center|Center[] $center
* @return Scope[] * @return Scope[]
*/ */
public function getReachableScopes(User $user, $role, Center $center) public function getReachableScopes(User $user, $role, $center)
{ {
if ($role instanceof Role) { if ($role instanceof Role) {
$role = $role->getRole(); $role = $role->getRole();
@ -283,15 +283,24 @@ class AuthorizationHelper
* *
* @param User $user * @param User $user
* @param string|Role $role * @param string|Role $role
* @param Center $center * @param Center|Center[] $center
* @return Scope[] * @return Scope[]
*/ */
public function getReachableCircles(User $user, $role, Center $center) public function getReachableCircles(User $user, $role, $center)
{ {
$scopes = [];
if (is_iterable($center)) {
foreach ($center as $c) {
$scopes = \array_merge($scopes, $this->getReachableCircles($user, $role, $c));
}
return $scopes;
}
if ($role instanceof Role) { if ($role instanceof Role) {
$role = $role->getRole(); $role = $role->getRole();
} }
$scopes = array();
foreach ($user->getGroupCenters() as $groupCenter){ foreach ($user->getGroupCenters() as $groupCenter){
if ($center->getId() === $groupCenter->getCenter()->getId()) { if ($center->getId() === $groupCenter->getCenter()->getId()) {

View File

@ -1050,7 +1050,7 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
} }
public function getCenters(): ?iterable public function getCenters(): ?iterable
{dump(__METHOD__); {
foreach ($this->getPersons() as $person) { foreach ($this->getPersons() as $person) {
if (!in_array($person->getCenter(), $centers ?? []) if (!in_array($person->getCenter(), $centers ?? [])
&& NULL !== $person->getCenter()) { && NULL !== $person->getCenter()) {
@ -1058,10 +1058,6 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
} }
} }
dump($centers);
return $centers ?? null; return $centers ?? null;
} }
} }