From 6931302c28761c7801a89f1a87b7831486a2e483 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sat, 6 Jan 2018 19:45:49 +0100 Subject: [PATCH] added voter for customer, project and activity #34 (#39) --- app/config/services.yml | 81 ++++++++++--- .../Controller/Admin/UserController.php | 2 + src/AppBundle/Voter/UserVoter.php | 18 ++- .../Controller/Admin/ActivityController.php | 1 + .../Controller/Admin/CustomerController.php | 1 + .../Controller/Admin/ProjectController.php | 1 + src/TimesheetBundle/Voter/ActivityVoter.php | 110 ++++++++++++++++++ src/TimesheetBundle/Voter/CustomerVoter.php | 110 ++++++++++++++++++ src/TimesheetBundle/Voter/ProjectVoter.php | 110 ++++++++++++++++++ 9 files changed, 414 insertions(+), 20 deletions(-) create mode 100644 src/TimesheetBundle/Voter/ActivityVoter.php create mode 100644 src/TimesheetBundle/Voter/CustomerVoter.php create mode 100644 src/TimesheetBundle/Voter/ProjectVoter.php diff --git a/app/config/services.yml b/app/config/services.yml index 681ca506..d35405f6 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -3,7 +3,10 @@ services: markdown: class: AppBundle\Utils\Markdown - # Twig extensions that create new filters and functions for our views +# ================================================================================ +# TWIG +# ================================================================================ + app.twig.app_extension: public: false class: AppBundle\Twig\Extensions @@ -23,12 +26,9 @@ services: tags: - { name: twig.extension } - # a route listener, that injects the locale through a URL directory - app.redirect_to_preferred_locale_listener: - class: AppBundle\EventListener\RedirectToPreferredLocaleListener - arguments: ['@router', '%app_locales%', '%locale%'] - tags: - - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } +# ================================================================================ +# SECURITY +# ================================================================================ # security voter to check user-profile access app.voter.user: @@ -37,6 +37,31 @@ services: tags: - { name: security.voter } + # security voter to check customer access + timesheet.voter.customer: + class: TimesheetBundle\Voter\CustomerVoter + arguments: ["@security.access.decision_manager"] + tags: + - { name: security.voter } + + # security voter to check project access + timesheet.voter.project: + class: TimesheetBundle\Voter\ProjectVoter + arguments: ["@security.access.decision_manager"] + tags: + - { name: security.voter } + + # security voter to check activity access + timesheet.voter.activity: + class: TimesheetBundle\Voter\ActivityVoter + arguments: ["@security.access.decision_manager"] + tags: + - { name: security.voter } + +# ================================================================================ +# FORMS +# ================================================================================ + # form to edit user roles app.admin.user_profile_roles: class: AppBundle\Form\UserRolesType @@ -51,6 +76,10 @@ services: tags: - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupNavbar } +# ================================================================================ +# DATABASE +# ================================================================================ + # service that prefixes every database table app.tableprefix_subscriber: class: AppBundle\Doctrine\TablePrefixSubscriber @@ -58,13 +87,6 @@ services: tags: - { name: doctrine.event_subscriber } - # additional menu entries for the timesheet bundle - timesheet.configure_menu_listener: - class: TimesheetBundle\EventListener\Menu - tags: - - { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure } - - { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure } - # Uncomment the following lines to define a service for the Post Doctrine repository. # It's not mandatory to create these services, but if you use repositories a lot, # these services simplify your code: @@ -81,6 +103,10 @@ services: # // same code using repository services # $posts = $this->get('app.post_repository')->findAll(); +# ================================================================================ +# THEME +# ================================================================================ + avanzu_admin_theme.navbar_user_listener: class: AppBundle\EventListener\NavbarShowUserListener arguments: ["@security.token_storage"] @@ -108,3 +134,30 @@ services: # tags: # - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu } # - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu } + +# ================================================================================ +# APPLICATION CORE +# ================================================================================ + + # a route listener, that injects the locale through a URL directory + app.redirect_to_preferred_locale_listener: + class: AppBundle\EventListener\RedirectToPreferredLocaleListener + arguments: ['@router', '%app_locales%', '%locale%'] + tags: + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } + + # additional menu entries for the timesheet bundle + timesheet.configure_menu_listener: + class: TimesheetBundle\EventListener\Menu + tags: + - { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure } + - { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure } + +# ================================================================================ +# CONSOLE COMMANDS +# ================================================================================ + app.command.create_user: + class: AppBundle\Command\CreateUserCommand + arguments: ["@security.password_encoder", "@doctrine", "@validator"] + tags: + - { name: console.command, command: kimai:create-user } diff --git a/src/AppBundle/Controller/Admin/UserController.php b/src/AppBundle/Controller/Admin/UserController.php index 0e94d10c..589c5ffa 100644 --- a/src/AppBundle/Controller/Admin/UserController.php +++ b/src/AppBundle/Controller/Admin/UserController.php @@ -36,6 +36,7 @@ class UserController extends AbstractController * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated") * @Method("GET") * @Cache(smaxage="10") + * @Security("is_granted('view_all', user)") */ public function indexAction($page) { @@ -48,6 +49,7 @@ class UserController extends AbstractController /** * @Route("/create", name="admin_user_create") * @Method({"GET", "POST"}) + * @Security("is_granted('create', user)") */ public function createAction(Request $request) { diff --git a/src/AppBundle/Voter/UserVoter.php b/src/AppBundle/Voter/UserVoter.php index 562a3f48..e5f845a3 100644 --- a/src/AppBundle/Voter/UserVoter.php +++ b/src/AppBundle/Voter/UserVoter.php @@ -23,9 +23,11 @@ class UserVoter extends AbstractVoter { const VIEW = 'view'; const EDIT = 'edit'; + const CREATE = 'create'; const DELETE = 'delete'; const PASSWORD = 'password'; const ROLES = 'roles'; + const VIEW_ALL = 'view_all'; /** * @param string $attribute @@ -34,7 +36,7 @@ class UserVoter extends AbstractVoter */ protected function supports($attribute, $subject) { - if (!in_array($attribute, array(self::VIEW, self::EDIT, self::ROLES, self::PASSWORD, self::DELETE))) { + if (!in_array($attribute, [self::VIEW, self::VIEW_ALL, self::EDIT, self::CREATE, self::ROLES, self::PASSWORD, self::DELETE])) { return false; } @@ -65,9 +67,13 @@ class UserVoter extends AbstractVoter case self::EDIT: case self::PASSWORD: return $this->canEdit($subject, $user, $token); + case self::VIEW_ALL: + case self::CREATE: + // create actually passes in the current user as $subject, not the new one case self::DELETE: + // if we ever allow to delete user for ADMIN we have to check if the user to be deleted is not in a higher level case self::ROLES: - return $this->canEditRoles($token); + return $this->canAdminUsers($token); } return false; @@ -78,7 +84,7 @@ class UserVoter extends AbstractVoter * @param User $user * @return bool */ - private function canView(User $profile, User $user, TokenInterface $token) + protected function canView(User $profile, User $user, TokenInterface $token) { if ($this->canEdit($profile, $user, $token)) { return true; @@ -92,9 +98,9 @@ class UserVoter extends AbstractVoter * @param User $user * @return bool */ - private function canEdit(User $profile, User $user, TokenInterface $token) + protected function canEdit(User $profile, User $user, TokenInterface $token) { - if ($this->canEditRoles($token)) { + if ($this->canAdminUsers($token)) { return true; } @@ -105,7 +111,7 @@ class UserVoter extends AbstractVoter * @param TokenInterface $token * @return bool */ - private function canEditRoles(TokenInterface $token) + protected function canAdminUsers(TokenInterface $token) { return $this->hasRole('ROLE_SUPER_ADMIN', $token); } diff --git a/src/TimesheetBundle/Controller/Admin/ActivityController.php b/src/TimesheetBundle/Controller/Admin/ActivityController.php index 154be6a6..24da5bfd 100644 --- a/src/TimesheetBundle/Controller/Admin/ActivityController.php +++ b/src/TimesheetBundle/Controller/Admin/ActivityController.php @@ -59,6 +59,7 @@ class ActivityController extends AbstractController /** * @Route("/{id}/edit", name="admin_activity_edit") * @Method({"GET", "POST"}) + * @Security("is_granted('edit', activity)") */ public function editAction(Activity $activity, Request $request) { diff --git a/src/TimesheetBundle/Controller/Admin/CustomerController.php b/src/TimesheetBundle/Controller/Admin/CustomerController.php index 9e176a5c..ef496721 100644 --- a/src/TimesheetBundle/Controller/Admin/CustomerController.php +++ b/src/TimesheetBundle/Controller/Admin/CustomerController.php @@ -62,6 +62,7 @@ class CustomerController extends AbstractController /** * @Route("/{id}/edit", name="admin_customer_edit") * @Method({"GET", "POST"}) + * @Security("is_granted('edit', customer)") */ public function editAction(Customer $customer, Request $request) { diff --git a/src/TimesheetBundle/Controller/Admin/ProjectController.php b/src/TimesheetBundle/Controller/Admin/ProjectController.php index e68b1f2c..95f1492c 100644 --- a/src/TimesheetBundle/Controller/Admin/ProjectController.php +++ b/src/TimesheetBundle/Controller/Admin/ProjectController.php @@ -59,6 +59,7 @@ class ProjectController extends AbstractController /** * @Route("/{id}/edit", name="admin_project_edit") * @Method({"GET", "POST"}) + * @Security("is_granted('edit', project)") */ public function editAction(Project $project, Request $request) { diff --git a/src/TimesheetBundle/Voter/ActivityVoter.php b/src/TimesheetBundle/Voter/ActivityVoter.php new file mode 100644 index 00000000..7371e31b --- /dev/null +++ b/src/TimesheetBundle/Voter/ActivityVoter.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace TimesheetBundle\Voter; + +use AppBundle\Entity\User; +use AppBundle\Voter\AbstractVoter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use TimesheetBundle\Entity\Activity; + +/** + * A voter to check permissions on Activities. + * + * @author Kevin Papst + */ +class ActivityVoter extends AbstractVoter +{ + const VIEW = 'view'; + const EDIT = 'edit'; + const DELETE = 'delete'; + + /** + * @param string $attribute + * @param mixed $subject + * @return bool + */ + protected function supports($attribute, $subject) + { + if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { + return false; + } + + if (!$subject instanceof Activity) { + return false; + } + + return true; + } + + /** + * @param string $attribute + * @param Activity $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::VIEW: + return $this->canView($subject, $user, $token); + case self::EDIT: + return $this->canEdit($subject, $user, $token); + case self::DELETE: + return $this->canDelete($token); + } + + return false; + } + + /** + * @param Activity $activity + * @param User $user + * @return bool + */ + protected function canView(Activity $activity, User $user, TokenInterface $token) + { + if ($this->canEdit($activity, $user, $token)) { + return true; + } + + return false; + } + + /** + * @param Activity $activity + * @param User $user + * @return bool + */ + protected function canEdit(Activity $activity, User $user, TokenInterface $token) + { + if ($this->canDelete($token)) { + return true; + } + + return false; + } + + /** + * @param TokenInterface $token + * @return bool + */ + protected function canDelete(TokenInterface $token) + { + return $this->hasRole('ROLE_ADMIN', $token); + } +} diff --git a/src/TimesheetBundle/Voter/CustomerVoter.php b/src/TimesheetBundle/Voter/CustomerVoter.php new file mode 100644 index 00000000..cdec3cde --- /dev/null +++ b/src/TimesheetBundle/Voter/CustomerVoter.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace TimesheetBundle\Voter; + +use AppBundle\Entity\User; +use AppBundle\Voter\AbstractVoter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use TimesheetBundle\Entity\Customer; + +/** + * A voter to check permissions on Customers. + * + * @author Kevin Papst + */ +class CustomerVoter extends AbstractVoter +{ + const VIEW = 'view'; + const EDIT = 'edit'; + const DELETE = 'delete'; + + /** + * @param string $attribute + * @param mixed $subject + * @return bool + */ + protected function supports($attribute, $subject) + { + if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { + return false; + } + + if (!$subject instanceof Customer) { + return false; + } + + return true; + } + + /** + * @param string $attribute + * @param Customer $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::VIEW: + return $this->canView($subject, $user, $token); + case self::EDIT: + return $this->canEdit($subject, $user, $token); + case self::DELETE: + return $this->canDelete($token); + } + + return false; + } + + /** + * @param Customer $customer + * @param User $user + * @return bool + */ + protected function canView(Customer $customer, User $user, TokenInterface $token) + { + if ($this->canEdit($customer, $user, $token)) { + return true; + } + + return false; + } + + /** + * @param Customer $customer + * @param User $user + * @return bool + */ + protected function canEdit(Customer $customer, User $user, TokenInterface $token) + { + if ($this->canDelete($token)) { + return true; + } + + return false; + } + + /** + * @param TokenInterface $token + * @return bool + */ + protected function canDelete(TokenInterface $token) + { + return $this->hasRole('ROLE_ADMIN', $token); + } +} diff --git a/src/TimesheetBundle/Voter/ProjectVoter.php b/src/TimesheetBundle/Voter/ProjectVoter.php new file mode 100644 index 00000000..33464d79 --- /dev/null +++ b/src/TimesheetBundle/Voter/ProjectVoter.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace TimesheetBundle\Voter; + +use AppBundle\Entity\User; +use AppBundle\Voter\AbstractVoter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use TimesheetBundle\Entity\Project; + +/** + * A voter to check permissions on Projects. + * + * @author Kevin Papst + */ +class ProjectVoter extends AbstractVoter +{ + const VIEW = 'view'; + const EDIT = 'edit'; + const DELETE = 'delete'; + + /** + * @param string $attribute + * @param mixed $subject + * @return bool + */ + protected function supports($attribute, $subject) + { + if (!in_array($attribute, array(self::VIEW, self::EDIT, self::DELETE))) { + return false; + } + + if (!$subject instanceof Project) { + return false; + } + + return true; + } + + /** + * @param string $attribute + * @param Project $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::VIEW: + return $this->canView($subject, $user, $token); + case self::EDIT: + return $this->canEdit($subject, $user, $token); + case self::DELETE: + return $this->canDelete($token); + } + + return false; + } + + /** + * @param Project $project + * @param User $user + * @return bool + */ + protected function canView(Project $project, User $user, TokenInterface $token) + { + if ($this->canEdit($project, $user, $token)) { + return true; + } + + return false; + } + + /** + * @param Project $project + * @param User $user + * @return bool + */ + protected function canEdit(Project $project, User $user, TokenInterface $token) + { + if ($this->canDelete($token)) { + return true; + } + + return false; + } + + /** + * @param TokenInterface $token + * @return bool + */ + protected function canDelete(TokenInterface $token) + { + return $this->hasRole('ROLE_ADMIN', $token); + } +}