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