From 7923b5a1ef25f7209fedde12d955e4e61af2005f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:35:50 +0200 Subject: [PATCH 01/76] initial commit --- .changes/unreleased/Feature-20240614-153537.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/unreleased/Feature-20240614-153537.yaml 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" From 65c41e6fa957b9616d9e9be3f5feec9080f23d21 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 16:48:09 +0200 Subject: [PATCH 02/76] Add StoredObjectVoterInterface An interface is defined that can be implemented by each context-specific voter in the future. --- .../StoredObjectVoterInterface.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php new file mode 100644 index 000000000..47b93eabb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -0,0 +1,22 @@ + Date: Fri, 14 Jun 2024 17:22:27 +0200 Subject: [PATCH 03/76] Refactorize StoredObjectVoter.php The StoredObjectVoter.php has been refactorized to handle context-specific voters.\ This way we can check if the context-specific voter should handle the authorization or not.\ If not, there is a simple fallback to check on the USER_ROLE. --- .../Authorization/StoredObjectVoter.php | 28 ++++++++++++++----- .../config/services/voter.yaml | 14 ++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/config/services/voter.yaml diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 2e253cf3c..ecfc56615 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -15,6 +15,7 @@ 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 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; */ class StoredObjectVoter extends Voter { + private $security; + private $storedObjectVoters; + + public function __construct(Security $security, iterable $storedObjectVoters) { + $this->security = $security; + $this->storedObjectVoters = $storedObjectVoters; + } + protected function supports($attribute, $subject): bool { return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum @@ -43,13 +52,18 @@ class StoredObjectVoter extends Voter return false; } - $askedRole = StoredObjectRoleEnum::from($attribute); - $tokenRoleAuthorization = - $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS); + // Loop through context-specific voters + foreach ($this->storedObjectVoters as $storedObjectVoter) { + if ($storedObjectVoter->supports($attribute, $subject)) { + return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + } + } - return match ($askedRole) { - StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization, - StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization - }; + // User role-based fallback + if ($this->security->isGranted('ROLE_USER')) { + return true; + } + + return false; } } diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml new file mode 100644 index 000000000..922d29cba --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: + arguments: + $storedObjectVoters: + # context specific voters + - '@accompanying_course_document_voter' + tags: + - { name: security.voter } + + accompanying_course_document_voter: + class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter From e9a9262fae52274da1012d070182f4825175def9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 17:27:22 +0200 Subject: [PATCH 04/76] Add config voter.yaml The voter.yaml was not configured to be taken into account. Now added\ to ChillDocStoreExtension.php --- .../DependencyInjection/ChillDocStoreExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index fe9aeecfa..5f87199a3 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,6 +42,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/voter.yaml'); } public function prepend(ContainerBuilder $container) From 4b82e67952983c7b2942e693596b467a4e75833c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:51:21 +0200 Subject: [PATCH 05/76] Type-hint $subject in StoredObjectVoterInterface.php Since the subject passed to these voters should\ always be of the type StoredObject, type-hinting\ added. --- .../Security/Authorization/StoredObjectVoterInterface.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 47b93eabb..a3a76e79a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, $subject): bool; + public function supports(string $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool; + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; } From ad4fe8024092e7b365ee0f6aba7a5987ca964d18 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:52:59 +0200 Subject: [PATCH 06/76] Add fall-back right for ROLE_ADMIN Within the StoredObjectVoter.php also the admin\ user should be able to edit documents as a fall-back --- .../Security/Authorization/StoredObjectVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index ecfc56615..781c2c542 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -60,7 +60,7 @@ class StoredObjectVoter extends Voter } // User role-based fallback - if ($this->security->isGranted('ROLE_USER')) { + if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { return true; } From 04a48f22ad85abd8fdb13155c805ec709a7757c9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:00:10 +0200 Subject: [PATCH 07/76] Use constructor property promotion In accordance with php8.1 use property promotion\ within the constructor method. Less clutter. --- .../Security/Authorization/StoredObjectVoter.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 781c2c542..7e2becab4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,12 +24,8 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - private $security; - private $storedObjectVoters; - public function __construct(Security $security, iterable $storedObjectVoters) { - $this->security = $security; - $this->storedObjectVoters = $storedObjectVoters; + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { } protected function supports($attribute, $subject): bool From e015f71bb001ca00662e9f97a82cc8095efdb284 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:02:25 +0200 Subject: [PATCH 08/76] Rename voter.yaml file to security.yaml For consistency with other bundles voters are\ registered under the security.yaml file. --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- .../config/services/{voter.yaml => security.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Bundle/ChillDocStoreBundle/config/services/{voter.yaml => security.yaml} (100%) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 5f87199a3..a0fcc2d5d 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,7 +42,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/voter.yaml'); + $loader->load('services/security.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml similarity index 100% rename from src/Bundle/ChillDocStoreBundle/config/services/voter.yaml rename to src/Bundle/ChillDocStoreBundle/config/services/security.yaml From e0828b1f0fa9d9c255a6a1189492e33d7c2ca31d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:16:39 +0200 Subject: [PATCH 09/76] Use service tags to inject all voters into StoredObjectVoter.php Instead of manually injecting services into StoredObjectVoter\ config is added to automatically tag each service that implements\ StoredObjectVoterInterface.php --- .../DependencyInjection/ChillDocStoreExtension.php | 3 +++ .../ChillDocStoreBundle/config/services/security.yaml | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index a0fcc2d5d..9f277e716 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 ChillDocStoreBundle\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'); diff --git a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml index 922d29cba..c57eb63c5 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml @@ -4,11 +4,10 @@ services: autoconfigure: true Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: arguments: - $storedObjectVoters: - # context specific voters - - '@accompanying_course_document_voter' + $storedObjectVoters: !tagged_iterator stored_object_voter tags: - { name: security.voter } - accompanying_course_document_voter: - class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter + Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter: + tags: + - { name: security.voter } From 482f279dc5635a79f0ee14c21670e570c9a5ba07 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:21:24 +0200 Subject: [PATCH 10/76] Implement StoredObjectVoterInterface An interface was created to be implemented by Stored Doc voters\ these will automatically be tagged and injected into DocStoreVoter. --- .../Authorization/AccompanyingCourseDocumentVoter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 5febc7e42..93243b1f4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,11 +19,12 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; @@ -70,12 +71,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; From 427f232ab805125a010a9a4a78cad62e795a32be Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 10:53:33 +0200 Subject: [PATCH 11/76] Correct namespace and use statement for StoredObjectVoterInterface.php The namespace was formed wrong and needed adjustment --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 1 - .../Security/Authorization/StoredObjectVoterInterface.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 93243b1f4..dac08b05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,7 +19,6 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index a3a76e79a..c0fd7e09f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace ChillDocStoreBundle\Security\Authorization; +namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; From d26fa6bde6e8113cdb2e6d4aded57635a1d8254a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 15:17:56 +0200 Subject: [PATCH 12/76] Implement voting logic: separation of concerns A separate AccompanyingCourseDocumentStoredObjectVoter was\ created to handle the specific access to a Stored object\ related to an Accompanying Course Document. --- .../AccompanyingCourseDocumentRepository.php | 13 +++++ ...panyingCourseDocumentStoredObjectVoter.php | 50 +++++++++++++++++++ ...nyingPeriodWorkEvaluationDocumentVoter.php | 7 +-- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 2679993c4..239b88b90 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -12,6 +12,7 @@ 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; @@ -45,6 +46,17 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->leftJoin('d.storedObject', 'do') + ->where('do.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObjectId) + ->getQuery(); + + return $query->getResult(); + } + public function find($id): ?AccompanyingCourseDocument { return $this->repository->find($id); @@ -69,4 +81,5 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository { return AccompanyingCourseDocument::class; } + } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php new file mode 100644 index 000000000..db1499a99 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository->findLinkedCourseDocument($subject->getId())); + } + + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // Retrieve the related accompanying course document + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + + // Determine the attribute to pass to AccompanyingCourseDocumentVoter + $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + + // Check access using AccompanyingCourseDocumentVoter + if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { + // TODO implement logic to check for associated workflow + return true; + } else { + return false; + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 77c131cd9..8f4a1b995 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -21,13 +22,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; 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; @@ -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( From 760d65b9723d66b4bd3dd1aec55cfd784864a962 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:27:21 +0200 Subject: [PATCH 13/76] Remove implementation of StoredObjectVoterInterface in AccompanyingCourseDocumentVoter.php This implementation has been moved to the voter\ AccompanyingCourseDocumentStoredObjectVoter.php --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index dac08b05b..7a46184cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; From 3d40db749386c47e4e6b690b41a53113f18fce29 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:28:19 +0200 Subject: [PATCH 14/76] Refactor AccompanyingCourseDocumentRepository.php Build where clause using StoredObject directly instead\ of based on it's id. --- .../Repository/AccompanyingCourseDocumentRepository.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 239b88b90..93cf00ecb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -46,12 +46,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->leftJoin('d.storedObject', 'do') - ->where('do.id = :storedObjectId') - ->setParameter('storedObjectId', $storedObjectId) + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) ->getQuery(); return $query->getResult(); From 73797b98f670c20535b8f1ca03a5f7de5862d224 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:32:09 +0200 Subject: [PATCH 15/76] Add WorkflowDocumentService and use in StoredObject voters A WorkflowDocumentService was created that can be injected\ in context-specific StoredObject voters that need to check whether\ the document in question is attached to a workflow. --- ...panyingCourseDocumentStoredObjectVoter.php | 31 ++++++++++------ .../Service/WorkflowDocumentService.php | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php index db1499a99..931378a61 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -2,13 +2,15 @@ namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Security; class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterInterface { @@ -16,15 +18,15 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, - private readonly Security $security, - private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter + private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter, + private readonly WorkflowDocumentService $workflowDocumentService ){ } public function supports(string $attribute, StoredObject $subject): bool { // check if the stored object is linked to an AccompanyingCourseDocument - return !empty($this->repository->findLinkedCourseDocument($subject->getId())); + return $this->repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; } public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool @@ -34,17 +36,26 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn } // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + $voterAttribute = match($attribute) { + self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, + default => AccompanyingCourseDocumentVoter::SEE_DETAILS, + }; // Check access using AccompanyingCourseDocumentVoter - if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - // TODO implement logic to check for associated workflow - return true; - } else { + if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { return false; } + + // Check if entity is related to a workflow, if so, check if user can apply transition + $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); + + if ($relatedWorkflow instanceof EntityWorkflow){ + return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); + } + + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php new file mode 100644 index 000000000..498348b96 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -0,0 +1,35 @@ +repository->findByRelatedEntity(get_class($entity), $entity->getId()); + } + + public function canApplyTransition(EntityWorkflow $entityWorkflow): bool + { + if ($entityWorkflow->isFinal()) { + return false; + } + + $currentUser = $this->security->getUser(); + if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } + + return false; + } + +} From 1310d53589c4fbc0ca5ce6080aee18bcf195cb15 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 13:45:15 +0200 Subject: [PATCH 16/76] Implement context-specific voters for all current entities that can be linked to a document For reusability an AbstractStoredObjectVoter was created and a StoredObjectVoterInterface. A WorkflowDocumentService checks whether the StoredObject is involved in a workflow. --- .../Repository/ActivityRepository.php | 17 ++++- .../AccompanyingCourseDocumentRepository.php | 14 ++-- ...ssociatedEntityToStoredObjectInterface.php | 10 +++ .../Repository/PersonDocumentRepository.php | 13 +++- ...panyingCourseDocumentStoredObjectVoter.php | 61 ----------------- .../Authorization/StoredObjectVoter.php | 8 ++- .../StoredObjectVoterInterface.php | 5 +- .../AbstractStoredObjectVoter.php | 67 +++++++++++++++++++ .../AccompanyingCourseStoredObjectVoter.php | 46 +++++++++++++ ...gPeriodWorkEvaluationStoredObjectVoter.php | 50 ++++++++++++++ .../ActivityStoredObjectVoter.php | 50 ++++++++++++++ .../EventStoredObjectVoter.php | 49 ++++++++++++++ .../PersonStoredObjectVoter.php | 49 ++++++++++++++ .../Service/WorkflowDocumentService.php | 14 ++-- src/Bundle/ChillEventBundle/Entity/Event.php | 2 +- .../Repository/EventRepository.php | 59 ++++++++++++++-- .../Workflow/EntityWorkflowRepository.php | 16 +++++ ...PeriodWorkEvaluationDocumentRepository.php | 14 +++- 18 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index a7025d4a9..54672c6b7 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,17 @@ class ActivityRepository extends ServiceEntityRepository return $qb->getQuery()->getResult(); } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('a'); + $query = $qb + ->join('a.documents', 'ad') + ->join('ad.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 93cf00ecb..246c7f2d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -19,7 +19,7 @@ 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; @@ -46,12 +46,12 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { - + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { $qb = $this->repository->createQueryBuilder('d'); $query = $qb->where('d.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); + ->setParameter('storedObject', $storedObject) + ->getQuery(); return $query->getResult(); } @@ -66,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); } @@ -76,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..5349251c5 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -0,0 +1,10 @@ + */ -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.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php deleted file mode 100644 index 931378a61..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ /dev/null @@ -1,61 +0,0 @@ -repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; - } - - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool - { - if (!$token->getUser() instanceof User) { - return false; - } - - // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); - - // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = match($attribute) { - self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, - default => AccompanyingCourseDocumentVoter::SEE_DETAILS, - }; - - // Check access using AccompanyingCourseDocumentVoter - if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - return false; - } - - // Check if entity is related to a workflow, if so, check if user can apply transition - $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); - - if ($relatedWorkflow instanceof EntityWorkflow){ - return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); - } - - return true; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 7e2becab4..3bb5fa396 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -48,15 +48,19 @@ class StoredObjectVoter extends Voter return false; } + $attributeAsEnum = StoredObjectRoleEnum::from($attribute); + // Loop through context-specific voters foreach ($this->storedObjectVoters as $storedObjectVoter) { - if ($storedObjectVoter->supports($attribute, $subject)) { - return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { + return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token); } } // User role-based fallback if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { + // TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which + // is potentially detached from an existing entity. return true; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index c0fd7e09f..516722654 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,12 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, StoredObject $subject): bool; + public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php new file mode 100644 index 000000000..7ca73f206 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -0,0 +1,67 @@ +getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; + } + + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // 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/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php new file mode 100644 index 000000000..a41ea3067 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php @@ -0,0 +1,46 @@ +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/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php new file mode 100644 index 000000000..423730767 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return AccompanyingPeriodWorkEvaluationDocument::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the + // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? + return match ($attribute) { + StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php new file mode 100644 index 000000000..a36535005 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php new file mode 100644 index 000000000..218ce1980 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php new file mode 100644 index 000000000..6c3b0b807 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 498348b96..f796fe56e 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -13,19 +13,19 @@ class WorkflowDocumentService { } - public function getRelatedWorkflow($entity): ?EntityWorkflow + public function notBlockedByWorkflow($entity): bool { - return $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - } + /** + * @var EntityWorkflow + */ + $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - public function canApplyTransition(EntityWorkflow $entityWorkflow): bool - { - if ($entityWorkflow->isFinal()) { + if ($workflow->isFinal()) { return false; } $currentUser = $this->security->getUser(); - if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { return true; } 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/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 7406748b4..24eb095ec 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -11,17 +11,66 @@ 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') + ->join('ed.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } + + 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/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 66b3ab379..7d2e047d3 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,6 +99,22 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } + public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + { + $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/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..60dcdf1b1 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,12 +11,14 @@ 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\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -58,4 +60,14 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi { return AccompanyingPeriodWorkEvaluationDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('ed'); + $query = $qb->where('ed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From bd36735cb16be8e7cd640aaa598383fa4d562a56 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:06:02 +0200 Subject: [PATCH 17/76] Ensure single result when retrieving activity and event linked to stored object Although a many-to-many relationship exists between these entities and stored object, only one activity or event will ever be linked to a single stored object. For extra safety measure we return a single result in the repository to ensure our voters will keep working. --- .../ChillActivityBundle/Repository/ActivityRepository.php | 8 +++++++- .../ChillEventBundle/Repository/EventRepository.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 54672c6b7..4d621d56e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,6 +17,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -100,6 +102,10 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('a'); @@ -110,6 +116,6 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 24eb095ec..b720866f2 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,6 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -36,6 +38,10 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); @@ -46,7 +52,7 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } public function find($id) From d3956319caf28f98e5512f568a4ad138aa7dd223 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:56:25 +0200 Subject: [PATCH 18/76] Add test for AccompayingCourseStoredObjectVoter Mainly to check the voteOnAttribute method, by mocking a scenario where a person is allowed to see/edit an AccompanyingCourseDocument and not. --- ...ccompanyingCourseStoredObjectVoterTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php new file mode 100644 index 000000000..f7ad25987 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php @@ -0,0 +1,106 @@ +repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->security = $this->createMock(Security::class); + $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + + $this->voter = new AccompanyingCourseStoredObjectVoter( + $this->repository, + $this->security, + $this->workflowDocumentService + ); + } + + private function setupMockObjects(): array + { + $user = $this->createMock(User::class); + $token = $this->createMock(TokenInterface::class); + $subject = $this->createMock(StoredObject::class); + $entity = $this->createMock(AccompanyingCourseDocument::class); + + return [$user, $token, $subject, $entity]; + } + + private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $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 attributeToRole to return appropriate role + $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); + + // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument + $this->security->method('isGranted')->willReturnMap([ + [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], + ]); + + // Mock case where user is blocked or not by workflow + $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); + } + + public function testVoteOnAttributeAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $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); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeWhenBlockedByWorkflow(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + + // Test voteOnAttribute method + $attribute = StoredObjectRoleEnum::SEE; + $result = $this->voter->voteOnAttribute($attribute, $subject, $token); + + // Assert that access is denied when workflow is not allowed + $this->assertFalse($result); + } +} From 3a87513a114ded794c6e0302fe5e34851bf3b687 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:35:50 +0200 Subject: [PATCH 19/76] initial commit --- .changes/unreleased/Feature-20240614-153537.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/unreleased/Feature-20240614-153537.yaml 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" From 2d09efb2e0b88a2ed43b19deba58ca52446f971c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 16:48:09 +0200 Subject: [PATCH 20/76] Add StoredObjectVoterInterface An interface is defined that can be implemented by each context-specific voter in the future. --- .../StoredObjectVoterInterface.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php new file mode 100644 index 000000000..47b93eabb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -0,0 +1,22 @@ + Date: Fri, 14 Jun 2024 17:22:27 +0200 Subject: [PATCH 21/76] Refactorize StoredObjectVoter.php The StoredObjectVoter.php has been refactorized to handle context-specific voters.\ This way we can check if the context-specific voter should handle the authorization or not.\ If not, there is a simple fallback to check on the USER_ROLE. --- .../Authorization/StoredObjectVoter.php | 28 ++++++++++++++----- .../config/services/voter.yaml | 14 ++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/config/services/voter.yaml diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 2e253cf3c..ecfc56615 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -15,6 +15,7 @@ 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 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; */ class StoredObjectVoter extends Voter { + private $security; + private $storedObjectVoters; + + public function __construct(Security $security, iterable $storedObjectVoters) { + $this->security = $security; + $this->storedObjectVoters = $storedObjectVoters; + } + protected function supports($attribute, $subject): bool { return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum @@ -43,13 +52,18 @@ class StoredObjectVoter extends Voter return false; } - $askedRole = StoredObjectRoleEnum::from($attribute); - $tokenRoleAuthorization = - $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS); + // Loop through context-specific voters + foreach ($this->storedObjectVoters as $storedObjectVoter) { + if ($storedObjectVoter->supports($attribute, $subject)) { + return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + } + } - return match ($askedRole) { - StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization, - StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization - }; + // User role-based fallback + if ($this->security->isGranted('ROLE_USER')) { + return true; + } + + return false; } } diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml new file mode 100644 index 000000000..922d29cba --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: + arguments: + $storedObjectVoters: + # context specific voters + - '@accompanying_course_document_voter' + tags: + - { name: security.voter } + + accompanying_course_document_voter: + class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter From aaac80be8439fcbbbd788bd5840bc9f7664bec99 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 17:27:22 +0200 Subject: [PATCH 22/76] Add config voter.yaml The voter.yaml was not configured to be taken into account. Now added\ to ChillDocStoreExtension.php --- .../DependencyInjection/ChillDocStoreExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index fe9aeecfa..5f87199a3 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,6 +42,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/voter.yaml'); } public function prepend(ContainerBuilder $container) From 30078db841bc9402f464278cd45280c159d98886 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:51:21 +0200 Subject: [PATCH 23/76] Type-hint $subject in StoredObjectVoterInterface.php Since the subject passed to these voters should\ always be of the type StoredObject, type-hinting\ added. --- .../Security/Authorization/StoredObjectVoterInterface.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 47b93eabb..a3a76e79a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, $subject): bool; + public function supports(string $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool; + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; } From 26b3d84d624e39ffe21b8d8c394aa64d1a1c9a12 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:52:59 +0200 Subject: [PATCH 24/76] Add fall-back right for ROLE_ADMIN Within the StoredObjectVoter.php also the admin\ user should be able to edit documents as a fall-back --- .../Security/Authorization/StoredObjectVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index ecfc56615..781c2c542 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -60,7 +60,7 @@ class StoredObjectVoter extends Voter } // User role-based fallback - if ($this->security->isGranted('ROLE_USER')) { + if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { return true; } From 2ce9810243f07f3bcf7b663096825db1ab752cbe Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:00:10 +0200 Subject: [PATCH 25/76] Use constructor property promotion In accordance with php8.1 use property promotion\ within the constructor method. Less clutter. --- .../Security/Authorization/StoredObjectVoter.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 781c2c542..7e2becab4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,12 +24,8 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - private $security; - private $storedObjectVoters; - public function __construct(Security $security, iterable $storedObjectVoters) { - $this->security = $security; - $this->storedObjectVoters = $storedObjectVoters; + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { } protected function supports($attribute, $subject): bool From 830dace1ba2a021434ca4bd94126dff838f839bd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:02:25 +0200 Subject: [PATCH 26/76] Rename voter.yaml file to security.yaml For consistency with other bundles voters are\ registered under the security.yaml file. --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- .../config/services/{voter.yaml => security.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Bundle/ChillDocStoreBundle/config/services/{voter.yaml => security.yaml} (100%) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 5f87199a3..a0fcc2d5d 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,7 +42,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/voter.yaml'); + $loader->load('services/security.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml similarity index 100% rename from src/Bundle/ChillDocStoreBundle/config/services/voter.yaml rename to src/Bundle/ChillDocStoreBundle/config/services/security.yaml From 062afd669596f5fb8cbfb5383bce8b2ed7c36c69 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:16:39 +0200 Subject: [PATCH 27/76] Use service tags to inject all voters into StoredObjectVoter.php Instead of manually injecting services into StoredObjectVoter\ config is added to automatically tag each service that implements\ StoredObjectVoterInterface.php --- .../DependencyInjection/ChillDocStoreExtension.php | 3 +++ .../ChillDocStoreBundle/config/services/security.yaml | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index a0fcc2d5d..9f277e716 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 ChillDocStoreBundle\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'); diff --git a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml index 922d29cba..c57eb63c5 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml @@ -4,11 +4,10 @@ services: autoconfigure: true Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: arguments: - $storedObjectVoters: - # context specific voters - - '@accompanying_course_document_voter' + $storedObjectVoters: !tagged_iterator stored_object_voter tags: - { name: security.voter } - accompanying_course_document_voter: - class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter + Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter: + tags: + - { name: security.voter } From f75c7a0232dda4b410c96a95652aa0e8594c675f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:21:24 +0200 Subject: [PATCH 28/76] Implement StoredObjectVoterInterface An interface was created to be implemented by Stored Doc voters\ these will automatically be tagged and injected into DocStoreVoter. --- .../Authorization/AccompanyingCourseDocumentVoter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 5febc7e42..93243b1f4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,11 +19,12 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; @@ -70,12 +71,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; From d9892f6822fff3fbc3526fd988e6ad2f73bc2a61 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 10:53:33 +0200 Subject: [PATCH 29/76] Correct namespace and use statement for StoredObjectVoterInterface.php The namespace was formed wrong and needed adjustment --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 1 - .../Security/Authorization/StoredObjectVoterInterface.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 93243b1f4..dac08b05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,7 +19,6 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index a3a76e79a..c0fd7e09f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace ChillDocStoreBundle\Security\Authorization; +namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; From e54633d14dd75ac3e596a5a78334a4277bee478f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 15:17:56 +0200 Subject: [PATCH 30/76] Implement voting logic: separation of concerns A separate AccompanyingCourseDocumentStoredObjectVoter was\ created to handle the specific access to a Stored object\ related to an Accompanying Course Document. --- .../AccompanyingCourseDocumentRepository.php | 13 +++++ ...panyingCourseDocumentStoredObjectVoter.php | 50 +++++++++++++++++++ ...nyingPeriodWorkEvaluationDocumentVoter.php | 7 +-- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 2679993c4..239b88b90 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -12,6 +12,7 @@ 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; @@ -45,6 +46,17 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->leftJoin('d.storedObject', 'do') + ->where('do.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObjectId) + ->getQuery(); + + return $query->getResult(); + } + public function find($id): ?AccompanyingCourseDocument { return $this->repository->find($id); @@ -69,4 +81,5 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository { return AccompanyingCourseDocument::class; } + } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php new file mode 100644 index 000000000..db1499a99 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository->findLinkedCourseDocument($subject->getId())); + } + + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // Retrieve the related accompanying course document + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + + // Determine the attribute to pass to AccompanyingCourseDocumentVoter + $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + + // Check access using AccompanyingCourseDocumentVoter + if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { + // TODO implement logic to check for associated workflow + return true; + } else { + return false; + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 77c131cd9..8f4a1b995 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -21,13 +22,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; 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; @@ -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( From cce04ee4900636bd9b92f3c63d713d0238854390 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:27:21 +0200 Subject: [PATCH 31/76] Remove implementation of StoredObjectVoterInterface in AccompanyingCourseDocumentVoter.php This implementation has been moved to the voter\ AccompanyingCourseDocumentStoredObjectVoter.php --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index dac08b05b..7a46184cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; From 7c03a25f1a8fb37cdddf1986bb0271af23fd5b52 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:28:19 +0200 Subject: [PATCH 32/76] Refactor AccompanyingCourseDocumentRepository.php Build where clause using StoredObject directly instead\ of based on it's id. --- .../Repository/AccompanyingCourseDocumentRepository.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 239b88b90..93cf00ecb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -46,12 +46,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->leftJoin('d.storedObject', 'do') - ->where('do.id = :storedObjectId') - ->setParameter('storedObjectId', $storedObjectId) + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) ->getQuery(); return $query->getResult(); From 4607c36b574916aa148e7973e15c4f51074cb315 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:32:09 +0200 Subject: [PATCH 33/76] Add WorkflowDocumentService and use in StoredObject voters A WorkflowDocumentService was created that can be injected\ in context-specific StoredObject voters that need to check whether\ the document in question is attached to a workflow. --- ...panyingCourseDocumentStoredObjectVoter.php | 31 ++++++++++------ .../Service/WorkflowDocumentService.php | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php index db1499a99..931378a61 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -2,13 +2,15 @@ namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Security; class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterInterface { @@ -16,15 +18,15 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, - private readonly Security $security, - private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter + private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter, + private readonly WorkflowDocumentService $workflowDocumentService ){ } public function supports(string $attribute, StoredObject $subject): bool { // check if the stored object is linked to an AccompanyingCourseDocument - return !empty($this->repository->findLinkedCourseDocument($subject->getId())); + return $this->repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; } public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool @@ -34,17 +36,26 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn } // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + $voterAttribute = match($attribute) { + self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, + default => AccompanyingCourseDocumentVoter::SEE_DETAILS, + }; // Check access using AccompanyingCourseDocumentVoter - if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - // TODO implement logic to check for associated workflow - return true; - } else { + if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { return false; } + + // Check if entity is related to a workflow, if so, check if user can apply transition + $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); + + if ($relatedWorkflow instanceof EntityWorkflow){ + return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); + } + + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php new file mode 100644 index 000000000..498348b96 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -0,0 +1,35 @@ +repository->findByRelatedEntity(get_class($entity), $entity->getId()); + } + + public function canApplyTransition(EntityWorkflow $entityWorkflow): bool + { + if ($entityWorkflow->isFinal()) { + return false; + } + + $currentUser = $this->security->getUser(); + if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } + + return false; + } + +} From c06e76a0ee8e2812f9efa9746e168b6b5239c95f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 13:45:15 +0200 Subject: [PATCH 34/76] Implement context-specific voters for all current entities that can be linked to a document For reusability an AbstractStoredObjectVoter was created and a StoredObjectVoterInterface. A WorkflowDocumentService checks whether the StoredObject is involved in a workflow. --- .../Repository/ActivityRepository.php | 17 ++++- .../AccompanyingCourseDocumentRepository.php | 14 ++-- ...ssociatedEntityToStoredObjectInterface.php | 10 +++ .../Repository/PersonDocumentRepository.php | 13 +++- ...panyingCourseDocumentStoredObjectVoter.php | 61 ----------------- .../Authorization/StoredObjectVoter.php | 8 ++- .../StoredObjectVoterInterface.php | 5 +- .../AbstractStoredObjectVoter.php | 67 +++++++++++++++++++ .../AccompanyingCourseStoredObjectVoter.php | 46 +++++++++++++ ...gPeriodWorkEvaluationStoredObjectVoter.php | 50 ++++++++++++++ .../ActivityStoredObjectVoter.php | 50 ++++++++++++++ .../EventStoredObjectVoter.php | 49 ++++++++++++++ .../PersonStoredObjectVoter.php | 49 ++++++++++++++ .../Service/WorkflowDocumentService.php | 14 ++-- src/Bundle/ChillEventBundle/Entity/Event.php | 2 +- .../Repository/EventRepository.php | 59 ++++++++++++++-- .../Workflow/EntityWorkflowRepository.php | 16 +++++ ...PeriodWorkEvaluationDocumentRepository.php | 14 +++- 18 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index a7025d4a9..54672c6b7 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,17 @@ class ActivityRepository extends ServiceEntityRepository return $qb->getQuery()->getResult(); } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('a'); + $query = $qb + ->join('a.documents', 'ad') + ->join('ad.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 93cf00ecb..246c7f2d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -19,7 +19,7 @@ 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; @@ -46,12 +46,12 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { - + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { $qb = $this->repository->createQueryBuilder('d'); $query = $qb->where('d.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); + ->setParameter('storedObject', $storedObject) + ->getQuery(); return $query->getResult(); } @@ -66,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); } @@ -76,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..5349251c5 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -0,0 +1,10 @@ + */ -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.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php deleted file mode 100644 index 931378a61..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ /dev/null @@ -1,61 +0,0 @@ -repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; - } - - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool - { - if (!$token->getUser() instanceof User) { - return false; - } - - // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); - - // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = match($attribute) { - self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, - default => AccompanyingCourseDocumentVoter::SEE_DETAILS, - }; - - // Check access using AccompanyingCourseDocumentVoter - if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - return false; - } - - // Check if entity is related to a workflow, if so, check if user can apply transition - $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); - - if ($relatedWorkflow instanceof EntityWorkflow){ - return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); - } - - return true; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 7e2becab4..3bb5fa396 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -48,15 +48,19 @@ class StoredObjectVoter extends Voter return false; } + $attributeAsEnum = StoredObjectRoleEnum::from($attribute); + // Loop through context-specific voters foreach ($this->storedObjectVoters as $storedObjectVoter) { - if ($storedObjectVoter->supports($attribute, $subject)) { - return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { + return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token); } } // User role-based fallback if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { + // TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which + // is potentially detached from an existing entity. return true; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index c0fd7e09f..516722654 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,12 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, StoredObject $subject): bool; + public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php new file mode 100644 index 000000000..7ca73f206 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -0,0 +1,67 @@ +getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; + } + + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // 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/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php new file mode 100644 index 000000000..a41ea3067 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php @@ -0,0 +1,46 @@ +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/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php new file mode 100644 index 000000000..423730767 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return AccompanyingPeriodWorkEvaluationDocument::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the + // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? + return match ($attribute) { + StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php new file mode 100644 index 000000000..a36535005 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php new file mode 100644 index 000000000..218ce1980 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php new file mode 100644 index 000000000..6c3b0b807 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 498348b96..f796fe56e 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -13,19 +13,19 @@ class WorkflowDocumentService { } - public function getRelatedWorkflow($entity): ?EntityWorkflow + public function notBlockedByWorkflow($entity): bool { - return $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - } + /** + * @var EntityWorkflow + */ + $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - public function canApplyTransition(EntityWorkflow $entityWorkflow): bool - { - if ($entityWorkflow->isFinal()) { + if ($workflow->isFinal()) { return false; } $currentUser = $this->security->getUser(); - if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { return true; } 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/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 7406748b4..24eb095ec 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -11,17 +11,66 @@ 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') + ->join('ed.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } + + 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/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 66b3ab379..7d2e047d3 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,6 +99,22 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } + public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + { + $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/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..60dcdf1b1 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,12 +11,14 @@ 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\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -58,4 +60,14 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi { return AccompanyingPeriodWorkEvaluationDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('ed'); + $query = $qb->where('ed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From a25f2c7539aeb1f9261ff2f22f75c1f5e591e60b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:06:02 +0200 Subject: [PATCH 35/76] Ensure single result when retrieving activity and event linked to stored object Although a many-to-many relationship exists between these entities and stored object, only one activity or event will ever be linked to a single stored object. For extra safety measure we return a single result in the repository to ensure our voters will keep working. --- .../ChillActivityBundle/Repository/ActivityRepository.php | 8 +++++++- .../ChillEventBundle/Repository/EventRepository.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 54672c6b7..4d621d56e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,6 +17,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -100,6 +102,10 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('a'); @@ -110,6 +116,6 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 24eb095ec..b720866f2 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,6 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -36,6 +38,10 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); @@ -46,7 +52,7 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } public function find($id) From bab6528ed6e7f85471cead28f639ee55088ac359 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:56:25 +0200 Subject: [PATCH 36/76] Add test for AccompayingCourseStoredObjectVoter Mainly to check the voteOnAttribute method, by mocking a scenario where a person is allowed to see/edit an AccompanyingCourseDocument and not. --- ...ccompanyingCourseStoredObjectVoterTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php new file mode 100644 index 000000000..f7ad25987 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php @@ -0,0 +1,106 @@ +repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->security = $this->createMock(Security::class); + $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + + $this->voter = new AccompanyingCourseStoredObjectVoter( + $this->repository, + $this->security, + $this->workflowDocumentService + ); + } + + private function setupMockObjects(): array + { + $user = $this->createMock(User::class); + $token = $this->createMock(TokenInterface::class); + $subject = $this->createMock(StoredObject::class); + $entity = $this->createMock(AccompanyingCourseDocument::class); + + return [$user, $token, $subject, $entity]; + } + + private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $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 attributeToRole to return appropriate role + $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); + + // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument + $this->security->method('isGranted')->willReturnMap([ + [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], + ]); + + // Mock case where user is blocked or not by workflow + $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); + } + + public function testVoteOnAttributeAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $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); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeWhenBlockedByWorkflow(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + + // Test voteOnAttribute method + $attribute = StoredObjectRoleEnum::SEE; + $result = $this->voter->voteOnAttribute($attribute, $subject, $token); + + // Assert that access is denied when workflow is not allowed + $this->assertFalse($result); + } +} From 742f2540f69685aaa4ce1c967bc668e3e397570d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 27 Jun 2024 11:59:31 +0200 Subject: [PATCH 37/76] Setup AccompanyingPeriodWorkEvaluationStoredObjectVoter.php to use AccompanyingPeriodWorkRepository.php The voter was not checking the correct permissions to\ establish whether a user can see/edit a storedObject\ The right to see/edit an AccompanyingPeriodWork has to\ be checked. --- ...ingPeriodWorkEvaluationStoredObjectVoter.php | 15 +++++++-------- ...ngPeriodWorkEvaluationDocumentRepository.php | 11 +---------- .../AccompanyingPeriodWorkRepository.php | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php index 423730767..72e422776 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -6,15 +6,15 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; -use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; -use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Symfony\Component\Security\Core\Security; class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredObjectVoter { public function __construct( - private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, + private readonly AccompanyingPeriodWorkRepository $repository, Security $security, WorkflowDocumentService $workflowDocumentService ){ @@ -31,15 +31,14 @@ class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredOb */ protected function getClass(): string { - return AccompanyingPeriodWorkEvaluationDocument::class; + return AccompanyingPeriodWork::class; } protected function attributeToRole(StoredObjectRoleEnum $attribute): string { - //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the - // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? return match ($attribute) { - StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkVoter::SEE, + StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkVoter::UPDATE, }; } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 60dcdf1b1..36de5ce3e 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -18,7 +18,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository { private readonly EntityRepository $repository; @@ -61,13 +61,4 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi return AccompanyingPeriodWorkEvaluationDocument::class; } - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object - { - $qb = $this->repository->createQueryBuilder('ed'); - $query = $qb->where('ed.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); - - return $query->getResult(); - } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 95b995e74..43ecd5337 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -22,7 +24,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository +final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private EntityRepository $repository; @@ -251,4 +253,17 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor return $qb; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWork + { + $qb = $this->repository->createQueryBuilder('acpw'); + $query = $qb + ->join('acpw.evaluations', 'acpwe') + ->join('acpwe.documents', 'acpwed') + ->where('acpwed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From efaad1981d2742c8e99fc27f9d51a6e641e4e498 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 27 Jun 2024 12:44:36 +0200 Subject: [PATCH 38/76] Fix namespaces and move voters to corresponding bundles --- .../ChillActivityBundle/Repository/ActivityRepository.php | 2 +- .../Security/Authorization}/ActivityStoredObjectVoter.php | 2 +- ...er.php => AccompanyingCourseDocumentStoredObjectVoter.php} | 4 ++-- .../AccompanyingPeriodWorkEvaluationStoredObjectVoter.php | 3 +-- ...redObjectVoter.php => PersonDocumentStoredObjectVoter.php} | 4 ++-- .../Security/Authorization}/EventStoredObjectVoter.php | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillActivityBundle/Security/Authorization}/ActivityStoredObjectVoter.php (94%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{AccompanyingCourseStoredObjectVoter.php => AccompanyingCourseDocumentStoredObjectVoter.php} (90%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{PersonStoredObjectVoter.php => PersonDocumentStoredObjectVoter.php} (90%) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillEventBundle/Security/Authorization}/EventStoredObjectVoter.php (94%) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 4d621d56e..47ecb66dc 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -106,7 +106,7 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn * @throws NonUniqueResultException * @throws NoResultException */ - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity { $qb = $this->createQueryBuilder('a'); $query = $qb diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php similarity index 94% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php rename to src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index a36535005..01ec68bd9 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -1,6 +1,6 @@ Date: Mon, 1 Jul 2024 12:14:03 +0200 Subject: [PATCH 39/76] Implement logic to check if editing of document is blocked by workflow Using the workflow handlers we return the workflow that is attached to an object so that within the workflowDocumentService we can then check whether this workflow blocks the edition of a document. --- .../Service/WorkflowDocumentService.php | 26 +++++++++++-------- ...ompanyingCourseDocumentWorkflowHandler.php | 20 +++++++++----- .../EntityWorkflowHandlerInterface.php | 2 ++ .../Workflow/EntityWorkflowManager.php | 9 +++++++ ...dWorkEvaluationDocumentWorkflowHandler.php | 16 +++++++++++- ...ingPeriodWorkEvaluationWorkflowHandler.php | 17 +++++++++++- .../AccompanyingPeriodWorkWorkflowHandler.php | 16 +++++++++++- 7 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index f796fe56e..17591b2d0 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -4,32 +4,36 @@ namespace Chill\DocStoreBundle\Service; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; +use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; class WorkflowDocumentService { - public function __construct(private readonly Security $security, private readonly EntityWorkflowRepository $repository) + public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) { } - public function notBlockedByWorkflow($entity): bool + public function notBlockedByWorkflow(object $entity): bool { /** * @var EntityWorkflow */ - $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - - if ($workflow->isFinal()) { - return false; - } - + $workflow = $this->entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { - return true; + + + if (null !== $workflow) { + if ($workflow->isFinal()) { + return false; + } + + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } } - return false; + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 9d1b619f9..e3f201914 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -12,8 +12,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Workflow; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Doctrine\ORM\EntityManagerInterface; @@ -22,16 +24,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - private readonly EntityRepository $repository; - /** - * TODO: injecter le repository directement. - */ public function __construct( EntityManagerInterface $em, - private readonly TranslatorInterface $translator + private readonly TranslatorInterface $translator, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly AccompanyingCourseDocumentRepository $repository ) { - $this->repository = $em->getRepository(AccompanyingCourseDocument::class); } public function getDeletionRoles(): array @@ -126,4 +125,13 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if(!$object instanceof AccompanyingCourseDocument) { + return null; + } + + return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId()); + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index 1d185ba0e..bff1057c8 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -51,4 +51,6 @@ interface EntityWorkflowHandlerInterface public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; + + public function findByRelatedEntity(object $object): ?EntityWorkflow; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 9a1f52280..3f2ff0439 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -37,4 +37,13 @@ class EntityWorkflowManager { return $this->registry->all($entityWorkflow); } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + foreach ($this->handlers as $handler) { + return $handler->findByRelatedEntity($object); + } + return null; + } + } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fc13224e..0fd3af0af 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.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\AccompanyingPeriodWorkEvaluationDocument; @@ -21,7 +22,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - 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 { @@ -128,4 +134,12 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { + return null; + } + 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 63b34d1dc..e007f03f7 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; @@ -22,7 +23,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkEvaluationRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private readonly AccompanyingPeriodWorkEvaluationRepository $repository, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -114,4 +120,13 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWorkEvaluation) { + return null; + } + + 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 5c74e5b17..81116b7b2 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; @@ -23,7 +24,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private readonly AccompanyingPeriodWorkRepository $repository, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -121,4 +127,12 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWork) { + return null; + } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId()); + } } From 254122d125151cf16634de0a858bac6bd2f10c65 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:20:21 +0200 Subject: [PATCH 40/76] Remove check to see if user is instance of User The admin user would not be identified as a User. --- .../StoredObjectVoters/AbstractStoredObjectVoter.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php index 7ca73f206..659d7d28a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -40,10 +40,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { - if (!$token->getUser() instanceof User) { - return false; - } - // Retrieve the related accompanying course document $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); From d1653a074bb3595bed9e828f16b8f90325e3272f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:21:25 +0200 Subject: [PATCH 41/76] Implement test on AbstractStoredObjectVoter To avoid having to duplicate tests, a test is written\ for the abstract voter. --- ....php => AbstractStoredObjectVoterTest.php} | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) rename src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/{AccompanyingCourseStoredObjectVoterTest.php => AbstractStoredObjectVoterTest.php} (73%) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php similarity index 73% rename from src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php rename to src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index f7ad25987..d5ab471ab 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -5,15 +5,17 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; use ChillDocStoreBundle\Security\Authorization\StoredObjectVoters\AccompanyingCourseStoredObjectVoter; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase +class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase { private $repository; private $security; @@ -26,11 +28,28 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase $this->security = $this->createMock(Security::class); $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); - $this->voter = new AccompanyingCourseStoredObjectVoter( - $this->repository, - $this->security, - $this->workflowDocumentService - ); + // Anonymous class extending the abstract class + $this->voter = new class($this->repository, $this->security, $this->workflowDocumentService) extends AbstractStoredObjectVoter { + protected function attributeToRole($attribute): string + { + return AccompanyingCourseDocumentVoter::SEE_DETAILS; + } + + protected function getRepository(): AssociatedEntityToStoredObjectInterface + { + // TODO: Implement getRepository() method. + } + + protected function getClass(): string + { + // TODO: Implement getClass() method. + } + + protected function canBeAssociatedWithWorkflow(): bool + { + // TODO: Implement canBeAssociatedWithWorkflow() method. + } + }; } private function setupMockObjects(): array @@ -94,7 +113,7 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase list($user, $token, $subject, $entity) = $this->setupMockObjects(); // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false); // Test voteOnAttribute method $attribute = StoredObjectRoleEnum::SEE; @@ -103,4 +122,16 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Assert that access is denied when workflow is not allowed $this->assertFalse($result); } + + public function testAbstractStoredObjectVoter(): void + { + $voter = new class extends AbstractStoredObjectVoter { + // Implement abstract methods here + public function someMethod() { + // method implementation + } + }; + // Run tests on $voter + } + } From fb743b522d35aea4fd82ed6fa2ef59fe1b2fc688 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:23:31 +0200 Subject: [PATCH 42/76] Remove implementation of StoredObjectInterface --- .../AccompanyingPeriodWorkEvaluationDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 8f4a1b995..ecf3cd802 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -22,7 +22,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; From 8c92d117225b77a9cd5951ddd721311ad3ebd022 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:23:07 +0200 Subject: [PATCH 43/76] Implement permissions for WOPI --- .../src/Service/Wopi/AuthorizationManager.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index c54f187ca..9ed421461 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -65,7 +65,11 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted('SEE', $document)) { + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool @@ -75,6 +79,11 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted('EDIT', $document)) { + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } + } From ac2f314395514328d8ddb5bdba7fbec9f291e581 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:23:32 +0200 Subject: [PATCH 44/76] Implement permissions for download button group --- .../Templating/WopiEditTwigExtensionRuntime.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index 716cb422e..be45bd2d4 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, ) {} /** @@ -150,6 +152,8 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt */ public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string { + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $document); + $accessToken = $this->davTokenProvider->createToken( $document, $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE From 5b0babb9b08d260bfc0c64be37b6b43feb4ba270 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:37:47 +0200 Subject: [PATCH 45/76] Implement permissions in AsyncUploadVoter.php --- .../Security/Authorization/AsyncUploadVoter.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index 1d7ad759a..d458bae08 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -36,6 +36,13 @@ final class AsyncUploadVoter extends Voter return false; } + //TODO get the StoredObject from the SignedUrl +/* match($subject->method) { + 'GET' => $this->security->isGranted('SEE', $storedObject), + 'PUT' => $this->security->isGranted('EDIT', $storedObject), + 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + };*/ + return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'); } } From 064dfc5a5655b619e5e0472644fb929769674a8c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:48:18 +0200 Subject: [PATCH 46/76] Fix repositories to fetch entity linked to stored object getSingleResult() replaced by getOneOrNullResult() to\ avoid error being thrown. Fix naming of properties. --- .../Repository/ActivityRepository.php | 11 +++-------- .../AccompanyingCourseDocumentRepository.php | 4 ++-- .../Repository/PersonDocumentRepository.php | 4 ++-- .../ChillEventBundle/Repository/EventRepository.php | 9 ++------- ...mpanyingPeriodWorkEvaluationDocumentRepository.php | 2 -- .../AccompanyingPeriodWorkRepository.php | 4 ++-- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 47ecb66dc..4942eef16 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -102,20 +102,15 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } - /** - * @throws NonUniqueResultException - * @throws NoResultException - */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity { $qb = $this->createQueryBuilder('a'); $query = $qb - ->join('a.documents', 'ad') - ->join('ad.storedObject', 'so') - ->where('so.id = :storedObjectId') + ->leftJoin('a.documents', 'ad') + ->where('ad.id = :storedObjectId') ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getSingleResult(); + return $query->getOneOrNullResult(); } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 246c7f2d9..744afd424 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -49,11 +49,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->where('d.storedObject = :storedObject') + $query = $qb->where('d.object = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } public function find($id): ?AccompanyingCourseDocument diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php index 57a964867..d0ef4c9d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php @@ -58,10 +58,10 @@ readonly class PersonDocumentRepository implements ObjectRepository, AssociatedE public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->where('d.storedObject = :storedObject') + $query = $qb->where('d.object = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index b720866f2..03ee366ac 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -38,21 +38,16 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } - /** - * @throws NonUniqueResultException - * @throws NoResultException - */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); $query = $qb ->join('e.documents', 'ed') - ->join('ed.storedObject', 'so') - ->where('so.id = :storedObjectId') + ->where('ed.id = :storedObjectId') ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getSingleResult(); + return $query->getOneOrNullResult(); } public function find($id) diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 95e0075e2..59bb3f915 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,8 +11,6 @@ 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; diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 43ecd5337..324c3c176 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -258,12 +258,12 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor { $qb = $this->repository->createQueryBuilder('acpw'); $query = $qb - ->join('acpw.evaluations', 'acpwe') + ->join('acpw.accompanyingPeriodWorkEvaluations', 'acpwe') ->join('acpwe.documents', 'acpwed') ->where('acpwed.storedObject = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } } From 03800029c90c030b38e5e7c6c1b87ca7c7c295d1 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:49:29 +0200 Subject: [PATCH 47/76] Fix the import of StoredObjectVoterInterface --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 9f277e716..b156af1ea 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -14,7 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection; use Chill\DocStoreBundle\Controller\StoredObjectApiController; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; From c19c597ba06e6874eb681e94c7960a8bbb09e686 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:50:44 +0200 Subject: [PATCH 48/76] Fix checking of permissions within document_button_group --- .../Resources/views/List/list_item.html.twig | 4 ++-- .../Security/Authorization/StoredObjectVoter.php | 2 ++ .../Templating/WopiEditTwigExtensionRuntime.php | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) 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/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 3bb5fa396..57688e05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -37,6 +37,7 @@ 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) @@ -47,6 +48,7 @@ class StoredObjectVoter extends Voter if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) { return false; } + */ $attributeAsEnum = StoredObjectRoleEnum::from($attribute); diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index be45bd2d4..b6290049c 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php @@ -150,9 +150,9 @@ 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, $document); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons; $accessToken = $this->davTokenProvider->createToken( $document, From a9f4f8c973cc9d4ef797c7108955e03eefb0a13d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 14:17:05 +0200 Subject: [PATCH 49/76] Resolve phpstan erorrs --- .../Service/WorkflowDocumentService.php | 2 +- .../AccompanyingCourseDocumentWorkflowHandler.php | 9 ++++----- .../Form/ChoiceLoader/EventChoiceLoader.php | 6 ++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 17591b2d0..a279912a9 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -23,7 +23,7 @@ class WorkflowDocumentService $currentUser = $this->security->getUser(); - if (null !== $workflow) { + if (null != $workflow) { if ($workflow->isFinal()) { return false; } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index e3f201914..d1a384ef3 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -22,14 +22,13 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface +readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { public function __construct( - EntityManagerInterface $em, - private readonly TranslatorInterface $translator, - private readonly EntityWorkflowRepository $workflowRepository, - private readonly AccompanyingCourseDocumentRepository $repository + private TranslatorInterface $translator, + private EntityWorkflowRepository $workflowRepository, + private AccompanyingCourseDocumentRepository $repository ) { } diff --git a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php index 9aa001170..19829a369 100644 --- a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php +++ b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Form\ChoiceLoader; use Chill\EventBundle\Entity\Event; +use Chill\EventBundle\Repository\EventRepository; use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; @@ -26,9 +27,6 @@ class EventChoiceLoader implements ChoiceLoaderInterface */ protected $centers = []; - /** - * @var EntityRepository - */ protected $eventRepository; /** @@ -40,7 +38,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface * EventChoiceLoader constructor. */ public function __construct( - EntityRepository $eventRepository, + EventRepository $eventRepository, ?array $centers = null ) { $this->eventRepository = $eventRepository; From 3262a1dd02df05c5f5f5763599f5cfd83dbdc831 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:35:41 +0200 Subject: [PATCH 50/76] Implement StoredObject permissions in AsyncUploadVoter.php --- .../TempUrlOpenstackGenerator.php | 2 +- .../AsyncUpload/SignedUrl.php | 1 + .../Security/Authorization/AsyncUploadVoter.php | 16 +++++++++------- .../TempUrlOpenstackGeneratorTest.php | 3 ++- .../Templating/AsyncUploadExtensionTest.php | 2 +- .../Controller/AsyncUploadControllerTest.php | 3 ++- .../Normalizer/SignedUrlNormalizerTest.php | 3 ++- .../Tests/Service/StoredObjectManagerTest.php | 3 ++- .../Constraints/AsyncFileExistsValidatorTest.php | 2 +- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php index e410529b4..06f660fbe 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php @@ -127,7 +127,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..1047e1344 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php @@ -21,6 +21,7 @@ readonly class SignedUrl #[Serializer\Groups(['read'])] public string $url, public \DateTimeImmutable $expires, + public string $object_name, ) {} #[Serializer\Groups(['read'])] diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index d458bae08..c92dffcd5 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\AsyncUpload\SignedUrl; +use Chill\DocStoreBundle\Entity\StoredObject; +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 +24,7 @@ final class AsyncUploadVoter extends Voter public function __construct( private readonly Security $security, + private readonly StoredObjectRepository $storedObjectRepository ) {} protected function supports($attribute, $subject): bool @@ -36,13 +39,12 @@ final class AsyncUploadVoter extends Voter return false; } - //TODO get the StoredObject from the SignedUrl -/* match($subject->method) { - 'GET' => $this->security->isGranted('SEE', $storedObject), - 'PUT' => $this->security->isGranted('EDIT', $storedObject), - 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') - };*/ + $storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]); - return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'); + return match($subject->method) { + 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), + 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), + default => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + }; } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php index e6250202f..f9cc49fa6 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) { 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..da223cea6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php @@ -87,7 +87,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/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index cb5294fff..2029fac74 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']]); 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); } From 345f379650a6ae7ab813d387d432de118ab88faf Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:39:31 +0200 Subject: [PATCH 51/76] Implement StoredObject permissions WOPI AuthorizationManager.php --- .../src/Service/Wopi/AuthorizationManager.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 9ed421461..9b70ea049 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Entity\Document; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\MainBundle\Entity\User; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Psr\Http\Message\RequestInterface; @@ -60,12 +61,17 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $document)) { + + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('SEE', $document)) { + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $document)) { return $this->isTokenValid($accessToken, $document, $request); } @@ -79,7 +85,7 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('EDIT', $document)) { + if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document)) { return $this->isTokenValid($accessToken, $document, $request); } From 3d7c8596eeb0bd32c92873c6381978462863f5f9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:49:53 +0200 Subject: [PATCH 52/76] Pass StoredObject instead of Document to check permission in AuthorizationManager.php --- .../src/Service/Wopi/AuthorizationManager.php | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 9ed421461..af78eb73c 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,21 @@ 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 { - if ($this->security->isGranted('SEE', $document)) { + $storedObject = $this->getRelatedStoredObject($document); + + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { return $this->isTokenValid($accessToken, $document, $request); } @@ -79,7 +95,9 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('EDIT', $document)) { + $storedObject = $this->getRelatedStoredObject($document); + + if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) { return $this->isTokenValid($accessToken, $document, $request); } From e9a9a3430f86c95f73cd5b4de9f2c77f007b50ae Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:27:16 +0200 Subject: [PATCH 53/76] Complete AbstractStoredObjectVoterTest.php --- .../AbstractStoredObjectVoterTest.php | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index d5ab471ab..c384e48b9 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -17,10 +17,9 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase { - private $repository; - private $security; - private $workflowDocumentService; - private $voter; + private AssociatedEntityToStoredObjectInterface $repository; + private Security $security; + private WorkflowDocumentService $workflowDocumentService; protected function setUp(): void { @@ -54,10 +53,10 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase private function setupMockObjects(): array { - $user = $this->createMock(User::class); + $user = new User(); $token = $this->createMock(TokenInterface::class); - $subject = $this->createMock(StoredObject::class); - $entity = $this->createMock(AccompanyingCourseDocument::class); + $subject = new StoredObject(); + $entity = new \stdClass(); return [$user, $token, $subject, $entity]; } @@ -90,9 +89,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); // The voteOnAttribute method should return True when workflow is allowed - $attributeSee = StoredObjectRoleEnum::SEE; - $attributeEdit = StoredObjectRoleEnum::EDIT; - $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } public function testVoteOnAttributeNotAllowed(): void @@ -101,11 +98,10 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // 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 - $attributeSee = StoredObjectRoleEnum::SEE; - $attributeEdit = StoredObjectRoleEnum::EDIT; - $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } public function testVoteOnAttributeWhenBlockedByWorkflow(): void @@ -114,10 +110,11 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // 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 = $this->voter->voteOnAttribute($attribute, $subject, $token); + $result = $voter->voteOnAttribute($attribute, $subject, $token); // Assert that access is denied when workflow is not allowed $this->assertFalse($result); From 719fabc87895554ad6dcd88a69155286efae027d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:27:54 +0200 Subject: [PATCH 54/76] Check permissions within StoredObjectNormalizer.php --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index f6eb5a1cb..4a0ba6dab 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -15,6 +15,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\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -32,7 +33,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa public function __construct( private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider, - private readonly UrlGeneratorInterface $urlGenerator + private readonly UrlGeneratorInterface $urlGenerator, + private readonly Security $security ) {} public function normalize($object, ?string $format = null, array $context = []) From 5d57ec8a3b533e1c07bc060544997784b8047ad4 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:38:13 +0200 Subject: [PATCH 55/76] Complete AbstractStoredObjectVoterTest.php --- .../AbstractStoredObjectVoterTest.php | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index c384e48b9..24370c2db 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -1,21 +1,33 @@ repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); $this->security = $this->createMock(Security::class); $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + } + private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowDocumentService $workflowDocumentService = null): AbstractStoredObjectVoter + { // Anonymous class extending the abstract class - $this->voter = new class($this->repository, $this->security, $this->workflowDocumentService) extends AbstractStoredObjectVoter { + return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { + public function __construct( + private bool $canBeAssociatedWithWorkflow, + private AssociatedEntityToStoredObjectInterface $repository, + Security $security, + ?WorkflowDocumentService $workflowDocumentService = null + ) { + parent::__construct($security, $workflowDocumentService); + } + protected function attributeToRole($attribute): string { - return AccompanyingCourseDocumentVoter::SEE_DETAILS; + return 'SOME_ROLE'; } protected function getRepository(): AssociatedEntityToStoredObjectInterface { - // TODO: Implement getRepository() method. + return $this->repository; } protected function getClass(): string { - // TODO: Implement getClass() method. + return \stdClass::class; } protected function canBeAssociatedWithWorkflow(): bool { - // TODO: Implement canBeAssociatedWithWorkflow() method. + return $this->canBeAssociatedWithWorkflow; } }; } @@ -61,7 +85,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase return [$user, $token, $subject, $entity]; } - private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $entity, bool $workflowAllowed): void + 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); @@ -69,24 +93,31 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Mock the return of an AccompanyingCourseDocument by the repository $this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity); - // Mock attributeToRole to return appropriate role - $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); - // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument - $this->security->method('isGranted')->willReturnMap([ - [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], - ]); + $this->security->method('isGranted')->willReturn($isGrantedForEntity); // Mock case where user is blocked or not by workflow $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); } - public function testVoteOnAttributeAllowed(): void + 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)); @@ -104,7 +135,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } - public function testVoteOnAttributeWhenBlockedByWorkflow(): void + public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void { list($user, $token, $subject, $entity) = $this->setupMockObjects(); @@ -119,16 +150,4 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Assert that access is denied when workflow is not allowed $this->assertFalse($result); } - - public function testAbstractStoredObjectVoter(): void - { - $voter = new class extends AbstractStoredObjectVoter { - // Implement abstract methods here - public function someMethod() { - // method implementation - } - }; - // Run tests on $voter - } - } From 428494ca1fe46cde4be97ad238b64447b41cc7cd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:38:41 +0200 Subject: [PATCH 56/76] Implement stored object permissions in serialization --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index 4a0ba6dab..17597901d 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -57,13 +57,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa // deprecated property $datas['creationDate'] = $datas['createdAt']; - $canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true); - $canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true); + $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE, $object); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $object); - if ($canDavSee || $canDavEdit) { + if ($canSee || $canEdit) { $accessToken = $this->JWTDavTokenProvider->createToken( $object, - $canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE + $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE ); $datas['_links'] = [ From 21b79c198106aedf3a542d005c58acbda5a7764e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:39:02 +0200 Subject: [PATCH 57/76] Php cs fixes --- .../Repository/ActivityRepository.php | 2 -- .../ActivityStoredObjectVoter.php | 16 +++++++++------ .../AccompanyingCourseDocumentRepository.php | 1 - ...ssociatedEntityToStoredObjectInterface.php | 9 +++++++++ .../Authorization/AsyncUploadVoter.php | 3 +-- .../Authorization/StoredObjectVoter.php | 4 +--- .../StoredObjectVoterInterface.php | 6 ++---- .../AbstractStoredObjectVoter.php | 20 ++++++++++++------- ...panyingCourseDocumentStoredObjectVoter.php | 12 +++++++++-- .../AccompanyingCourseStoredObjectVoter.php | 11 +++++++++- ...gPeriodWorkEvaluationStoredObjectVoter.php | 15 +++++++++----- .../ActivityStoredObjectVoter.php | 15 +++++++++----- .../EventStoredObjectVoter.php | 14 +++++++++---- .../PersonDocumentStoredObjectVoter.php | 15 +++++++++----- .../PersonStoredObjectVoter.php | 14 +++++++++---- .../Service/WorkflowDocumentService.php | 17 +++++++++------- ...ompanyingCourseDocumentWorkflowHandler.php | 12 ++++------- .../Form/ChoiceLoader/EventChoiceLoader.php | 1 - .../Repository/EventRepository.php | 2 -- .../Authorization/EventStoredObjectVoter.php | 15 +++++++++----- .../Workflow/EntityWorkflowRepository.php | 1 - .../Workflow/EntityWorkflowManager.php | 2 +- ...nyingPeriodWorkEvaluationDocumentVoter.php | 1 - ...dWorkEvaluationDocumentWorkflowHandler.php | 1 + .../AccompanyingPeriodWorkWorkflowHandler.php | 1 + .../src/Service/Wopi/AuthorizationManager.php | 3 --- 26 files changed, 133 insertions(+), 80 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 4942eef16..ff4904f9e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,8 +17,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index 01ec68bd9..be3013a08 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -1,11 +1,18 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Activity::class; diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 744afd424..7033a53c9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -80,5 +80,4 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat { return AccompanyingCourseDocument::class; } - } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php index 5349251c5..81d230e67 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -1,5 +1,14 @@ storedObjectRepository->findOneBy(['filename' => $subject->object_name]); - return match($subject->method) { + return match ($subject->method) { 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), default => $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 57688e05b..c0851144a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,9 +24,7 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - - public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { - } + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) {} protected function supports($attribute, $subject): bool { diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 516722654..97d45eec3 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,13 +12,11 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -interface StoredObjectVoterInterface { - +interface StoredObjectVoterInterface +{ public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; - } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php index 659d7d28a..24def7ca9 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -1,14 +1,21 @@ getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; } @@ -52,7 +58,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface if ($this->canBeAssociatedWithWorkflow()) { if (null === $this->workflowDocumentService) { - throw new \LogicException("Provide a workflow document service"); + throw new \LogicException('Provide a workflow document service'); } return $this->workflowDocumentService->notBlockedByWorkflow($entity); diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php index 86e7bd9de..2665553e8 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return AccompanyingPeriodWork::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php index a36535005..4f897b36c 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -1,9 +1,17 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Activity::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php index 218ce1980..d258b3ae3 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Event::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php index a40d08849..5791d724b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return PersonDocument::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php index 6c3b0b807..4d8a1220f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return PersonDocument::class; diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index a279912a9..78c7b7b7d 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -1,18 +1,23 @@ entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if (null != $workflow) { if ($workflow->isFinal()) { return false; @@ -35,5 +39,4 @@ class WorkflowDocumentService return true; } - } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index d1a384ef3..74e51b19e 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -18,19 +18,15 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\TranslatorInterface; readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct( - private TranslatorInterface $translator, - private EntityWorkflowRepository $workflowRepository, + private TranslatorInterface $translator, + private EntityWorkflowRepository $workflowRepository, private AccompanyingCourseDocumentRepository $repository - ) { - } + ) {} public function getDeletionRoles(): array { @@ -127,7 +123,7 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl public function findByRelatedEntity(object $object): ?EntityWorkflow { - if(!$object instanceof AccompanyingCourseDocument) { + if (!$object instanceof AccompanyingCourseDocument) { return null; } diff --git a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php index 19829a369..3fe305ff7 100644 --- a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php +++ b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php @@ -13,7 +13,6 @@ namespace Chill\EventBundle\Form\ChoiceLoader; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; -use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 03ee366ac..89723b770 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,8 +16,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 9db6177c8..8dda94aad 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Event::class; diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 7d2e047d3..dba68ee41 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -112,7 +112,6 @@ class EntityWorkflowRepository implements ObjectRepository ->setParameter('entity_id', $relatedEntityId); return $query->getQuery()->getResult(); - } /** diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 3f2ff0439..26c31506f 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -43,7 +43,7 @@ class EntityWorkflowManager foreach ($this->handlers as $handler) { return $handler->findByRelatedEntity($object); } + return null; } - } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index ecf3cd802..97ca84a13 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fd3af0af..34f8db4a7 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -140,6 +140,7 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { return null; } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()); } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 81116b7b2..d40080e33 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -133,6 +133,7 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte if (!$object instanceof AccompanyingPeriodWork) { return null; } + 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 d773a8eb4..4c9cb4a2f 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -12,7 +12,6 @@ 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; @@ -70,7 +69,6 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori { $storedObject = $this->getRelatedStoredObject($document); if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { - return $this->isTokenValid($accessToken, $document, $request); } @@ -103,5 +101,4 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori return false; } - } From 2adc8b3bf6cdffad987d3c58a80d74f59bc7286a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:58:48 +0200 Subject: [PATCH 58/76] Fix construct of SignedUrlPost --- .../Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php | 1 + src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php | 3 ++- .../OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php | 1 + .../Tests/Controller/AsyncUploadControllerTest.php | 1 + .../Serializer/Normalizer/SignedUrlPostNormalizerTest.php | 2 ++ 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php index 06f660fbe..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, 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/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php index f9cc49fa6..bb83c698a 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php @@ -154,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/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php index da223cea6..4c7dc92d0 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, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php index ceb777f23..3bfa279e5 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 ); From af4db2218404704b76d453dc502e40b94488a169 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 13:58:10 +0200 Subject: [PATCH 59/76] php cs fixer and rector: add missing comma in AsyncUploadControllerTest --- .../Tests/Controller/AsyncUploadControllerTest.php | 2 +- .../Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php index 4c7dc92d0..6b49b2919 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php @@ -73,7 +73,7 @@ class AsyncUploadControllerTest extends TestCase return new SignedUrlPost( 'https://object.store.example', new \DateTimeImmutable('1 hour'), - 'abc' + 'abc', 150, 1, 1800, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php index 3bfa279e5..69d7958b6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php @@ -60,7 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase 'method' => 'POST', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', - 'object_name' => 'abc' + 'object_name' => 'abc', ], $actual ); From 435836c7d1ba38b853c151efe87f4f4b554d5514 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 15:46:18 +0200 Subject: [PATCH 60/76] Delete unused storedobject voter --- .../AccompanyingCourseStoredObjectVoter.php | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php deleted file mode 100644 index 040f4dc32..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} From 25e89571f7be08802c6e1aa4604dc5ce848b65ae Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 15:48:08 +0200 Subject: [PATCH 61/76] Change usage of match function in AsyncUploadVoter --- .../Security/Authorization/AsyncUploadVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index ccda803e4..25981db1e 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -43,7 +43,7 @@ final class AsyncUploadVoter extends Voter return match ($subject->method) { 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), - default => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + 'POST', 'HEAD' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') }; } } From 3b80d9a93b15d9981641ad7b80d7e301aeba1b6e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 16:24:53 +0200 Subject: [PATCH 62/76] Delete voters that are not in use anymore --- .../ActivityStoredObjectVoter.php | 55 ------------------- .../EventStoredObjectVoter.php | 55 ------------------- .../PersonStoredObjectVoter.php | 55 ------------------- 3 files changed, 165 deletions(-) delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php deleted file mode 100644 index 4f897b36c..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php deleted file mode 100644 index d258b3ae3..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php deleted file mode 100644 index 4d8a1220f..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} From 7dd5f542a6296687ba99e9d922bc68718ef7cc37 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 16:28:09 +0200 Subject: [PATCH 63/76] Fix serialization of SignedUrl An annotation was missing to include the object_name in the serialization. --- src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php index 1047e1344..ec0debca9 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php @@ -21,6 +21,7 @@ readonly class SignedUrl #[Serializer\Groups(['read'])] public string $url, public \DateTimeImmutable $expires, + #[Serializer\Groups(['read'])] public string $object_name, ) {} From f9122341d17ea2e300487ccffe84a79c3e07c9d2 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 13:30:58 +0200 Subject: [PATCH 64/76] Fix phpstan error in match() function --- .../Security/Authorization/AsyncUploadVoter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index 25981db1e..708df0467 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -34,16 +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; } $storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]); return match ($subject->method) { - 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), + 'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), - 'POST', 'HEAD' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') }; } } From facc4affed1bbc66fef0cdf6bdc99645bf353e88 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 14:57:14 +0200 Subject: [PATCH 65/76] Fix testNormalizerSignedUrl method fixed --- .../Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index 2029fac74..253eac2ca 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php @@ -49,6 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase 'method' => 'GET', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', + 'object_name' => 'object' ], $actual ); From cfa51cd6590f550206c3b810f52622f731cd920e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 15:43:22 +0200 Subject: [PATCH 66/76] php cs fixer --- .../Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index 253eac2ca..7f0c342bb 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php @@ -49,7 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase 'method' => 'GET', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', - 'object_name' => 'object' + 'object_name' => 'object', ], $actual ); From 215eba41b716e59849c52abfe73edb463574abba Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 11 Jul 2024 15:52:47 +0200 Subject: [PATCH 67/76] Fix unit test to accomodate changed constructor in StoredObjectNormalizer --- .../ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php | 5 +++++ 1 file changed, 5 insertions(+) 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() ), ], [ From e83307ca6d13dda2851d86c8e75d349bb5db1982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:17:56 +0200 Subject: [PATCH 68/76] Remove obsolete security checks in StoredObjectVoter This commit eliminates antiquated security checks in the StoredObjectVoter class. Specifically, it removes a chunk of commented out code that checked for certain attributes on the token and also the import for DavTokenAuthenticationEventSubscriber class which is no longer needed. This results in code cleanup and prevents future confusion. --- .../Security/Authorization/StoredObjectVoter.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index c0851144a..91e767af2 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -12,7 +12,6 @@ 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; @@ -35,19 +34,6 @@ 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; - } - - if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) { - return false; - } - */ - $attributeAsEnum = StoredObjectRoleEnum::from($attribute); // Loop through context-specific voters From 7d0f9175be562871b5d692ba8a6728e31fc819ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:18:28 +0200 Subject: [PATCH 69/76] Refactor StoredObjectVoterTest to improve testing logic The existing StoredObjectVoter test logic was reworked to utilize UsernamePasswordToken and Security mock objects instead of defining its own token. This change improves the testing for different scenarios such as unsupported attributes and cases where role voters cannot see the stored object. Also, the redundancy in the test case provider was removed, which leads to cleaner and more maintainable code. --- .../Authorization/StoredObjectVoterTest.php | 132 +++++++++--------- 1 file changed, 65 insertions(+), 67 deletions(-) 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, + ]; } } From 31f842471aa68d680d61a397b43f6ea612897b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:53:06 +0200 Subject: [PATCH 70/76] Refactor authorization for AccompanyingPeriodWorkEvaluationDocuments The AccompanyingPeriodWorkEvaluationStoredObjectVoter has been updated to use the AccompanyingPeriodWorkEvaluationDocument-related classes instead of the AccompanyingPeriodWork classes. Additionally, a new voters class, AccompanyingPeriodWorkEvaluationDocumentVoter has been created. Changes are also made in the repository to find the associated entity in the AccompanyingPeriodWorkEvaluationDocument repository instead of the AccompanyingPeriodWork repository. --- ...rkEvaluationDocumentStoredObjectVoter.php} | 16 +++++++------- ...PeriodWorkEvaluationDocumentRepository.php | 22 +++++++++++++++++-- .../AccompanyingPeriodWorkRepository.php | 17 +------------- ...nyingPeriodWorkEvaluationDocumentVoter.php | 8 ++++++- .../AccompanyingPeriodWorkEvaluationVoter.php | 4 ++++ 5 files changed, 40 insertions(+), 27 deletions(-) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{AccompanyingPeriodWorkEvaluationStoredObjectVoter.php => AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php} (74%) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php similarity index 74% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index 7ba7e276e..d9eb4a843 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,15 +14,15 @@ namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Service\WorkflowDocumentService; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; -use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; -use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; use Symfony\Component\Security\Core\Security; -class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredObjectVoter +class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends AbstractStoredObjectVoter { public function __construct( - private readonly AccompanyingPeriodWorkRepository $repository, + private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, WorkflowDocumentService $workflowDocumentService ) { @@ -36,14 +36,14 @@ class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredOb protected function getClass(): string { - return AccompanyingPeriodWork::class; + return AccompanyingPeriodWorkEvaluationDocument::class; } protected function attributeToRole(StoredObjectRoleEnum $attribute): string { return match ($attribute) { - StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkVoter::SEE, - StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkVoter::UPDATE, + StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE_AND_EDIT, }; } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..5da541cf8 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,14 +11,18 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; 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 +62,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/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 324c3c176..95b995e74 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -11,8 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; -use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -24,7 +22,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface +final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository { private EntityRepository $repository; @@ -253,17 +251,4 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor return $qb; } - - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWork - { - $qb = $this->repository->createQueryBuilder('acpw'); - $query = $qb - ->join('acpw.accompanyingPeriodWorkEvaluations', 'acpwe') - ->join('acpwe.documents', 'acpwed') - ->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 97ca84a13..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) {} public function supports($attribute, $subject): bool { return $subject instanceof AccompanyingPeriodWorkEvaluationDocument - && self::SEE === $attribute; + && (self::SEE === $attribute || self::SEE_AND_EDIT === $attribute); } /** @@ -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"), }; } From 9e92ede16f61211bc9a2d2a054ed6a212a8a9c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 18:58:08 +0200 Subject: [PATCH 71/76] Handle cases when there are multiple EntityWorkflows associated with one entity --- .../Service/WorkflowDocumentService.php | 8 ++------ .../AccompanyingCourseDocumentWorkflowHandler.php | 4 ++-- .../Workflow/EntityWorkflowRepository.php | 5 ++++- .../Workflow/EntityWorkflowHandlerInterface.php | 5 ++++- .../Workflow/EntityWorkflowManager.php | 11 ++++++++--- ...nyingPeriodWorkEvaluationDocumentRepository.php | 1 - ...PeriodWorkEvaluationDocumentWorkflowHandler.php | 4 ++-- ...mpanyingPeriodWorkEvaluationWorkflowHandler.php | 14 +++++++------- .../AccompanyingPeriodWorkWorkflowHandler.php | 4 ++-- 9 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 78c7b7b7d..3444653b1 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service; -use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; @@ -21,13 +20,10 @@ class WorkflowDocumentService public function notBlockedByWorkflow(object $entity): bool { - /** - * @var EntityWorkflow - */ - $workflow = $this->entityWorkflowManager->findByRelatedEntity($entity); + $workflows = $this->entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if (null != $workflow) { + foreach ($workflows as $workflow) { if ($workflow->isFinal()) { return false; } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 74e51b19e..59d2718c8 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -121,10 +121,10 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingCourseDocument) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId()); diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index dba68ee41..81cdcd551 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,7 +99,10 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } - public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + /** + * @return list + */ + public function findByRelatedEntity($entityClass, $relatedEntityId): array { $qb = $this->repository->createQueryBuilder('w'); diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index bff1057c8..ebe412587 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -52,5 +52,8 @@ interface EntityWorkflowHandlerInterface public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; - public function findByRelatedEntity(object $object): ?EntityWorkflow; + /** + * @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 26c31506f..a5b9b6a23 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -38,12 +38,17 @@ class EntityWorkflowManager return $this->registry->all($entityWorkflow); } - public function findByRelatedEntity(object $object): ?EntityWorkflow + /** + * @return list + */ + public function findByRelatedEntity(object $object): array { foreach ($this->handlers as $handler) { - return $handler->findByRelatedEntity($object); + if ([] !== $workflows = $handler->findByRelatedEntity($object)) { + return $workflows; + } } - return null; + return []; } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 5da541cf8..e3a484f0b 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -13,7 +13,6 @@ namespace Chill\PersonBundle\Repository\AccompanyingPeriod; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 34f8db4a7..cc59a509b 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -135,10 +135,10 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { - return null; + 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 e007f03f7..a9897f9bd 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -21,13 +21,13 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvalu use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface +readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface { public function __construct( - private readonly AccompanyingPeriodWorkEvaluationRepository $repository, - private readonly EntityWorkflowRepository $workflowRepository, - private readonly TranslatableStringHelperInterface $translatableStringHelper, - private readonly TranslatorInterface $translator + private AccompanyingPeriodWorkEvaluationRepository $repository, + private EntityWorkflowRepository $workflowRepository, + private TranslatableStringHelperInterface $translatableStringHelper, + private TranslatorInterface $translator ) {} public function getDeletionRoles(): array @@ -121,10 +121,10 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWorkEvaluation) { - return null; + 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 d40080e33..cb9462b9e 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -128,10 +128,10 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWork) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId()); From 747a1de32185be597784962d7637151d65c16ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 18:58:44 +0200 Subject: [PATCH 72/76] Add locale requirement to search route, to avoid conflict with profiler route Introduces a locale requirement to the search route in the ChillMainBundle. This update specifies that a valid locale should consist of 1 to 3 lowercase alphabetic characters. This change will help constrain acceptable locale values. --- src/Bundle/ChillMainBundle/Controller/SearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', '')); From ca68b5824637404d5cf9ee3e822d65280ff9b4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:09:29 +0200 Subject: [PATCH 73/76] Move classes to dedicated bundle, and avoid plural for namespace name --- .../Security/Authorization/ActivityStoredObjectVoter.php | 2 +- .../AbstractStoredObjectVoter.php | 2 +- .../AccompanyingCourseDocumentStoredObjectVoter.php | 2 +- .../PersonDocumentStoredObjectVoter.php | 2 +- .../Security/Authorization/AbstractStoredObjectVoterTest.php | 3 +-- .../Security/Authorization/EventStoredObjectVoter.php | 2 +- ...companyingPeriodWorkEvaluationDocumentStoredObjectVoter.php | 3 ++- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/AbstractStoredObjectVoter.php (99%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/AccompanyingCourseDocumentStoredObjectVoter.php (99%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/PersonDocumentStoredObjectVoter.php (99%) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillPersonBundle/Security/Authorization/StoredObjectVoter}/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php (91%) diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index be3013a08..f57df2a30 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -15,7 +15,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 24def7ca9..293473b7e 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index 2665553e8..75035474d 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index 5791d724b..e0d6108af 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\PersonDocument; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index 24370c2db..a545a6a2e 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -11,11 +11,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Tests\Security\Authorization; -use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 8dda94aad..eec707b8e 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -13,7 +13,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php similarity index 91% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php rename to src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index d9eb4a843..979edd4f6 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -9,10 +9,11 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; From d5e499198240a083f0fb81801d363fe31b1191e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:14:01 +0200 Subject: [PATCH 74/76] Rename WorkflowDocumentService to WorkflowStoredObjectPermissionHelper The previous name, WorkflowDocumentService, was misleading as its functionality extends to all stored objects and not limited to documents. Therefore, it was renamed to WorkflowStoredObjectPermissionHelper. Consequently, all references to this service were updated throughout the codebase. --- .../Authorization/ActivityStoredObjectVoter.php | 4 ++-- .../StoredObjectVoter/AbstractStoredObjectVoter.php | 4 ++-- .../AccompanyingCourseDocumentStoredObjectVoter.php | 4 ++-- .../PersonDocumentStoredObjectVoter.php | 4 ++-- ...ce.php => WorkflowStoredObjectPermissionHelper.php} | 2 +- .../Authorization/AbstractStoredObjectVoterTest.php | 10 +++++----- .../Security/Authorization/EventStoredObjectVoter.php | 4 ++-- ...ngPeriodWorkEvaluationDocumentStoredObjectVoter.php | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) rename src/Bundle/ChillDocStoreBundle/Service/{WorkflowDocumentService.php => WorkflowStoredObjectPermissionHelper.php} (95%) diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index f57df2a30..4be101c45 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; class ActivityStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly ActivityRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 293473b7e..2e76da318 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function __construct( private readonly Security $security, - private readonly ?WorkflowDocumentService $workflowDocumentService = null, + private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null, ) {} public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index 75035474d..0f7015b10 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index e0d6108af..577f362ef 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly PersonDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php similarity index 95% rename from src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php rename to src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php index 3444653b1..519eee8de 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php @@ -14,7 +14,7 @@ namespace Chill\DocStoreBundle\Service; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; -class WorkflowDocumentService +class WorkflowStoredObjectPermissionHelper { public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index a545a6a2e..4f420223a 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -30,16 +30,16 @@ class AbstractStoredObjectVoterTest extends TestCase { private AssociatedEntityToStoredObjectInterface $repository; private Security $security; - private WorkflowDocumentService $workflowDocumentService; + private WorkflowStoredObjectPermissionHelper $workflowDocumentService; protected function setUp(): void { $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); $this->security = $this->createMock(Security::class); - $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + $this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class); } - private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowDocumentService $workflowDocumentService = null): AbstractStoredObjectVoter + 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 { @@ -47,7 +47,7 @@ class AbstractStoredObjectVoterTest extends TestCase private bool $canBeAssociatedWithWorkflow, private AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowDocumentService $workflowDocumentService = null + ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index eec707b8e..66f54bc6d 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; use Symfony\Component\Security\Core\Security; @@ -24,7 +24,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly EventRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index 979edd4f6..4a81367d7 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; @@ -25,7 +25,7 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract public function __construct( private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } From d689ce9aef392ad4c6f973333096ddbd4d4651d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:14:17 +0200 Subject: [PATCH 75/76] Fix condition for checking if the user is allowed to edit a document attached to a workflow --- .../Service/WorkflowStoredObjectPermissionHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php index 519eee8de..b27b6d96a 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php @@ -28,8 +28,8 @@ class WorkflowStoredObjectPermissionHelper return false; } - if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { - return true; + if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return false; } } From 9f88eef2493c0cac48ffe29524c14db36aae83ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 21:25:22 +0200 Subject: [PATCH 76/76] Fix permission logic in StoredObjectNormalizer The logic for checking 'see' and 'edit' permissions within the StoredObjectNormalizer has been updated. It now correctly refers to the value of the StoredObjectRoleEnum to check access rights. --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index 17597901d..ebd7d3564 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -57,8 +57,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa // deprecated property $datas['creationDate'] = $datas['createdAt']; - $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE, $object); - $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $object); + $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object); if ($canSee || $canEdit) { $accessToken = $this->JWTDavTokenProvider->createToken(