From b984e96a61f9f0f1564180f81626a79b8e91ca04 Mon Sep 17 00:00:00 2001
From: hvckxm <barsiklegend@gmail.com>
Date: Sun, 28 Aug 2022 09:49:57 +0400
Subject: [PATCH] Add security voters

---
 src/Controller/GroupController.php  |  11 ++
 src/Controller/LessonController.php |   7 ++
 src/Controller/MarkController.php   |   7 ++
 src/Controller/UserController.php   |  16 ++-
 src/Entity/User.php                 |  11 ++
 src/Security/GroupVoter.php         | 125 +++++++++++++++++++++
 src/Security/LessonVoter.php        |  98 +++++++++++++++++
 src/Security/MarkVoter.php          | 102 +++++++++++++++++
 src/Security/UserVoter.php          | 164 ++++++++++++++++++++++++++++
 9 files changed, 538 insertions(+), 3 deletions(-)
 create mode 100644 src/Security/GroupVoter.php
 create mode 100644 src/Security/LessonVoter.php
 create mode 100644 src/Security/MarkVoter.php
 create mode 100644 src/Security/UserVoter.php

diff --git a/src/Controller/GroupController.php b/src/Controller/GroupController.php
index 59717e2..6a8e184 100644
--- a/src/Controller/GroupController.php
+++ b/src/Controller/GroupController.php
@@ -35,6 +35,9 @@ class GroupController extends AbstractController
     public function new(Request $request, GroupRepository $groupRepository): Response
     {
         $group = new Group();
+
+        $this->denyAccessUnlessGranted('create', $group);
+
         $form = $this->createForm(GroupType::class, $group);
         $form->handleRequest($request);
 
@@ -53,6 +56,8 @@ class GroupController extends AbstractController
     #[Route('/{id}', name: 'app_group_show', methods: ['GET'])]
     public function show(Group $group, GroupService $groupService): Response
     {
+        $this->denyAccessUnlessGranted('view', $group);
+
         return $this->render('group/show.html.twig', [
             'group' => $group,
         ]);
@@ -61,6 +66,8 @@ class GroupController extends AbstractController
     #[Route('/{id}/edit', name: 'app_group_edit', methods: ['GET', 'POST'])]
     public function edit(Request $request, Group $group, GroupRepository $groupRepository): Response
     {
+        $this->denyAccessUnlessGranted('edit', $group);
+
         $form = $this->createForm(GroupType::class, $group);
         $form->handleRequest($request);
 
@@ -79,6 +86,8 @@ class GroupController extends AbstractController
     #[Route('/{id}', name: 'app_group_delete', methods: ['POST'])]
     public function delete(Request $request, Group $group, GroupRepository $groupRepository): Response
     {
+        $this->denyAccessUnlessGranted('delete', $group);
+
         if ($this->isCsrfTokenValid('delete' . $group->getId(), $request->request->get('_token'))) {
             $groupRepository->remove($group, true);
         }
@@ -95,6 +104,8 @@ class GroupController extends AbstractController
         LessonService $lessonService,
         PaginatorService $paginatorService): Response
     {
+        $this->denyAccessUnlessGranted('view.journal', $group);
+
         $offset = max(0, $request->query->getInt('offset'));
         $paginator = $lessonRepository->paginate($offset);
 
diff --git a/src/Controller/LessonController.php b/src/Controller/LessonController.php
index 8a3c460..f475c93 100644
--- a/src/Controller/LessonController.php
+++ b/src/Controller/LessonController.php
@@ -32,6 +32,9 @@ class LessonController extends AbstractController
     public function new(Request $request, LessonRepository $lessonRepository): Response
     {
         $lesson = new Lesson();
+
+        $this->denyAccessUnlessGranted('create', $lesson);
+
         $form = $this->createForm(LessonType::class, $lesson);
         $form->handleRequest($request);
 
@@ -58,6 +61,8 @@ class LessonController extends AbstractController
     #[Route('/{id}/edit', name: 'app_lesson_edit', methods: ['GET', 'POST'])]
     public function edit(Request $request, Lesson $lesson, LessonRepository $lessonRepository): Response
     {
+        $this->denyAccessUnlessGranted('edit', $lesson);
+
         $form = $this->createForm(LessonType::class, $lesson);
         $form->handleRequest($request);
 
@@ -76,6 +81,8 @@ class LessonController extends AbstractController
     #[Route('/{id}', name: 'app_lesson_delete', methods: ['POST'])]
     public function delete(Request $request, Lesson $lesson, LessonRepository $lessonRepository): Response
     {
+        $this->denyAccessUnlessGranted('delete', $lesson);
+
         if ($this->isCsrfTokenValid('delete'.$lesson->getId(), $request->request->get('_token'))) {
             $lessonRepository->remove($lesson, true);
         }
diff --git a/src/Controller/MarkController.php b/src/Controller/MarkController.php
index 0e3dac2..9879229 100644
--- a/src/Controller/MarkController.php
+++ b/src/Controller/MarkController.php
@@ -27,6 +27,9 @@ class MarkController extends AbstractController
     public function new(Request $request, MarkRepository $markRepository, User $user): Response
     {
         $mark = new Mark();
+
+        $this->denyAccessUnlessGranted('create', $mark);
+
         $form = $this->createForm(MarkType::class, $mark);
         $form->handleRequest($request);
 
@@ -59,6 +62,8 @@ class MarkController extends AbstractController
     #[Route('/{id}/edit', name: 'app_mark_edit', methods: ['GET', 'POST'])]
     public function edit(Request $request, Mark $mark, MarkRepository $markRepository, User $user): Response
     {
+        $this->denyAccessUnlessGranted('edit', $mark);
+
         $form = $this->createForm(MarkType::class, $mark);
         $form->handleRequest($request);
 
@@ -81,6 +86,8 @@ class MarkController extends AbstractController
     #[Route('/{id}', name: 'app_mark_delete', methods: ['POST'])]
     public function delete(Request $request, Mark $mark, MarkRepository $markRepository, User $user): Response
     {
+        $this->denyAccessUnlessGranted('delete', $mark);
+
         if ($this->isCsrfTokenValid('delete' . $mark->getId(), $request->request->get('_token'))) {
             $markRepository->remove($mark, true);
         }
diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
index df3035d..05f963c 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -9,11 +9,11 @@ use App\Form\UserType;
 use App\Repository\UserRepository;
 use App\Services\PaginatorService;
 use App\Services\UserService;
+use Exception;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 use Symfony\Component\Routing\Annotation\Route;
 
 #[Route('/group/{group}/user')]
@@ -35,13 +35,15 @@ class UserController extends AbstractController
     }
 
     /**
-     * @throws TransportExceptionInterface
-     * @throws \Exception
+     * @throws Exception
      */
     #[Route('/new', name: 'app_user_new', methods: ['GET', 'POST'])]
     public function new(Request $request, Group $group, UserRepository $userRepository, UserService $userService, EventDispatcherInterface $eventDispatcher): Response
     {
         $user = new User();
+
+        $this->denyAccessUnlessGranted('create', $user);
+
         $form = $this->createForm(UserType::class, $user);
         $form->handleRequest($request);
 
@@ -50,6 +52,8 @@ class UserController extends AbstractController
 
             $userService->setUserPassword($user);
 
+            $this->denyAccessUnlessGranted('store', $user);
+
             $userRepository->add($user, true);
 
             $eventDispatcher->dispatch(new CreateUserEvent($user));
@@ -76,10 +80,14 @@ class UserController extends AbstractController
     #[Route('/{id}/edit', name: 'app_user_edit', methods: ['GET', 'POST'])]
     public function edit(Request $request, Group $group, User $user, UserRepository $userRepository): Response
     {
+        $this->denyAccessUnlessGranted('edit', $user);
+
         $form = $this->createForm(UserType::class, $user);
         $form->handleRequest($request);
 
         if ($form->isSubmitted() && $form->isValid()) {
+            $this->denyAccessUnlessGranted('update', $user);
+
             $userRepository->add($user, true);
 
             return $this->redirectToRoute('app_user_index', ['group' => $group->getId()], Response::HTTP_SEE_OTHER);
@@ -95,6 +103,8 @@ class UserController extends AbstractController
     #[Route('/{id}', name: 'app_user_delete', methods: ['POST'])]
     public function delete(Request $request, Group $group, User $user, UserRepository $userRepository): Response
     {
+        $this->denyAccessUnlessGranted('delete', $user);
+
         if ($this->isCsrfTokenValid('delete' . $user->getId(), $request->request->get('_token'))) {
             $userRepository->remove($user, true);
         }
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 34ef8af..37b9abb 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -4,6 +4,7 @@ namespace App\Entity;
 
 use App\Enums\UserColors;
 use App\Enums\UserMarks;
+use App\Enums\UserRoles;
 use App\Repository\UserRepository;
 use DateTimeInterface;
 use Doctrine\Common\Collections\ArrayCollection;
@@ -286,4 +287,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
         return $this;
     }
+
+    public function isAdmin(): bool
+    {
+        return in_array(UserRoles::ROLE_ADMIN->value, $this->getRoles(), true);
+    }
+
+    public function isTeacher(): bool
+    {
+        return in_array(UserRoles::ROLE_TEACHER->value, $this->getRoles(), true);
+    }
 }
diff --git a/src/Security/GroupVoter.php b/src/Security/GroupVoter.php
new file mode 100644
index 0000000..acbb620
--- /dev/null
+++ b/src/Security/GroupVoter.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace App\Security;
+
+use App\Entity\Group;
+use App\Entity\User;
+use App\Enums\UserRoles;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Security;
+
+class GroupVoter extends Voter
+{
+    private const CREATE = 'create';
+    private const EDIT = 'edit';
+    private const DELETE = 'delete';
+    private const VIEW = 'view';
+    private const VIEW_JOURNAL = 'view.journal';
+
+    private Security $security;
+
+    public function __construct(Security $security)
+    {
+        $this->security = $security;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function supports(string $attribute, mixed $subject): bool
+    {
+        if (!in_array($attribute, [self::CREATE, self::EDIT,  self::DELETE, self::VIEW, self::VIEW_JOURNAL])) {
+            return false;
+        }
+
+        if (!$subject instanceof Group) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+    {
+        $user = $token->getUser();
+
+        if (!$user instanceof User) {
+            return false;
+        }
+
+        /** @var Group $group */
+        $group = $subject;
+
+        switch ($attribute) {
+            case self::CREATE:
+                return $this->canCreate();
+            case self::EDIT:
+                return $this->canEdit($group, $user);
+            case self::DELETE:
+                return $this->canDelete();
+            case self::VIEW:
+                return $this->canView($group, $user);
+            case self::VIEW_JOURNAL:
+                return $this->canViewJournal($group, $user);
+        }
+
+        throw new \LogicException('This code should not be reached!');
+    }
+
+    private function canCreate(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canEdit(Group $group, User $user): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return $group === $user->getEducationGroup();
+        }
+
+        return false;
+    }
+
+    private function canView(Group $group, User $user): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)
+            || $this->security->isGranted(UserRoles::ROLE_TEACHER->value)
+        ) {
+            return true;
+        }
+
+        if ($group === $user->getEducationGroup()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canViewJournal(Group $group, User $user): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)
+            || $this->security->isGranted(UserRoles::ROLE_TEACHER->value)
+        ) {
+            return true;
+        }
+
+        if ($group === $user->getEducationGroup()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canDelete(): bool
+    {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/Security/LessonVoter.php b/src/Security/LessonVoter.php
new file mode 100644
index 0000000..b2f6203
--- /dev/null
+++ b/src/Security/LessonVoter.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace App\Security;
+
+use App\Entity\Lesson;
+use App\Entity\User;
+use App\Enums\UserRoles;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Security;
+
+class LessonVoter extends Voter
+{
+    private const CREATE = 'create';
+    private const EDIT = 'edit';
+    private const DELETE = 'delete';
+
+    private Security $security;
+
+    public function __construct(Security $security)
+    {
+        $this->security = $security;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function supports(string $attribute, mixed $subject): bool
+    {
+        if (!in_array($attribute, [self::CREATE, self::EDIT,  self::DELETE])) {
+            return false;
+        }
+
+        if (!$subject instanceof Lesson) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+    {
+        $user = $token->getUser();
+
+        if (!$user instanceof User) {
+            return false;
+        }
+
+        switch ($attribute) {
+            case self::CREATE:
+                return $this->canCreate();
+            case self::EDIT:
+                return $this->canEdit();
+            case self::DELETE:
+                return $this->canDelete();
+        }
+
+        throw new \LogicException('This code should not be reached!');
+    }
+
+    private function canCreate(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canEdit(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canDelete(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/Security/MarkVoter.php b/src/Security/MarkVoter.php
new file mode 100644
index 0000000..9b72e15
--- /dev/null
+++ b/src/Security/MarkVoter.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Security;
+
+use App\Entity\Mark;
+use App\Entity\User;
+use App\Enums\UserRoles;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Security;
+
+class MarkVoter extends Voter
+{
+    private const CREATE = 'create';
+    private const EDIT = 'edit';
+    private const DELETE = 'delete';
+
+    private Security $security;
+
+    public function __construct(Security $security)
+    {
+        $this->security = $security;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function supports(string $attribute, mixed $subject): bool
+    {
+        if (!in_array($attribute, [self::CREATE, self::EDIT,  self::DELETE])) {
+            return false;
+        }
+
+        if (!$subject instanceof Mark) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+    {
+        $user = $token->getUser();
+
+        if (!$user instanceof User) {
+            return false;
+        }
+
+        switch ($attribute) {
+            case self::CREATE:
+                return $this->canCreate();
+            case self::EDIT:
+                return $this->canEdit();
+            case self::DELETE:
+                return $this->canDelete();
+        }
+
+        throw new \LogicException('This code should not be reached!');
+    }
+
+    private function canCreate(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canEdit(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canDelete(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)) {
+            return true;
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/src/Security/UserVoter.php b/src/Security/UserVoter.php
new file mode 100644
index 0000000..6bf571d
--- /dev/null
+++ b/src/Security/UserVoter.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Security;
+
+use App\Entity\User;
+use App\Enums\UserRoles;
+use App\Repository\UserRepository;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Security;
+
+class UserVoter extends Voter
+{
+    private const CREATE = 'create';
+    private const STORE = 'store';
+    private const EDIT = 'edit';
+    private const UPDATE = 'update';
+    private const DELETE = 'delete';
+
+    private Security $security;
+    private UserRepository $userRepository;
+
+    public function __construct(Security $security, UserRepository $userRepository)
+    {
+        $this->security = $security;
+        $this->userRepository = $userRepository;
+    }
+
+    /**
+     * @param string $attribute
+     * @param mixed $subject
+     * @return bool
+     */
+    protected function supports(string $attribute, mixed $subject): bool
+    {
+        if (!in_array($attribute, [self::CREATE, self::EDIT, self::UPDATE, self::STORE, self::DELETE])) {
+            return false;
+        }
+
+        if (!$subject instanceof User) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @param string $attribute
+     * @param mixed $subject
+     * @param TokenInterface $token
+     * @return bool
+     */
+    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
+    {
+        $auth = $token->getUser();
+
+        if (!$auth instanceof User) {
+            return false;
+        }
+
+        /** @var User $user */
+        $user = $subject;
+
+        switch ($attribute) {
+            case self::CREATE:
+                return $this->canCreate();
+            case self::STORE:
+                return $this->canStore($user, $auth);
+            case self::EDIT:
+                return $this->canEdit($user, $auth);
+            case self::UPDATE:
+                return $this->canUpdate($user, $auth);
+            case self::DELETE:
+                return $this->canDelete($user);
+        }
+
+        throw new \LogicException('This code should not be reached!');
+    }
+
+    private function canCreate(): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canStore(User $user, User $auth): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)
+            && $user->getEducationGroup() === $auth->getEducationGroup()
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canEdit(User $user, User $auth): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN)) {
+            return true;
+        }
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER)
+            && $user->getEducationGroup() === $auth->getEducationGroup()
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function canUpdate(User $user, User $auth): bool
+    {
+        $originalUser = $this->userRepository->findOneBy(['id' => $user->getId()]);
+
+        if (!$originalUser) {
+            return false;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            if ($originalUser === $user && $user->getRoles() !== $originalUser->getRoles()) {
+                return false;
+            }
+
+            if ($originalUser !== $user && $user->isAdmin()) {
+                return false;
+            }
+
+            return true;
+        }
+
+        if ($this->security->isGranted(UserRoles::ROLE_TEACHER->value)
+            && $auth->getEducationGroup() === $user->getEducationGroup()
+        ) {
+            return $user->getRoles() === $originalUser->getRoles();
+        }
+
+
+        return false;
+    }
+
+    public function canDelete(User $user): bool
+    {
+        if ($this->security->isGranted(UserRoles::ROLE_ADMIN->value)) {
+            if ($user->isAdmin()) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
-- 
GitLab