mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '286-storedobject-voter' into 'signature-app-master'
Adjust behavoir of voters for stored objects See merge request Chill-Projet/chill-bundles!701
This commit is contained in:
commit
db73dcffc7
7
.changes/unreleased/Feature-20240614-153537.yaml
Normal file
7
.changes/unreleased/Feature-20240614-153537.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
|
||||||
|
and delete possibilities to users related to the activity, social action or workflow
|
||||||
|
entity.
|
||||||
|
time: 2024-06-14T15:35:37.582159301+02:00
|
||||||
|
custom:
|
||||||
|
Issue: "286"
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
|||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
|
|||||||
* @method Activity[] findAll()
|
* @method Activity[] findAll()
|
||||||
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
*/
|
*/
|
||||||
class ActivityRepository extends ServiceEntityRepository
|
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
@ -97,4 +99,16 @@ class ActivityRepository extends ServiceEntityRepository
|
|||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('a');
|
||||||
|
$query = $qb
|
||||||
|
->leftJoin('a.documents', 'ad')
|
||||||
|
->where('ad.id = :storedObjectId')
|
||||||
|
->setParameter('storedObjectId', $storedObject->getId())
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<?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 Chill\ActivityBundle\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ActivityRepository $repository,
|
||||||
|
Security $security,
|
||||||
|
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return Activity::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||||
|
{
|
||||||
|
return match ($attribute) {
|
||||||
|
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
|
||||||
|
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -89,6 +89,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
|||||||
$g = new SignedUrlPost(
|
$g = new SignedUrlPost(
|
||||||
$url = $this->generateUrl($object_name),
|
$url = $this->generateUrl($object_name),
|
||||||
$expires,
|
$expires,
|
||||||
|
$object_name,
|
||||||
$this->max_post_file_size,
|
$this->max_post_file_size,
|
||||||
$max_file_count,
|
$max_file_count,
|
||||||
$submit_delay,
|
$submit_delay,
|
||||||
@ -127,7 +128,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
|||||||
];
|
];
|
||||||
$url = $url.'?'.\http_build_query($args);
|
$url = $url.'?'.\http_build_query($args);
|
||||||
|
|
||||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
$signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);
|
||||||
|
|
||||||
$this->event_dispatcher->dispatch(
|
$this->event_dispatcher->dispatch(
|
||||||
new TempUrlGenerateEvent($signature)
|
new TempUrlGenerateEvent($signature)
|
||||||
|
@ -21,6 +21,8 @@ readonly class SignedUrl
|
|||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public string $url,
|
public string $url,
|
||||||
public \DateTimeImmutable $expires,
|
public \DateTimeImmutable $expires,
|
||||||
|
#[Serializer\Groups(['read'])]
|
||||||
|
public string $object_name,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
|
@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
string $url,
|
string $url,
|
||||||
\DateTimeImmutable $expires,
|
\DateTimeImmutable $expires,
|
||||||
|
string $object_name,
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public int $max_file_size,
|
public int $max_file_size,
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl
|
|||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public string $signature,
|
public string $signature,
|
||||||
) {
|
) {
|
||||||
parent::__construct('POST', $url, $expires);
|
parent::__construct('POST', $url, $expires, $object_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection;
|
|||||||
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||||
@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
|||||||
|
|
||||||
$container->setParameter('chill_doc_store', $config);
|
$container->setParameter('chill_doc_store', $config);
|
||||||
|
|
||||||
|
$container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
$loader->load('services/controller.yaml');
|
$loader->load('services/controller.yaml');
|
||||||
@ -42,6 +45,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
|||||||
$loader->load('services/fixtures.yaml');
|
$loader->load('services/fixtures.yaml');
|
||||||
$loader->load('services/form.yaml');
|
$loader->load('services/form.yaml');
|
||||||
$loader->load('services/templating.yaml');
|
$loader->load('services/templating.yaml');
|
||||||
|
$loader->load('services/security.yaml');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepend(ContainerBuilder $container)
|
public function prepend(ContainerBuilder $container)
|
||||||
|
@ -12,13 +12,14 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Repository;
|
namespace Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
class AccompanyingCourseDocumentRepository implements ObjectRepository
|
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
@ -45,6 +46,16 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
|||||||
return $qb->getQuery()->getSingleScalarResult();
|
return $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('d');
|
||||||
|
$query = $qb->where('d.object = :storedObject')
|
||||||
|
->setParameter('storedObject', $storedObject)
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function find($id): ?AccompanyingCourseDocument
|
public function find($id): ?AccompanyingCourseDocument
|
||||||
{
|
{
|
||||||
return $this->repository->find($id);
|
return $this->repository->find($id);
|
||||||
@ -55,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||||
{
|
{
|
||||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
}
|
}
|
||||||
@ -65,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
|||||||
return $this->findOneBy($criteria);
|
return $this->findOneBy($criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClassName()
|
public function getClassName(): string
|
||||||
{
|
{
|
||||||
return AccompanyingCourseDocument::class;
|
return AccompanyingCourseDocument::class;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
|
||||||
|
interface AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
|
||||||
|
}
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Repository;
|
namespace Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
@ -19,7 +20,7 @@ use Doctrine\Persistence\ObjectRepository;
|
|||||||
/**
|
/**
|
||||||
* @template ObjectRepository<PersonDocument::class>
|
* @template ObjectRepository<PersonDocument::class>
|
||||||
*/
|
*/
|
||||||
readonly class PersonDocumentRepository implements ObjectRepository
|
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
|
|||||||
{
|
{
|
||||||
return PersonDocument::class;
|
return PersonDocument::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('d');
|
||||||
|
$query = $qb->where('d.object = :storedObject')
|
||||||
|
->setParameter('storedObject', $storedObject)
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
|
{{ document.object|chill_document_button_group(document.title) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||||
@ -90,7 +90,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
{{ document.object|chill_document_button_group(document.title) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||||
|
@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function supports($attribute, $subject): bool
|
public function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
return $this->voterHelper->supports($attribute, $subject);
|
return $this->voterHelper->supports($attribute, $subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
if (!$token->getUser() instanceof User) {
|
if (!$token->getUser() instanceof User) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@ -22,6 +23,7 @@ final class AsyncUploadVoter extends Voter
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
|
private readonly StoredObjectRepository $storedObjectRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
protected function supports($attribute, $subject): bool
|
protected function supports($attribute, $subject): bool
|
||||||
@ -32,10 +34,16 @@ final class AsyncUploadVoter extends Voter
|
|||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
/** @var SignedUrl $subject */
|
/** @var SignedUrl $subject */
|
||||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]);
|
||||||
|
|
||||||
|
return match ($subject->method) {
|
||||||
|
'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
|
||||||
|
'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
|
||||||
|
'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,9 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Voter for the content of a stored object.
|
* Voter for the content of a stored object.
|
||||||
@ -23,6 +23,8 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||||||
*/
|
*/
|
||||||
class StoredObjectVoter extends Voter
|
class StoredObjectVoter extends Voter
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) {}
|
||||||
|
|
||||||
protected function supports($attribute, $subject): bool
|
protected function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
||||||
@ -32,24 +34,22 @@ class StoredObjectVoter extends Voter
|
|||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
/** @var StoredObject $subject */
|
/** @var StoredObject $subject */
|
||||||
if (
|
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
|
||||||
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
|
||||||
|| $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
// Loop through context-specific voters
|
||||||
) {
|
foreach ($this->storedObjectVoters as $storedObjectVoter) {
|
||||||
|
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
|
||||||
|
return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User role-based fallback
|
||||||
|
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
|
||||||
|
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
|
||||||
|
// is potentially detached from an existing entity.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$askedRole = StoredObjectRoleEnum::from($attribute);
|
|
||||||
$tokenRoleAuthorization =
|
|
||||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
|
|
||||||
|
|
||||||
return match ($askedRole) {
|
|
||||||
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
|
|
||||||
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||||
|
{
|
||||||
|
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return class-string
|
||||||
|
*/
|
||||||
|
abstract protected function getClass(): string;
|
||||||
|
|
||||||
|
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
|
||||||
|
|
||||||
|
abstract protected function canBeAssociatedWithWorkflow(): bool;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Security $security,
|
||||||
|
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||||
|
{
|
||||||
|
$class = $this->getClass();
|
||||||
|
|
||||||
|
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
// Retrieve the related accompanying course document
|
||||||
|
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||||
|
|
||||||
|
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
|
||||||
|
$voterAttribute = $this->attributeToRole($attribute);
|
||||||
|
|
||||||
|
if (false === $this->security->isGranted($voterAttribute, $entity)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->canBeAssociatedWithWorkflow()) {
|
||||||
|
if (null === $this->workflowDocumentService) {
|
||||||
|
throw new \LogicException('Provide a workflow document service');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
|
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||||
|
Security $security,
|
||||||
|
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||||
|
{
|
||||||
|
return match ($attribute) {
|
||||||
|
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
|
||||||
|
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return AccompanyingCourseDocument::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PersonDocumentRepository $repository,
|
||||||
|
Security $security,
|
||||||
|
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return PersonDocument::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||||
|
{
|
||||||
|
return match ($attribute) {
|
||||||
|
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
|
||||||
|
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
|
interface StoredObjectVoterInterface
|
||||||
|
{
|
||||||
|
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
||||||
|
|
||||||
|
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
|
||||||
|
}
|
@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
@ -32,7 +33,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
||||||
private readonly UrlGeneratorInterface $urlGenerator
|
private readonly UrlGeneratorInterface $urlGenerator,
|
||||||
|
private readonly Security $security
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
@ -55,13 +57,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
// deprecated property
|
// deprecated property
|
||||||
$datas['creationDate'] = $datas['createdAt'];
|
$datas['creationDate'] = $datas['createdAt'];
|
||||||
|
|
||||||
$canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
|
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
||||||
$canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
|
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
||||||
|
|
||||||
if ($canDavSee || $canDavEdit) {
|
if ($canSee || $canEdit) {
|
||||||
$accessToken = $this->JWTDavTokenProvider->createToken(
|
$accessToken = $this->JWTDavTokenProvider->createToken(
|
||||||
$object,
|
$object,
|
||||||
$canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
);
|
);
|
||||||
|
|
||||||
$datas['_links'] = [
|
$datas['_links'] = [
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Service;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class WorkflowStoredObjectPermissionHelper
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
||||||
|
|
||||||
|
public function notBlockedByWorkflow(object $entity): bool
|
||||||
|
{
|
||||||
|
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
|
foreach ($workflows as $workflow) {
|
||||||
|
if ($workflow->isFinal()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@ -128,6 +129,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
private NormalizerInterface $normalizer,
|
private NormalizerInterface $normalizer,
|
||||||
private JWTDavTokenProviderInterface $davTokenProvider,
|
private JWTDavTokenProviderInterface $davTokenProvider,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
private Security $security,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,8 +150,10 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
* @throws \Twig\Error\RuntimeError
|
* @throws \Twig\Error\RuntimeError
|
||||||
* @throws \Twig\Error\SyntaxError
|
* @throws \Twig\Error\SyntaxError
|
||||||
*/
|
*/
|
||||||
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
|
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $showEditButtons = true, array $options = []): string
|
||||||
{
|
{
|
||||||
|
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons;
|
||||||
|
|
||||||
$accessToken = $this->davTokenProvider->createToken(
|
$accessToken = $this->davTokenProvider->createToken(
|
||||||
$document,
|
$document,
|
||||||
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
|
@ -122,7 +122,8 @@ class TempUrlOpenstackGeneratorTest extends TestCase
|
|||||||
$signedUrl = new SignedUrl(
|
$signedUrl = new SignedUrl(
|
||||||
'GET',
|
'GET',
|
||||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1702043543')
|
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||||
|
$objectName
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($baseUrls as $baseUrl) {
|
foreach ($baseUrls as $baseUrl) {
|
||||||
@ -153,6 +154,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase
|
|||||||
$signedUrl = new SignedUrlPost(
|
$signedUrl = new SignedUrlPost(
|
||||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||||
|
$objectName,
|
||||||
150,
|
150,
|
||||||
1,
|
1,
|
||||||
1800,
|
1800,
|
||||||
|
@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase
|
|||||||
{
|
{
|
||||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||||
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
||||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
|
||||||
|
|
||||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||||
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
||||||
|
@ -73,6 +73,7 @@ class AsyncUploadControllerTest extends TestCase
|
|||||||
return new SignedUrlPost(
|
return new SignedUrlPost(
|
||||||
'https://object.store.example',
|
'https://object.store.example',
|
||||||
new \DateTimeImmutable('1 hour'),
|
new \DateTimeImmutable('1 hour'),
|
||||||
|
'abc',
|
||||||
150,
|
150,
|
||||||
1,
|
1,
|
||||||
1800,
|
1800,
|
||||||
@ -87,7 +88,8 @@ class AsyncUploadControllerTest extends TestCase
|
|||||||
return new SignedUrl(
|
return new SignedUrl(
|
||||||
$method,
|
$method,
|
||||||
'https://object.store.example',
|
'https://object.store.example',
|
||||||
new \DateTimeImmutable('1 hour')
|
new \DateTimeImmutable('1 hour'),
|
||||||
|
$object_name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@ use Prophecy\PhpUnit\ProphecyTrait;
|
|||||||
use Symfony\Component\Form\PreloadedExtension;
|
use Symfony\Component\Form\PreloadedExtension;
|
||||||
use Symfony\Component\Form\Test\TypeTestCase;
|
use Symfony\Component\Form\Test\TypeTestCase;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||||
use Symfony\Component\Serializer\Serializer;
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
|
||||||
@ -80,11 +81,15 @@ class StoredObjectTypeTest extends TypeTestCase
|
|||||||
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
|
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
|
||||||
->willReturn('http://url/fake');
|
->willReturn('http://url/fake');
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted(Argument::cetera())->willReturn(true);
|
||||||
|
|
||||||
$serializer = new Serializer(
|
$serializer = new Serializer(
|
||||||
[
|
[
|
||||||
new StoredObjectNormalizer(
|
new StoredObjectNormalizer(
|
||||||
$jwtTokenProvider->reveal(),
|
$jwtTokenProvider->reveal(),
|
||||||
$urlGenerator->reveal(),
|
$urlGenerator->reveal(),
|
||||||
|
$security->reveal()
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
<?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 Chill\DocStoreBundle\Tests\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class AbstractStoredObjectVoterTest extends TestCase
|
||||||
|
{
|
||||||
|
private AssociatedEntityToStoredObjectInterface $repository;
|
||||||
|
private Security $security;
|
||||||
|
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
|
||||||
|
$this->security = $this->createMock(Security::class);
|
||||||
|
$this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
// Anonymous class extending the abstract class
|
||||||
|
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||||
|
public function __construct(
|
||||||
|
private bool $canBeAssociatedWithWorkflow,
|
||||||
|
private AssociatedEntityToStoredObjectInterface $repository,
|
||||||
|
Security $security,
|
||||||
|
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole($attribute): string
|
||||||
|
{
|
||||||
|
return 'SOME_ROLE';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return \stdClass::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return $this->canBeAssociatedWithWorkflow;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setupMockObjects(): array
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$token = $this->createMock(TokenInterface::class);
|
||||||
|
$subject = new StoredObject();
|
||||||
|
$entity = new \stdClass();
|
||||||
|
|
||||||
|
return [$user, $token, $subject, $entity];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
|
||||||
|
{
|
||||||
|
// Set up token to return user
|
||||||
|
$token->method('getUser')->willReturn($user);
|
||||||
|
|
||||||
|
// Mock the return of an AccompanyingCourseDocument by the repository
|
||||||
|
$this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
|
||||||
|
|
||||||
|
// Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
|
||||||
|
$this->security->method('isGranted')->willReturn($isGrantedForEntity);
|
||||||
|
|
||||||
|
// Mock case where user is blocked or not by workflow
|
||||||
|
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsOnAttribute(): void
|
||||||
|
{
|
||||||
|
list($user, $token, $subject, $entity) = $this->setupMockObjects();
|
||||||
|
|
||||||
|
// Setup mocks for voteOnAttribute method
|
||||||
|
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
|
||||||
|
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||||
|
|
||||||
|
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
|
||||||
|
{
|
||||||
|
list($user, $token, $subject, $entity) = $this->setupMockObjects();
|
||||||
|
|
||||||
|
// Setup mocks for voteOnAttribute method
|
||||||
|
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
|
||||||
|
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||||
|
|
||||||
|
// The voteOnAttribute method should return True when workflow is allowed
|
||||||
|
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVoteOnAttributeNotAllowed(): void
|
||||||
|
{
|
||||||
|
list($user, $token, $subject, $entity) = $this->setupMockObjects();
|
||||||
|
|
||||||
|
// Setup mocks for voteOnAttribute method where isGranted() returns false
|
||||||
|
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
|
||||||
|
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||||
|
|
||||||
|
// The voteOnAttribute method should return True when workflow is allowed
|
||||||
|
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
|
||||||
|
{
|
||||||
|
list($user, $token, $subject, $entity) = $this->setupMockObjects();
|
||||||
|
|
||||||
|
// Setup mocks for voteOnAttribute method
|
||||||
|
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
|
||||||
|
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||||
|
|
||||||
|
// Test voteOnAttribute method
|
||||||
|
$attribute = StoredObjectRoleEnum::SEE;
|
||||||
|
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
||||||
|
|
||||||
|
// Assert that access is denied when workflow is not allowed
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
}
|
@ -14,11 +14,13 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -27,97 +29,93 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
|||||||
*/
|
*/
|
||||||
class StoredObjectVoterTest extends TestCase
|
class StoredObjectVoterTest extends TestCase
|
||||||
{
|
{
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider provideDataVote
|
* @dataProvider provideDataVote
|
||||||
*/
|
*/
|
||||||
public function testVote(TokenInterface $token, ?object $subject, string $attribute, mixed $expected): void
|
public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void
|
||||||
{
|
{
|
||||||
$voter = new StoredObjectVoter();
|
$storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition);
|
||||||
|
$token = new UsernamePasswordToken(new User(), 'chill_main', ['ROLE_USER']);
|
||||||
|
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
$security->expects($fallbackSecurityExpected ? $this->atLeastOnce() : $this->never())
|
||||||
|
->method('isGranted')
|
||||||
|
->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
|
||||||
|
->willReturn($securityIsGrantedResult);
|
||||||
|
|
||||||
|
$voter = new StoredObjectVoter($security, $storedObjectVoters);
|
||||||
|
|
||||||
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
|
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideDataVote(): iterable
|
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
|
||||||
|
{
|
||||||
|
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
|
||||||
|
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
|
||||||
|
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
|
||||||
|
->willReturn($supports);
|
||||||
|
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
|
||||||
|
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
|
||||||
|
->willReturn($voteOnAttribute);
|
||||||
|
|
||||||
|
return $storedObjectVoter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideDataVote(): iterable
|
||||||
{
|
{
|
||||||
yield [
|
yield [
|
||||||
$this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
|
// we try with something else than a SToredObject, the voter should abstain
|
||||||
|
[[false, false, false]],
|
||||||
new \stdClass(),
|
new \stdClass(),
|
||||||
'SOMETHING',
|
'SOMETHING',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
VoterInterface::ACCESS_ABSTAIN,
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
// we try with an unsupported attribute, the voter must abstain
|
||||||
$so,
|
[[false, false, false]],
|
||||||
|
new StoredObject(),
|
||||||
'SOMETHING',
|
'SOMETHING',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
VoterInterface::ACCESS_ABSTAIN,
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
// happy scenario: there is a role voter
|
||||||
$so,
|
[[true, true, true]],
|
||||||
StoredObjectRoleEnum::SEE->value,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield [
|
|
||||||
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
|
||||||
$so,
|
|
||||||
StoredObjectRoleEnum::EDIT->value,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield [
|
|
||||||
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
|
||||||
$so,
|
|
||||||
StoredObjectRoleEnum::EDIT->value,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield [
|
|
||||||
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
|
||||||
$so,
|
|
||||||
StoredObjectRoleEnum::SEE->value,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield [
|
|
||||||
$this->buildToken(null, null),
|
|
||||||
new StoredObject(),
|
new StoredObject(),
|
||||||
StoredObjectRoleEnum::SEE->value,
|
StoredObjectRoleEnum::SEE->value,
|
||||||
VoterInterface::ACCESS_DENIED,
|
false,
|
||||||
|
false,
|
||||||
|
VoterInterface::ACCESS_GRANTED,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
$this->buildToken(null, null),
|
// there is a role voter, but not allowed to see the stored object
|
||||||
|
[[true, true, false]],
|
||||||
new StoredObject(),
|
new StoredObject(),
|
||||||
StoredObjectRoleEnum::SEE->value,
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
VoterInterface::ACCESS_DENIED,
|
VoterInterface::ACCESS_DENIED,
|
||||||
];
|
];
|
||||||
}
|
yield [
|
||||||
|
// there is no role voter, fallback to security, which does not grant access
|
||||||
private function buildToken(?StoredObjectRoleEnum $storedObjectRoleEnum = null, ?StoredObject $storedObject = null): TokenInterface
|
[[true, false, false]],
|
||||||
{
|
new StoredObject(),
|
||||||
$token = $this->prophesize(TokenInterface::class);
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
true,
|
||||||
if (null !== $storedObjectRoleEnum) {
|
false,
|
||||||
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
|
VoterInterface::ACCESS_DENIED,
|
||||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
|
];
|
||||||
} else {
|
yield [
|
||||||
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
|
// there is no role voter, fallback to security, which does grant access
|
||||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
|
[[true, false, false]],
|
||||||
}
|
new StoredObject(),
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
if (null !== $storedObject) {
|
true,
|
||||||
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
|
true,
|
||||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
|
VoterInterface::ACCESS_GRANTED,
|
||||||
} else {
|
];
|
||||||
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
|
|
||||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $token->reveal();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,8 @@ class SignedUrlNormalizerTest extends KernelTestCase
|
|||||||
$signedUrl = new SignedUrl(
|
$signedUrl = new SignedUrl(
|
||||||
'GET',
|
'GET',
|
||||||
'https://object.store.example/container/object',
|
'https://object.store.example/container/object',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1700000')
|
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||||
|
'object'
|
||||||
);
|
);
|
||||||
|
|
||||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||||
@ -48,6 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase
|
|||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'expires' => 1_700_000,
|
'expires' => 1_700_000,
|
||||||
'url' => 'https://object.store.example/container/object',
|
'url' => 'https://object.store.example/container/object',
|
||||||
|
'object_name' => 'object',
|
||||||
],
|
],
|
||||||
$actual
|
$actual
|
||||||
);
|
);
|
||||||
|
@ -38,6 +38,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
|
|||||||
$signedUrl = new SignedUrlPost(
|
$signedUrl = new SignedUrlPost(
|
||||||
'https://object.store.example/container/object',
|
'https://object.store.example/container/object',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||||
|
'abc',
|
||||||
15000,
|
15000,
|
||||||
1,
|
1,
|
||||||
180,
|
180,
|
||||||
@ -59,6 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
|
|||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'expires' => 1_700_000,
|
'expires' => 1_700_000,
|
||||||
'url' => 'https://object.store.example/container/object',
|
'url' => 'https://object.store.example/container/object',
|
||||||
|
'object_name' => 'abc',
|
||||||
],
|
],
|
||||||
$actual
|
$actual
|
||||||
);
|
);
|
||||||
|
@ -202,7 +202,8 @@ final class StoredObjectManagerTest extends TestCase
|
|||||||
$response = new SignedUrl(
|
$response = new SignedUrl(
|
||||||
'PUT',
|
'PUT',
|
||||||
'https://example.com/'.$storedObject->getFilename(),
|
'https://example.com/'.$storedObject->getFilename(),
|
||||||
new \DateTimeImmutable('1 hours')
|
new \DateTimeImmutable('1 hours'),
|
||||||
|
$storedObject->getFilename()
|
||||||
);
|
);
|
||||||
|
|
||||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||||
|
@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
|
|||||||
|
|
||||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||||
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
||||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
|
||||||
|
|
||||||
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
||||||
}
|
}
|
||||||
|
@ -12,31 +12,25 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Workflow;
|
namespace Chill\DocStoreBundle\Workflow;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
|
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
||||||
*/
|
*/
|
||||||
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: injecter le repository directement.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityManagerInterface $em,
|
private TranslatorInterface $translator,
|
||||||
private readonly TranslatorInterface $translator
|
private EntityWorkflowRepository $workflowRepository,
|
||||||
) {
|
private AccompanyingCourseDocumentRepository $repository
|
||||||
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
public function getDeletionRoles(): array
|
public function getDeletionRoles(): array
|
||||||
{
|
{
|
||||||
@ -128,4 +122,18 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithSto
|
|||||||
{
|
{
|
||||||
return $this->getRelatedEntity($entityWorkflow)?->getObject();
|
return $this->getRelatedEntity($entityWorkflow)?->getObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof AccompanyingCourseDocument) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
src/Bundle/ChillDocStoreBundle/config/services/security.yaml
Normal file
13
src/Bundle/ChillDocStoreBundle/config/services/security.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter:
|
||||||
|
arguments:
|
||||||
|
$storedObjectVoters: !tagged_iterator stored_object_voter
|
||||||
|
tags:
|
||||||
|
- { name: security.voter }
|
||||||
|
|
||||||
|
Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter:
|
||||||
|
tags:
|
||||||
|
- { name: security.voter }
|
@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
/**
|
/**
|
||||||
* Class Event.
|
* Class Event.
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
|
#[ORM\Entity]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ORM\Table(name: 'chill_event_event')]
|
#[ORM\Table(name: 'chill_event_event')]
|
||||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
||||||
|
@ -12,7 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\EventBundle\Form\ChoiceLoader;
|
namespace Chill\EventBundle\Form\ChoiceLoader;
|
||||||
|
|
||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Chill\EventBundle\Repository\EventRepository;
|
||||||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
||||||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
||||||
|
|
||||||
@ -26,9 +26,6 @@ class EventChoiceLoader implements ChoiceLoaderInterface
|
|||||||
*/
|
*/
|
||||||
protected $centers = [];
|
protected $centers = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var EntityRepository
|
|
||||||
*/
|
|
||||||
protected $eventRepository;
|
protected $eventRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,7 +37,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface
|
|||||||
* EventChoiceLoader constructor.
|
* EventChoiceLoader constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityRepository $eventRepository,
|
EventRepository $eventRepository,
|
||||||
?array $centers = null
|
?array $centers = null
|
||||||
) {
|
) {
|
||||||
$this->eventRepository = $eventRepository;
|
$this->eventRepository = $eventRepository;
|
||||||
|
@ -11,17 +11,65 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Repository;
|
namespace Chill\EventBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EventRepository.
|
* Class EventRepository.
|
||||||
*/
|
*/
|
||||||
class EventRepository extends ServiceEntityRepository
|
class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, Event::class);
|
$this->repository = $entityManager->getRepository(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
|
||||||
|
{
|
||||||
|
return $this->repository->createQueryBuilder($alias, $indexBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('e');
|
||||||
|
$query = $qb
|
||||||
|
->join('e.documents', 'ed')
|
||||||
|
->where('ed.id = :storedObjectId')
|
||||||
|
->setParameter('storedObjectId', $storedObject->getId())
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($id)
|
||||||
|
{
|
||||||
|
return $this->repository->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
return $this->repository->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||||
|
{
|
||||||
|
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findOneBy(array $criteria)
|
||||||
|
{
|
||||||
|
return $this->repository->findOneBy($criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassName(): string
|
||||||
|
{
|
||||||
|
return Event::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<?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 Chill\EventBundle\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Chill\EventBundle\Entity\Event;
|
||||||
|
use Chill\EventBundle\Repository\EventRepository;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EventRepository $repository,
|
||||||
|
Security $security,
|
||||||
|
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return Event::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||||
|
{
|
||||||
|
return match ($attribute) {
|
||||||
|
StoredObjectRoleEnum::EDIT => EventVoter::UPDATE,
|
||||||
|
StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -96,7 +96,7 @@ class SearchController extends AbstractController
|
|||||||
return $this->render('@ChillMain/Search/choose_list.html.twig');
|
return $this->render('@ChillMain/Search/choose_list.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
|
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
|
||||||
public function searchAction(Request $request, mixed $_format)
|
public function searchAction(Request $request, mixed $_format)
|
||||||
{
|
{
|
||||||
$pattern = trim((string) $request->query->get('q', ''));
|
$pattern = trim((string) $request->query->get('q', ''));
|
||||||
|
@ -99,6 +99,24 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<EntityWorkflow>
|
||||||
|
*/
|
||||||
|
public function findByRelatedEntity($entityClass, $relatedEntityId): array
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('w');
|
||||||
|
|
||||||
|
$query = $qb->where(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('w.relatedEntityClass', ':entity_class'),
|
||||||
|
$qb->expr()->eq('w.relatedEntityId', ':entity_id'),
|
||||||
|
)
|
||||||
|
)->setParameter('entity_class', $entityClass)
|
||||||
|
->setParameter('entity_id', $relatedEntityId);
|
||||||
|
|
||||||
|
return $query->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed|null $limit
|
* @param mixed|null $limit
|
||||||
* @param mixed|null $offset
|
* @param mixed|null $offset
|
||||||
|
@ -55,4 +55,11 @@ interface EntityWorkflowHandlerInterface
|
|||||||
public function isObjectSupported(object $object): bool;
|
public function isObjectSupported(object $object): bool;
|
||||||
|
|
||||||
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<EntityWorkflow>
|
||||||
|
*/
|
||||||
|
public function findByRelatedEntity(object $object): array;
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,18 @@ class EntityWorkflowManager
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<EntityWorkflow>
|
||||||
|
*/
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
foreach ($this->handlers as $handler) {
|
||||||
|
if ([] !== $workflows = $handler->findByRelatedEntity($object)) {
|
||||||
|
return $workflows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
|
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository
|
readonly class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
public function __construct(EntityManagerInterface $em)
|
||||||
{
|
{
|
||||||
@ -58,4 +61,18 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi
|
|||||||
{
|
{
|
||||||
return AccompanyingPeriodWorkEvaluationDocument::class;
|
return AccompanyingPeriodWorkEvaluationDocument::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
*/
|
||||||
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWorkEvaluationDocument
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('acpwed');
|
||||||
|
$query = $qb
|
||||||
|
->where('acpwed.storedObject = :storedObject')
|
||||||
|
->setParameter('storedObject', $storedObject)
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||||||
class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
|
class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
|
||||||
{
|
{
|
||||||
final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW';
|
final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW';
|
||||||
|
final public const SEE_AND_EDIT = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_EDIT';
|
||||||
|
|
||||||
public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager) {}
|
public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager) {}
|
||||||
|
|
||||||
protected function supports($attribute, $subject)
|
public function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
return $subject instanceof AccompanyingPeriodWorkEvaluationDocument
|
return $subject instanceof AccompanyingPeriodWorkEvaluationDocument
|
||||||
&& self::SEE === $attribute;
|
&& (self::SEE === $attribute || self::SEE_AND_EDIT === $attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +40,7 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
|
|||||||
*
|
*
|
||||||
* @return bool|void
|
* @return bool|void
|
||||||
*/
|
*/
|
||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
return match ($attribute) {
|
return match ($attribute) {
|
||||||
self::SEE => $this->accessDecisionManager->decide(
|
self::SEE => $this->accessDecisionManager->decide(
|
||||||
@ -47,6 +48,11 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
|
|||||||
[AccompanyingPeriodWorkEvaluationVoter::SEE],
|
[AccompanyingPeriodWorkEvaluationVoter::SEE],
|
||||||
$subject->getAccompanyingPeriodWorkEvaluation()
|
$subject->getAccompanyingPeriodWorkEvaluation()
|
||||||
),
|
),
|
||||||
|
self::SEE_AND_EDIT => $this->accessDecisionManager->decide(
|
||||||
|
$token,
|
||||||
|
[AccompanyingPeriodWorkEvaluationVoter::SEE_AND_EDIT],
|
||||||
|
$subject->getAccompanyingPeriodWorkEvaluation()
|
||||||
|
),
|
||||||
default => throw new \UnexpectedValueException("The attribute {$attribute} is not supported"),
|
default => throw new \UnexpectedValueException("The attribute {$attribute} is not supported"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,14 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI
|
|||||||
{
|
{
|
||||||
final public const ALL = [
|
final public const ALL = [
|
||||||
self::SEE,
|
self::SEE,
|
||||||
|
self::SEE_AND_EDIT,
|
||||||
self::STATS,
|
self::STATS,
|
||||||
];
|
];
|
||||||
|
|
||||||
final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_SHOW';
|
final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_SHOW';
|
||||||
|
|
||||||
|
final public const SEE_AND_EDIT = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_EDIT';
|
||||||
|
|
||||||
final public const STATS = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_STATS';
|
final public const STATS = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_STATS';
|
||||||
|
|
||||||
public function __construct(private readonly Security $security) {}
|
public function __construct(private readonly Security $security) {}
|
||||||
@ -45,6 +48,7 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI
|
|||||||
return match ($attribute) {
|
return match ($attribute) {
|
||||||
self::STATS => $this->security->isGranted(AccompanyingPeriodVoter::STATS, $subject),
|
self::STATS => $this->security->isGranted(AccompanyingPeriodVoter::STATS, $subject),
|
||||||
self::SEE => $this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $subject->getAccompanyingPeriodWork()),
|
self::SEE => $this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $subject->getAccompanyingPeriodWork()),
|
||||||
|
self::SEE_AND_EDIT => $this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $subject->getAccompanyingPeriodWork()),
|
||||||
default => throw new \UnexpectedValueException("attribute {$attribute} is not supported"),
|
default => throw new \UnexpectedValueException("attribute {$attribute} is not supported"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
<?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 Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||||
|
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
|
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||||
|
Security $security,
|
||||||
|
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||||
|
) {
|
||||||
|
parent::__construct($security, $workflowDocumentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClass(): string
|
||||||
|
{
|
||||||
|
return AccompanyingPeriodWorkEvaluationDocument::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||||
|
{
|
||||||
|
return match ($attribute) {
|
||||||
|
StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE,
|
||||||
|
StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE_AND_EDIT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canBeAssociatedWithWorkflow(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Workflow;
|
|||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
@ -25,7 +26,12 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
*/
|
*/
|
||||||
class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {}
|
public function __construct(
|
||||||
|
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||||
|
private readonly EntityWorkflowRepository $workflowRepository,
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private readonly TranslatorInterface $translator
|
||||||
|
) {}
|
||||||
|
|
||||||
public function getDeletionRoles(): array
|
public function getDeletionRoles(): array
|
||||||
{
|
{
|
||||||
@ -130,4 +136,18 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
|
|||||||
{
|
{
|
||||||
return $this->getRelatedEntity($entityWorkflow)?->getStoredObject();
|
return $this->getRelatedEntity($entityWorkflow)?->getStoredObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Workflow;
|
namespace Chill\PersonBundle\Workflow;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||||
@ -27,6 +28,7 @@ readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements Entity
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AccompanyingPeriodWorkEvaluationRepository $repository,
|
private AccompanyingPeriodWorkEvaluationRepository $repository,
|
||||||
|
private EntityWorkflowRepository $workflowRepository,
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
private TranslatorInterface $translator
|
private TranslatorInterface $translator
|
||||||
) {}
|
) {}
|
||||||
@ -113,4 +115,18 @@ readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements Entity
|
|||||||
{
|
{
|
||||||
return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass();
|
return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof AccompanyingPeriodWorkEvaluation) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluation::class, $object->getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Workflow;
|
namespace Chill\PersonBundle\Workflow;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
||||||
@ -28,6 +29,7 @@ readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHa
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AccompanyingPeriodWorkRepository $repository,
|
private AccompanyingPeriodWorkRepository $repository,
|
||||||
|
private EntityWorkflowRepository $workflowRepository,
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
private TranslatorInterface $translator
|
private TranslatorInterface $translator
|
||||||
) {}
|
) {}
|
||||||
@ -120,4 +122,18 @@ readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHa
|
|||||||
{
|
{
|
||||||
return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass();
|
return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof AccompanyingPeriodWork) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
|||||||
namespace Chill\WopiBundle\Service\Wopi;
|
namespace Chill\WopiBundle\Service\Wopi;
|
||||||
|
|
||||||
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
@ -19,13 +21,18 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
|
|
||||||
class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface
|
class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly JWTTokenManagerInterface $tokenManager, private readonly Security $security) {}
|
public function __construct(private readonly JWTTokenManagerInterface $tokenManager, private readonly Security $security, private readonly StoredObjectRepository $storedObjectRepository) {}
|
||||||
|
|
||||||
public function isRestrictedWebViewOnly(string $accessToken, Document $document, RequestInterface $request): bool
|
public function isRestrictedWebViewOnly(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRelatedStoredObject(Document $document)
|
||||||
|
{
|
||||||
|
return $this->storedObjectRepository->findOneBy(['uuid' => $document->getWopiDocId()]);
|
||||||
|
}
|
||||||
|
|
||||||
public function isTokenValid(string $accessToken, Document $document, RequestInterface $request): bool
|
public function isTokenValid(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
$metadata = $this->tokenManager->parse($accessToken);
|
$metadata = $this->tokenManager->parse($accessToken);
|
||||||
@ -60,14 +67,25 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori
|
|||||||
|
|
||||||
public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool
|
public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
|
$storedObject = $this->getRelatedStoredObject($document);
|
||||||
|
if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
return $this->isTokenValid($accessToken, $document, $request);
|
return $this->isTokenValid($accessToken, $document, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool
|
public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
|
$storedObject = $this->getRelatedStoredObject($document);
|
||||||
|
|
||||||
|
if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
return $this->isTokenValid($accessToken, $document, $request);
|
return $this->isTokenValid($accessToken, $document, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool
|
public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@ -75,6 +93,12 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori
|
|||||||
|
|
||||||
public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool
|
public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool
|
||||||
{
|
{
|
||||||
|
$storedObject = $this->getRelatedStoredObject($document);
|
||||||
|
|
||||||
|
if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
||||||
return $this->isTokenValid($accessToken, $document, $request);
|
return $this->isTokenValid($accessToken, $document, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user