diff --git a/.changes/unreleased/Feature-20240614-153537.yaml b/.changes/unreleased/Feature-20240614-153537.yaml
new file mode 100644
index 000000000..c16d49b2d
--- /dev/null
+++ b/.changes/unreleased/Feature-20240614-153537.yaml
@@ -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"
diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php
index a7025d4a9..ff4904f9e 100644
--- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php
+++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
+use Chill\DocStoreBundle\Entity\StoredObject;
+use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
* @method Activity[] findAll()
* @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)
{
@@ -97,4 +99,16 @@ class ActivityRepository extends ServiceEntityRepository
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();
+ }
}
diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php
new file mode 100644
index 000000000..4be101c45
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php
@@ -0,0 +1,54 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php
index e410529b4..ae74f9d0e 100644
--- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php
+++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php
@@ -89,6 +89,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
$g = new SignedUrlPost(
$url = $this->generateUrl($object_name),
$expires,
+ $object_name,
$this->max_post_file_size,
$max_file_count,
$submit_delay,
@@ -127,7 +128,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
];
$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(
new TempUrlGenerateEvent($signature)
diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
index aba289652..ec0debca9 100644
--- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
+++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
@@ -21,6 +21,8 @@ readonly class SignedUrl
#[Serializer\Groups(['read'])]
public string $url,
public \DateTimeImmutable $expires,
+ #[Serializer\Groups(['read'])]
+ public string $object_name,
) {}
#[Serializer\Groups(['read'])]
diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
index 9d37771d1..e9aecf6b2 100644
--- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
+++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
@@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl
public function __construct(
string $url,
\DateTimeImmutable $expires,
+ string $object_name,
#[Serializer\Groups(['read'])]
public int $max_file_size,
#[Serializer\Groups(['read'])]
@@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl
#[Serializer\Groups(['read'])]
public string $signature,
) {
- parent::__construct('POST', $url, $expires);
+ parent::__construct('POST', $url, $expires, $object_name);
}
}
diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php
index fe9aeecfa..b156af1ea 100644
--- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php
+++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection;
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
+use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$container->setParameter('chill_doc_store', $config);
+ $container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
+
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/controller.yaml');
@@ -42,6 +45,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$loader->load('services/fixtures.yaml');
$loader->load('services/form.yaml');
$loader->load('services/templating.yaml');
+ $loader->load('services/security.yaml');
}
public function prepend(ContainerBuilder $container)
diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php
index 2679993c4..7033a53c9 100644
--- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php
+++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php
@@ -12,13 +12,14 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
+use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
-class AccompanyingCourseDocumentRepository implements ObjectRepository
+class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
private readonly EntityRepository $repository;
@@ -45,6 +46,16 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
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
{
return $this->repository->find($id);
@@ -55,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
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);
}
@@ -65,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
return $this->findOneBy($criteria);
}
- public function getClassName()
+ public function getClassName(): string
{
return AccompanyingCourseDocument::class;
}
diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php
new file mode 100644
index 000000000..81d230e67
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php
@@ -0,0 +1,19 @@
+
*/
-readonly class PersonDocumentRepository implements ObjectRepository
+readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
{
private EntityRepository $repository;
@@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
{
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();
+ }
}
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig
index 58504b095..2a3592d73 100644
--- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig
+++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig
@@ -71,7 +71,7 @@
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
- {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
+ {{ document.object|chill_document_button_group(document.title) }}
@@ -90,7 +90,7 @@
{% else %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
- {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
+ {{ document.object|chill_document_button_group(document.title) }}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php
index 5febc7e42..7a46184cb 100644
--- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php
@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
return [];
}
- protected function supports($attribute, $subject): bool
+ public function supports($attribute, $subject): bool
{
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) {
return false;
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php
index 1d7ad759a..708df0467 100644
--- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
+use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
@@ -22,6 +23,7 @@ final class AsyncUploadVoter extends Voter
public function __construct(
private readonly Security $security,
+ private readonly StoredObjectRepository $storedObjectRepository
) {}
protected function supports($attribute, $subject): bool
@@ -32,10 +34,16 @@ final class AsyncUploadVoter extends Voter
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** @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 $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')
+ };
}
}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php
index 2e253cf3c..91e767af2 100644
--- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php
@@ -12,9 +12,9 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
-use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Security;
/**
* Voter for the content of a stored object.
@@ -23,6 +23,8 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
*/
class StoredObjectVoter extends Voter
{
+ public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) {}
+
protected function supports($attribute, $subject): bool
{
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
@@ -32,24 +34,22 @@ class StoredObjectVoter extends Voter
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** @var StoredObject $subject */
- if (
- !$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
- || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
- ) {
- return false;
+ $attributeAsEnum = StoredObjectRoleEnum::from($attribute);
+
+ // Loop through context-specific voters
+ foreach ($this->storedObjectVoters as $storedObjectVoter) {
+ if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
+ return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
+ }
}
- if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
- return false;
+ // 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;
}
- $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
- };
+ return false;
}
}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php
new file mode 100644
index 000000000..2e76da318
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php
@@ -0,0 +1,69 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php
new file mode 100644
index 000000000..0f7015b10
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php
@@ -0,0 +1,54 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php
new file mode 100644
index 000000000..577f362ef
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php
@@ -0,0 +1,54 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php
new file mode 100644
index 000000000..97d45eec3
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php
@@ -0,0 +1,22 @@
+security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
+ $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
- if ($canDavSee || $canDavEdit) {
+ if ($canSee || $canEdit) {
$accessToken = $this->JWTDavTokenProvider->createToken(
$object,
- $canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
+ $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
);
$datas['_links'] = [
diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php
new file mode 100644
index 000000000..b27b6d96a
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php
@@ -0,0 +1,38 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php
index 716cb422e..b6290049c 100644
--- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php
+++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php
@@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment;
@@ -128,6 +129,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
private NormalizerInterface $normalizer,
private JWTDavTokenProviderInterface $davTokenProvider,
private UrlGeneratorInterface $urlGenerator,
+ private Security $security,
) {}
/**
@@ -148,8 +150,10 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
* @throws \Twig\Error\RuntimeError
* @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(
$document,
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php
index e6250202f..bb83c698a 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php
@@ -122,7 +122,8 @@ class TempUrlOpenstackGeneratorTest extends TestCase
$signedUrl = new SignedUrl(
'GET',
'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) {
@@ -153,6 +154,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase
$signedUrl = new SignedUrlPost(
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'),
+ $objectName,
150,
1,
1800,
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php
index e309c02ec..d39809a12 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php
@@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase
{
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
$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->generate('async_upload.generate_url', Argument::type('array'))
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php
index c378ba989..6b49b2919 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php
@@ -73,6 +73,7 @@ class AsyncUploadControllerTest extends TestCase
return new SignedUrlPost(
'https://object.store.example',
new \DateTimeImmutable('1 hour'),
+ 'abc',
150,
1,
1800,
@@ -87,7 +88,8 @@ class AsyncUploadControllerTest extends TestCase
return new SignedUrl(
$method,
'https://object.store.example',
- new \DateTimeImmutable('1 hour')
+ new \DateTimeImmutable('1 hour'),
+ $object_name
);
}
};
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php
index 2fc17787a..9d486ac93 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php
@@ -23,6 +23,7 @@ use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
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)
->willReturn('http://url/fake');
+ $security = $this->prophesize(Security::class);
+ $security->isGranted(Argument::cetera())->willReturn(true);
+
$serializer = new Serializer(
[
new StoredObjectNormalizer(
$jwtTokenProvider->reveal(),
$urlGenerator->reveal(),
+ $security->reveal()
),
],
[
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php
new file mode 100644
index 000000000..4f420223a
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php
@@ -0,0 +1,152 @@
+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);
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php
index 92c928681..a113cfb7b 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php
@@ -14,11 +14,13 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
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 Prophecy\PhpUnit\ProphecyTrait;
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\Security;
/**
* @internal
@@ -27,97 +29,93 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
*/
class StoredObjectVoterTest extends TestCase
{
- use ProphecyTrait;
-
/**
* @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]));
}
- 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 [
- $this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
+ // we try with something else than a SToredObject, the voter should abstain
+ [[false, false, false]],
new \stdClass(),
'SOMETHING',
+ false,
+ false,
VoterInterface::ACCESS_ABSTAIN,
];
-
yield [
- $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
- $so,
+ // we try with an unsupported attribute, the voter must abstain
+ [[false, false, false]],
+ new StoredObject(),
'SOMETHING',
+ false,
+ false,
VoterInterface::ACCESS_ABSTAIN,
];
-
yield [
- $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
- $so,
- 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),
+ // happy scenario: there is a role voter
+ [[true, true, true]],
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
- VoterInterface::ACCESS_DENIED,
+ false,
+ false,
+ VoterInterface::ACCESS_GRANTED,
];
-
yield [
- $this->buildToken(null, null),
+ // there is a role voter, but not allowed to see the stored object
+ [[true, true, false]],
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
+ false,
+ false,
VoterInterface::ACCESS_DENIED,
];
- }
-
- private function buildToken(?StoredObjectRoleEnum $storedObjectRoleEnum = null, ?StoredObject $storedObject = null): TokenInterface
- {
- $token = $this->prophesize(TokenInterface::class);
-
- if (null !== $storedObjectRoleEnum) {
- $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
- $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
- } else {
- $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
- $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
- }
-
- if (null !== $storedObject) {
- $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
- $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
- } else {
- $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
- $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
- }
-
- return $token->reveal();
+ yield [
+ // there is no role voter, fallback to security, which does not grant access
+ [[true, false, false]],
+ new StoredObject(),
+ StoredObjectRoleEnum::SEE->value,
+ true,
+ false,
+ VoterInterface::ACCESS_DENIED,
+ ];
+ yield [
+ // there is no role voter, fallback to security, which does grant access
+ [[true, false, false]],
+ new StoredObject(),
+ StoredObjectRoleEnum::SEE->value,
+ true,
+ true,
+ VoterInterface::ACCESS_GRANTED,
+ ];
}
}
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php
index cb5294fff..7f0c342bb 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php
@@ -38,7 +38,8 @@ class SignedUrlNormalizerTest extends KernelTestCase
$signedUrl = new SignedUrl(
'GET',
'https://object.store.example/container/object',
- \DateTimeImmutable::createFromFormat('U', '1700000')
+ \DateTimeImmutable::createFromFormat('U', '1700000'),
+ 'object'
);
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
@@ -48,6 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase
'method' => 'GET',
'expires' => 1_700_000,
'url' => 'https://object.store.example/container/object',
+ 'object_name' => 'object',
],
$actual
);
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php
index ceb777f23..69d7958b6 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php
@@ -38,6 +38,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
$signedUrl = new SignedUrlPost(
'https://object.store.example/container/object',
\DateTimeImmutable::createFromFormat('U', '1700000'),
+ 'abc',
15000,
1,
180,
@@ -59,6 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
'method' => 'POST',
'expires' => 1_700_000,
'url' => 'https://object.store.example/container/object',
+ 'object_name' => 'abc',
],
$actual
);
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php
index 531d19592..c5cc8185e 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php
@@ -202,7 +202,8 @@ final class StoredObjectManagerTest extends TestCase
$response = new SignedUrl(
'PUT',
'https://example.com/'.$storedObject->getFilename(),
- new \DateTimeImmutable('1 hours')
+ new \DateTimeImmutable('1 hours'),
+ $storedObject->getFilename()
);
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php
index 6e46ee370..ac0f4a6e7 100644
--- a/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php
+++ b/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php
@@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
$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);
}
diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
index ad16ae722..dc5394c9c 100644
--- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
+++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
@@ -12,31 +12,25 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Workflow;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
+use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
+use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\EntityRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @implements EntityWorkflowWithStoredObjectHandlerInterface
*/
-class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
+readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
{
- private readonly EntityRepository $repository;
-
- /**
- * TODO: injecter le repository directement.
- */
public function __construct(
- EntityManagerInterface $em,
- private readonly TranslatorInterface $translator
- ) {
- $this->repository = $em->getRepository(AccompanyingCourseDocument::class);
- }
+ private TranslatorInterface $translator,
+ private EntityWorkflowRepository $workflowRepository,
+ private AccompanyingCourseDocumentRepository $repository
+ ) {}
public function getDeletionRoles(): array
{
@@ -128,4 +122,18 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithSto
{
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());
+ }
}
diff --git a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml
new file mode 100644
index 000000000..c57eb63c5
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml
@@ -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 }
diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php
index 6f7ee7ef0..be5969363 100644
--- a/src/Bundle/ChillEventBundle/Entity/Event.php
+++ b/src/Bundle/ChillEventBundle/Entity/Event.php
@@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* Class Event.
*/
-#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
+#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_event_event')]
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
diff --git a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
index 9aa001170..3fe305ff7 100644
--- a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
+++ b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Form\ChoiceLoader;
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\Loader\ChoiceLoaderInterface;
@@ -26,9 +26,6 @@ class EventChoiceLoader implements ChoiceLoaderInterface
*/
protected $centers = [];
- /**
- * @var EntityRepository
- */
protected $eventRepository;
/**
@@ -40,7 +37,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface
* EventChoiceLoader constructor.
*/
public function __construct(
- EntityRepository $eventRepository,
+ EventRepository $eventRepository,
?array $centers = null
) {
$this->eventRepository = $eventRepository;
diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php
index 7406748b4..89723b770 100644
--- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php
+++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php
@@ -11,17 +11,65 @@ declare(strict_types=1);
namespace Chill\EventBundle\Repository;
+use Chill\DocStoreBundle\Entity\StoredObject;
+use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\EventBundle\Entity\Event;
-use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
-use Doctrine\Persistence\ManagerRegistry;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\QueryBuilder;
+use Doctrine\Persistence\ObjectRepository;
/**
* 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;
}
}
diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php
new file mode 100644
index 000000000..66f54bc6d
--- /dev/null
+++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php
@@ -0,0 +1,54 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php
index 4c4b6d22c..09e558b0a 100644
--- a/src/Bundle/ChillMainBundle/Controller/SearchController.php
+++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php
@@ -96,7 +96,7 @@ class SearchController extends AbstractController
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)
{
$pattern = trim((string) $request->query->get('q', ''));
diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php
index 66b3ab379..81cdcd551 100644
--- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php
@@ -99,6 +99,24 @@ class EntityWorkflowRepository implements ObjectRepository
return $this->repository->findAll();
}
+ /**
+ * @return list
+ */
+ 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 $offset
diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php
index edd1120a7..c0d6469f6 100644
--- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php
+++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php
@@ -55,4 +55,11 @@ interface EntityWorkflowHandlerInterface
public function isObjectSupported(object $object): bool;
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
+
+ public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
+
+ /**
+ * @return list
+ */
+ public function findByRelatedEntity(object $object): array;
}
diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php
index a45abe081..9f7b54ccd 100644
--- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php
+++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php
@@ -49,4 +49,18 @@ class EntityWorkflowManager
return null;
}
+
+ /**
+ * @return list
+ */
+ public function findByRelatedEntity(object $object): array
+ {
+ foreach ($this->handlers as $handler) {
+ if ([] !== $workflows = $handler->findByRelatedEntity($object)) {
+ return $workflows;
+ }
+ }
+
+ return [];
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php
index 59bb3f915..e3a484f0b 100644
--- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php
+++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php
@@ -11,14 +11,17 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
+use Chill\DocStoreBundle\Entity\StoredObject;
+use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\NonUniqueResultException;
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)
{
@@ -58,4 +61,18 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi
{
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();
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php
index 77c131cd9..0e9f4201c 100644
--- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php
+++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php
@@ -24,13 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
{
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) {}
- protected function supports($attribute, $subject)
+ public function supports($attribute, $subject): bool
{
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
*/
- protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
+ public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
return match ($attribute) {
self::SEE => $this->accessDecisionManager->decide(
@@ -47,6 +48,11 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
[AccompanyingPeriodWorkEvaluationVoter::SEE],
$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"),
};
}
diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php
index ce5faca8d..bea63018c 100644
--- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php
+++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php
@@ -21,11 +21,14 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI
{
final public const ALL = [
self::SEE,
+ self::SEE_AND_EDIT,
self::STATS,
];
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';
public function __construct(private readonly Security $security) {}
@@ -45,6 +48,7 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI
return match ($attribute) {
self::STATS => $this->security->isGranted(AccompanyingPeriodVoter::STATS, $subject),
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"),
};
}
diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php
new file mode 100644
index 000000000..4a81367d7
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php
@@ -0,0 +1,55 @@
+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;
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php
index 97d48e5b0..c987ea17e 100644
--- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php
+++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php
@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
+use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
@@ -25,7 +26,12 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
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
{
@@ -130,4 +136,18 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
{
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());
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php
index 113d5f5c8..11b47a684 100644
--- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php
+++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
+use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
@@ -27,6 +28,7 @@ readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements Entity
{
public function __construct(
private AccompanyingPeriodWorkEvaluationRepository $repository,
+ private EntityWorkflowRepository $workflowRepository,
private TranslatableStringHelperInterface $translatableStringHelper,
private TranslatorInterface $translator
) {}
@@ -113,4 +115,18 @@ readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements Entity
{
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());
+ }
}
diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php
index 837ee2aac..6bed52cfb 100644
--- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php
+++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
+use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@@ -28,6 +29,7 @@ readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHa
{
public function __construct(
private AccompanyingPeriodWorkRepository $repository,
+ private EntityWorkflowRepository $workflowRepository,
private TranslatableStringHelperInterface $translatableStringHelper,
private TranslatorInterface $translator
) {}
@@ -120,4 +122,18 @@ readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHa
{
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());
+ }
}
diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php
index c54f187ca..4c9cb4a2f 100644
--- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php
+++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php
@@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\WopiBundle\Service\Wopi;
use ChampsLibres\WopiLib\Contract\Entity\Document;
+use Chill\DocStoreBundle\Repository\StoredObjectRepository;
+use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Psr\Http\Message\RequestInterface;
@@ -19,13 +21,18 @@ use Symfony\Component\Security\Core\Security;
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
{
return false;
}
+ public function getRelatedStoredObject(Document $document)
+ {
+ return $this->storedObjectRepository->findOneBy(['uuid' => $document->getWopiDocId()]);
+ }
+
public function isTokenValid(string $accessToken, Document $document, RequestInterface $request): bool
{
$metadata = $this->tokenManager->parse($accessToken);
@@ -60,12 +67,23 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori
public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool
{
- return $this->isTokenValid($accessToken, $document, $request);
+ $storedObject = $this->getRelatedStoredObject($document);
+ if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ return $this->isTokenValid($accessToken, $document, $request);
+ }
+
+ return false;
}
public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool
{
- return $this->isTokenValid($accessToken, $document, $request);
+ $storedObject = $this->getRelatedStoredObject($document);
+
+ if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
+ return $this->isTokenValid($accessToken, $document, $request);
+ }
+
+ return false;
}
public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool
@@ -75,6 +93,12 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori
public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool
{
- return $this->isTokenValid($accessToken, $document, $request);
+ $storedObject = $this->getRelatedStoredObject($document);
+
+ if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
+ return $this->isTokenValid($accessToken, $document, $request);
+ }
+
+ return false;
}
}