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; } }