From b8819cc3d763f3dee00ca94d8aa79d940488d5df Mon Sep 17 00:00:00 2001 From: Yassine Guedidi Date: Sat, 23 Mar 2024 15:34:02 +0100 Subject: [PATCH 1/2] Use IsGranted in EntryController --- src/Controller/EntryController.php | 58 ++++---- src/Security/Voter/EntryVoter.php | 58 ++++++++ src/Security/Voter/MainVoter.php | 46 +++++++ templates/Entry/Card/_content.html.twig | 8 +- templates/Entry/_card_actions.html.twig | 32 +++-- templates/Entry/_card_full_image.html.twig | 10 +- templates/Entry/_card_list.html.twig | 26 +++- templates/Entry/_card_preview.html.twig | 18 ++- templates/Entry/entries.html.twig | 4 +- templates/Entry/entry.html.twig | 146 ++++++++++++-------- templates/Static/howto.html.twig | 21 +-- templates/Static/quickstart.html.twig | 4 +- templates/Tag/tags.html.twig | 8 +- templates/layout.html.twig | 52 ++++--- tests/Security/Voter/EntryVoterTest.php | 149 +++++++++++++++++++++ tests/Security/Voter/MainVoterTest.php | 86 ++++++++++++ 16 files changed, 575 insertions(+), 151 deletions(-) create mode 100644 src/Security/Voter/EntryVoter.php create mode 100644 src/Security/Voter/MainVoter.php create mode 100644 tests/Security/Voter/EntryVoterTest.php create mode 100644 tests/Security/Voter/MainVoterTest.php diff --git a/src/Controller/EntryController.php b/src/Controller/EntryController.php index 4129b3b02..92563b77c 100644 --- a/src/Controller/EntryController.php +++ b/src/Controller/EntryController.php @@ -8,12 +8,14 @@ use Doctrine\ORM\NoResultException; use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Spiriit\Bundle\FormFilterBundle\Filter\FilterBuilderUpdaterInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; use Wallabag\Entity\Entry; use Wallabag\Entity\Tag; @@ -38,8 +40,9 @@ class EntryController extends AbstractController private PreparePagerForEntries $preparePagerForEntriesHelper; private FilterBuilderUpdaterInterface $filterBuilderUpdater; private ContentProxy $contentProxy; + private Security $security; - public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, EntryRepository $entryRepository, Redirect $redirectHelper, PreparePagerForEntries $preparePagerForEntriesHelper, FilterBuilderUpdaterInterface $filterBuilderUpdater, ContentProxy $contentProxy) + public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, EntryRepository $entryRepository, Redirect $redirectHelper, PreparePagerForEntries $preparePagerForEntriesHelper, FilterBuilderUpdaterInterface $filterBuilderUpdater, ContentProxy $contentProxy, Security $security) { $this->entityManager = $entityManager; $this->eventDispatcher = $eventDispatcher; @@ -48,10 +51,12 @@ class EntryController extends AbstractController $this->preparePagerForEntriesHelper = $preparePagerForEntriesHelper; $this->filterBuilderUpdater = $filterBuilderUpdater; $this->contentProxy = $contentProxy; + $this->security = $security; } /** * @Route("/mass", name="mass_action") + * @IsGranted("EDIT_ENTRIES") * * @return Response */ @@ -104,7 +109,9 @@ class EntryController extends AbstractController /** @var Entry * */ $entry = $this->entryRepository->findById((int) $id)[0]; - $this->checkUserAction($entry); + if (!$this->security->isGranted('EDIT', $entry)) { + throw $this->createAccessDeniedException('You can not access this entry.'); + } if ('toggle-read' === $action) { $entry->toggleArchive(); @@ -135,6 +142,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/search/{page}", name="search", defaults={"page" = 1}) + * @IsGranted("LIST_ENTRIES") * * Default parameter for page is hardcoded (in duplication of the defaults from the Route) * because this controller is also called inside the layout template without any page as argument @@ -164,6 +172,7 @@ class EntryController extends AbstractController /** * @Route("/new-entry", name="new_entry") + * @IsGranted("CREATE_ENTRIES") * * @return Response */ @@ -205,6 +214,7 @@ class EntryController extends AbstractController /** * @Route("/bookmarklet", name="bookmarklet") + * @IsGranted("CREATE_ENTRIES") * * @return Response */ @@ -228,6 +238,7 @@ class EntryController extends AbstractController /** * @Route("/new", name="new") + * @IsGranted("CREATE_ENTRIES") * * @return Response */ @@ -240,13 +251,12 @@ class EntryController extends AbstractController * Edit an entry content. * * @Route("/edit/{id}", requirements={"id" = "\d+"}, name="edit") + * @IsGranted("EDIT", subject="entry") * * @return Response */ public function editEntryAction(Request $request, Entry $entry) { - $this->checkUserAction($entry); - $form = $this->createForm(EditEntryType::class, $entry); $form->handleRequest($request); @@ -274,6 +284,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/all/list/{page}", name="all", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -288,6 +299,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/unread/list/{page}", name="unread", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -307,6 +319,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/archive/list/{page}", name="archive", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -321,6 +334,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/starred/list/{page}", name="starred", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -335,6 +349,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/untagged/list/{page}", name="untagged", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -349,6 +364,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/annotated/list/{page}", name="annotated", defaults={"page" = "1"}) + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -361,6 +377,7 @@ class EntryController extends AbstractController * Shows random entry depending on the given type. * * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|annotated|all"}) + * @IsGranted("LIST_ENTRIES") * * @return RedirectResponse */ @@ -382,13 +399,12 @@ class EntryController extends AbstractController * Shows entry content. * * @Route("/view/{id}", requirements={"id" = "\d+"}, name="view") + * @IsGranted("VIEW", subject="entry") * * @return Response */ public function viewAction(Entry $entry) { - $this->checkUserAction($entry); - return $this->render( 'Entry/entry.html.twig', ['entry' => $entry] @@ -400,13 +416,12 @@ class EntryController extends AbstractController * Refetch content from the website and make it readable again. * * @Route("/reload/{id}", requirements={"id" = "\d+"}, name="reload_entry") + * @IsGranted("RELOAD", subject="entry") * * @return RedirectResponse */ public function reloadAction(Entry $entry) { - $this->checkUserAction($entry); - $this->updateEntry($entry, 'entry_reloaded'); // if refreshing entry failed, don't save it @@ -429,13 +444,12 @@ class EntryController extends AbstractController * Changes read status for an entry. * * @Route("/archive/{id}", requirements={"id" = "\d+"}, name="archive_entry") + * @IsGranted("ARCHIVE", subject="entry") * * @return RedirectResponse */ public function toggleArchiveAction(Request $request, Entry $entry) { - $this->checkUserAction($entry); - $entry->toggleArchive(); $this->entityManager->flush(); @@ -458,13 +472,12 @@ class EntryController extends AbstractController * Changes starred status for an entry. * * @Route("/star/{id}", requirements={"id" = "\d+"}, name="star_entry") + * @IsGranted("STAR", subject="entry") * * @return RedirectResponse */ public function toggleStarAction(Request $request, Entry $entry) { - $this->checkUserAction($entry); - $entry->toggleStar(); $entry->updateStar($entry->isStarred()); $this->entityManager->flush(); @@ -488,13 +501,12 @@ class EntryController extends AbstractController * Deletes entry and redirect to the homepage or the last viewed page. * * @Route("/delete/{id}", requirements={"id" = "\d+"}, name="delete_entry") + * @IsGranted("DELETE", subject="entry") * * @return RedirectResponse */ public function deleteEntryAction(Request $request, Entry $entry) { - $this->checkUserAction($entry); - // generates the view url for this entry to check for redirection later // to avoid redirecting to the deleted entry. Ugh. $url = $this->generateUrl( @@ -526,13 +538,12 @@ class EntryController extends AbstractController * Get public URL for entry (and generate it if necessary). * * @Route("/share/{id}", requirements={"id" = "\d+"}, name="share") + * @IsGranted("SHARE", subject="entry") * * @return Response */ public function shareAction(Entry $entry) { - $this->checkUserAction($entry); - if (null === $entry->getUid()) { $entry->generateUid(); @@ -549,13 +560,12 @@ class EntryController extends AbstractController * Disable public sharing for an entry. * * @Route("/share/delete/{id}", requirements={"id" = "\d+"}, name="delete_share") + * @IsGranted("UNSHARE", subject="entry") * * @return Response */ public function deleteShareAction(Entry $entry) { - $this->checkUserAction($entry); - $entry->cleanUid(); $this->entityManager->persist($entry); @@ -571,6 +581,7 @@ class EntryController extends AbstractController * * @Route("/share/{uid}", requirements={"uid" = ".+"}, name="share_entry") * @Cache(maxage="25200", smaxage="25200", public=true) + * @IsGranted("PUBLIC_ACCESS") * * @return Response */ @@ -592,6 +603,7 @@ class EntryController extends AbstractController * @param int $page * * @Route("/domain/{id}/{page}", requirements={"id" = ".+"}, defaults={"page" = 1}, name="same_domain") + * @IsGranted("LIST_ENTRIES") * * @return Response */ @@ -713,16 +725,6 @@ class EntryController extends AbstractController $this->addFlash('notice', $message); } - /** - * Check if the logged user can manage the given entry. - */ - private function checkUserAction(Entry $entry) - { - if (null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) { - throw $this->createAccessDeniedException('You can not access this entry.'); - } - } - /** * Check for existing entry, if it exists, redirect to it with a message. * diff --git a/src/Security/Voter/EntryVoter.php b/src/Security/Voter/EntryVoter.php new file mode 100644 index 000000000..5a8f8a709 --- /dev/null +++ b/src/Security/Voter/EntryVoter.php @@ -0,0 +1,58 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::VIEW: + case self::EDIT: + case self::RELOAD: + case self::STAR: + case self::ARCHIVE: + case self::SHARE: + case self::UNSHARE: + case self::DELETE: + return $user === $subject->getUser(); + } + + return false; + } +} diff --git a/src/Security/Voter/MainVoter.php b/src/Security/Voter/MainVoter.php new file mode 100644 index 000000000..015bac4b3 --- /dev/null +++ b/src/Security/Voter/MainVoter.php @@ -0,0 +1,46 @@ +security = $security; + } + + protected function supports(string $attribute, $subject): bool + { + if (null !== $subject) { + return false; + } + + if (!\in_array($attribute, [self::LIST_ENTRIES, self::CREATE_ENTRIES, self::EDIT_ENTRIES], true)) { + return false; + } + + return true; + } + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + switch ($attribute) { + case self::LIST_ENTRIES: + case self::CREATE_ENTRIES: + case self::EDIT_ENTRIES: + return $this->security->isGranted('ROLE_USER'); + } + + return false; + } +} diff --git a/templates/Entry/Card/_content.html.twig b/templates/Entry/Card/_content.html.twig index 39bb8447c..2d3088368 100644 --- a/templates/Entry/Card/_content.html.twig +++ b/templates/Entry/Card/_content.html.twig @@ -2,9 +2,13 @@ {% if withPreview is defined %} more_vert {% endif %} - + {% if is_granted('VIEW', entry) %} + + {{ entry.title|striptags|u.truncate(80, '…', false)|default('entry.default_title'|trans)|raw }} + + {% else %} {{ entry.title|striptags|u.truncate(80, '…', false)|default('entry.default_title'|trans)|raw }} - + {% endif %}
{{ entry.domainName|removeWww }} diff --git a/templates/Entry/_card_actions.html.twig b/templates/Entry/_card_actions.html.twig index 88c5029e3..f3f18ba95 100644 --- a/templates/Entry/_card_actions.html.twig +++ b/templates/Entry/_card_actions.html.twig @@ -10,17 +10,25 @@ {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
diff --git a/templates/Entry/_card_full_image.html.twig b/templates/Entry/_card_full_image.html.twig index e4d79ed30..c09beee6e 100644 --- a/templates/Entry/_card_full_image.html.twig +++ b/templates/Entry/_card_full_image.html.twig @@ -7,9 +7,13 @@ {% endfor %} {% if app.user.config.displayThumbnails %} - - - + {% if is_granted('VIEW', entry) %} + + + + {% else %} + + {% endif %} {% endif %} {% include "Entry/Card/_content.html.twig" with {'entry': entry} only %} diff --git a/templates/Entry/_card_list.html.twig b/templates/Entry/_card_list.html.twig index 99b89c7b8..6aaabbd2d 100644 --- a/templates/Entry/_card_list.html.twig +++ b/templates/Entry/_card_list.html.twig @@ -2,10 +2,14 @@ {% include "Entry/Card/_mass_checkbox.html.twig" with {'entry': entry} only %} {% if app.user.config.displayThumbnails %}
- - {% set preview_class_modifier = entry.previewPicture ? '' : ' preview--default' %} + {% set preview_class_modifier = entry.previewPicture ? '' : ' preview--default' %} + {% if is_granted('VIEW', entry) %} + + + + {% else %} - + {% endif %}
{% endif %} {% include "Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %} @@ -14,10 +18,18 @@ diff --git a/templates/Entry/_card_preview.html.twig b/templates/Entry/_card_preview.html.twig index 2ea6c6eba..0a1cf4f2e 100644 --- a/templates/Entry/_card_preview.html.twig +++ b/templates/Entry/_card_preview.html.twig @@ -8,10 +8,14 @@ {% endfor %} {% if app.user.config.displayThumbnails %} - {% set preview_class_modifier = entry.previewPicture ? '' : ' preview--default' %} - - + {% if is_granted('VIEW', entry) %} + + + + {% else %} + + {% endif %} {% endif %} {% include "Entry/Card/_content.html.twig" with {'entry': entry, 'withPreview': true} only %} @@ -20,9 +24,13 @@
clear - + {% if is_granted('VIEW', entry) %} + + {{ entry.title|striptags|u.truncate(80, '…', false)|raw }} + + {% else %} {{ entry.title|striptags|u.truncate(80, '…', false)|raw }} - + {% endif %}

{{ entry.content|striptags|slice(0, 250)|raw }}…

diff --git a/templates/Entry/entries.html.twig b/templates/Entry/entries.html.twig index bb63a8d4f..2e8b8ebce 100644 --- a/templates/Entry/entries.html.twig +++ b/templates/Entry/entries.html.twig @@ -33,7 +33,7 @@ {% if entries.count > 0 %} {% if list_mode == 0 %}view_list{% else %}view_module{% endif %} {% endif %} - {% if entries.count > 0 %} + {% if entries.count > 0 and is_granted('EDIT_ENTRIES') %} {% endif %} {% if app.user.config.feedToken %} @@ -109,7 +109,7 @@
- {% if form is not null %} + {% if form is not null and is_granted('LIST_ENTRIES') %}
diff --git a/templates/Entry/entry.html.twig b/templates/Entry/entry.html.twig index 2a53016cb..79714095b 100644 --- a/templates/Entry/entry.html.twig +++ b/templates/Entry/entry.html.twig @@ -25,16 +25,20 @@
@@ -55,41 +59,50 @@
-
  • - - refresh - {{ 'entry.view.left_menu.re_fetch_content'|trans }} - -
    -
  • + {% if is_granted('RELOAD', entry) %} +
  • + + refresh + {{ 'entry.view.left_menu.re_fetch_content'|trans }} + +
    +
  • + {% endif %} {% set mark_as_read_label = 'entry.view.left_menu.set_as_unread' %} {% if entry.isArchived == 0 %} {% set mark_as_read_label = 'entry.view.left_menu.set_as_read' %} {% endif %} -
  • - - {% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %} - {{ mark_as_read_label|trans }} - -
    -
  • + {% if is_granted('ARCHIVE', entry) %} +
  • + + {% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %} + {{ mark_as_read_label|trans }} + +
    +
  • + {% endif %} -
  • - - {% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %} - {{ 'entry.view.left_menu.set_as_starred'|trans }} - -
    -
  • -
  • - - delete - {{ 'entry.view.left_menu.delete'|trans }} - -
    -
  • + {% if is_granted('STAR', entry) %} +
  • + + {% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %} + {{ 'entry.view.left_menu.set_as_starred'|trans }} + +
    +
  • + {% endif %} + + {% if is_granted('DELETE', entry) %} +
  • + + delete + {{ 'entry.view.left_menu.delete'|trans }} + +
    +
  • + {% endif %}
  • @@ -134,16 +147,20 @@
    -
    -
    {{ 'howto.top_menu.bookmarklet'|trans }}
    - {{ 'howto.bookmarklet.description'|trans }} - {% include 'Static/_bookmarklet.html.twig' %} -
    - + {% if is_granted('CREATE_ENTRIES') %} +
    +
    {{ 'howto.top_menu.bookmarklet'|trans }}
    + {{ 'howto.bookmarklet.description'|trans }} + {% include 'Static/_bookmarklet.html.twig' %} +
    + {% endif %} diff --git a/templates/Static/quickstart.html.twig b/templates/Static/quickstart.html.twig index bc8f4c402..bf5f2e047 100644 --- a/templates/Static/quickstart.html.twig +++ b/templates/Static/quickstart.html.twig @@ -37,7 +37,9 @@ diff --git a/templates/Tag/tags.html.twig b/templates/Tag/tags.html.twig index f1003e311..fb3b3ed2d 100644 --- a/templates/Tag/tags.html.twig +++ b/templates/Tag/tags.html.twig @@ -11,9 +11,11 @@
    - {{ render(controller('Wallabag\\Controller\\EntryController::searchFormAction', {'currentRoute': current_route})) }} - {{ render(controller('Wallabag\\Controller\\EntryController::addEntryFormAction')) }} + {% if is_granted('LIST_ENTRIES') %} + {{ render(controller('Wallabag\\Controller\\EntryController::searchFormAction', {'currentRoute': current_route})) }} + {% endif %} + {% if is_granted('CREATE_ENTRIES') %} + {{ render(controller('Wallabag\\Controller\\EntryController::addEntryFormAction')) }} + {% endif %} {% endblock %} diff --git a/tests/Security/Voter/EntryVoterTest.php b/tests/Security/Voter/EntryVoterTest.php new file mode 100644 index 000000000..949858511 --- /dev/null +++ b/tests/Security/Voter/EntryVoterTest.php @@ -0,0 +1,149 @@ +token = $this->createMock(TokenInterface::class); + $this->user = new User(); + $this->entry = new Entry($this->user); + + $this->entryVoter = new EntryVoter(); + } + + public function testVoteReturnsAbstainForInvalidSubject(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->entryVoter->vote($this->token, new \stdClass(), [EntryVoter::EDIT])); + } + + public function testVoteReturnsAbstainForInvalidAttribute(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->entryVoter->vote($this->token, $this->entry, ['INVALID'])); + } + + public function testVoteReturnsDeniedForNonEntryUserView(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::VIEW])); + } + + public function testVoteReturnsGrantedForEntryUserView(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::VIEW])); + } + + public function testVoteReturnsDeniedForNonEntryUserEdit(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::EDIT])); + } + + public function testVoteReturnsGrantedForEntryUserEdit(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::EDIT])); + } + + public function testVoteReturnsDeniedForNonEntryUserReload(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::RELOAD])); + } + + public function testVoteReturnsGrantedForEntryUserReload(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::RELOAD])); + } + + public function testVoteReturnsDeniedForNonEntryUserStar(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::STAR])); + } + + public function testVoteReturnsGrantedForEntryUserStar(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::STAR])); + } + + public function testVoteReturnsDeniedForNonEntryUserArchive(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::ARCHIVE])); + } + + public function testVoteReturnsGrantedForEntryUserArchive(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::ARCHIVE])); + } + + public function testVoteReturnsDeniedForNonEntryUserShare(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::SHARE])); + } + + public function testVoteReturnsGrantedForEntryUserShare(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::SHARE])); + } + + public function testVoteReturnsDeniedForNonEntryUserUnshare(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::UNSHARE])); + } + + public function testVoteReturnsGrantedForEntryUserUnshare(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::UNSHARE])); + } + + public function testVoteReturnsDeniedForNonEntryUserDelete(): void + { + $this->token->method('getUser')->willReturn(new User()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::DELETE])); + } + + public function testVoteReturnsGrantedForEntryUserDelete(): void + { + $this->token->method('getUser')->willReturn($this->user); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->entryVoter->vote($this->token, $this->entry, [EntryVoter::DELETE])); + } +} diff --git a/tests/Security/Voter/MainVoterTest.php b/tests/Security/Voter/MainVoterTest.php new file mode 100644 index 000000000..ba7b6a5a7 --- /dev/null +++ b/tests/Security/Voter/MainVoterTest.php @@ -0,0 +1,86 @@ +security = $this->createMock(Security::class); + + $this->token = $this->createMock(TokenInterface::class); + $this->token->method('getUser')->willReturn(new User()); + + $this->mainVoter = new MainVoter($this->security); + } + + public function testVoteReturnsAbstainForInvalidAttribute(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->mainVoter->vote($this->token, null, ['INVALID'])); + } + + public function testVoteReturnsAbstainForInvalidSubject(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->mainVoter->vote($this->token, new \stdClass(), [MainVoter::LIST_ENTRIES])); + } + + public function testVoteReturnsDeniedForInvalidUser(): void + { + $this->token->method('getUser')->willReturn(new \stdClass()); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->mainVoter->vote($this->token, null, [MainVoter::LIST_ENTRIES])); + } + + public function testVoteReturnsDeniedForNonUserListEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(false); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->mainVoter->vote($this->token, null, [MainVoter::LIST_ENTRIES])); + } + + public function testVoteReturnsGrantedForUserListEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(true); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->mainVoter->vote($this->token, null, [MainVoter::LIST_ENTRIES])); + } + + public function testVoteReturnsDeniedForNonUserCreateEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(false); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->mainVoter->vote($this->token, null, [MainVoter::CREATE_ENTRIES])); + } + + public function testVoteReturnsGrantedForUserCreateEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(true); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->mainVoter->vote($this->token, null, [MainVoter::CREATE_ENTRIES])); + } + + public function testVoteReturnsDeniedForNonUserEditEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(false); + + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->mainVoter->vote($this->token, null, [MainVoter::EDIT_ENTRIES])); + } + + public function testVoteReturnsGrantedForUserEditEntries(): void + { + $this->security->method('isGranted')->with('ROLE_USER')->willReturn(true); + + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->mainVoter->vote($this->token, null, [MainVoter::EDIT_ENTRIES])); + } +} From e66ea216cee2086f60761d87f0f69c6dffd5007b Mon Sep 17 00:00:00 2001 From: Yassine Guedidi Date: Sat, 23 Mar 2024 16:07:43 +0100 Subject: [PATCH 2/2] Fix CSS class name --- templates/Entry/entry.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Entry/entry.html.twig b/templates/Entry/entry.html.twig index 79714095b..f23a32844 100644 --- a/templates/Entry/entry.html.twig +++ b/templates/Entry/entry.html.twig @@ -87,7 +87,7 @@ {% if is_granted('STAR', entry) %}
  • - {% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %} + {% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %} {{ 'entry.view.left_menu.set_as_starred'|trans }}