added configurable permission system (#424)
This commit is contained in:
@@ -28,8 +28,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Timesheet")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
@@ -67,6 +65,8 @@ class TimesheetController extends BaseApiController
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered (allowed values: 'id', 'begin', 'end', 'rate')")
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
@@ -120,6 +120,8 @@ class TimesheetController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
@@ -146,6 +148,8 @@ class TimesheetController extends BaseApiController
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
|
||||
@@ -22,9 +22,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("User")
|
||||
*
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
*/
|
||||
class UserController extends BaseApiController
|
||||
{
|
||||
@@ -55,6 +52,8 @@ class UserController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/UserCollection"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
@@ -73,6 +72,8 @@ class UserController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
|
||||
@@ -27,8 +27,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
@@ -44,6 +43,11 @@ class ActivityController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -71,6 +75,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_activity')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
@@ -94,8 +99,6 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
|
||||
@@ -26,8 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_customer')")
|
||||
*/
|
||||
class CustomerController extends AbstractController
|
||||
{
|
||||
@@ -55,6 +54,11 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_customer')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -81,6 +85,10 @@ class CustomerController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -95,6 +103,10 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -102,35 +114,6 @@ class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
@@ -179,6 +162,33 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
@@ -28,8 +28,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage projects in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/project")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_project')")
|
||||
*/
|
||||
class ProjectController extends AbstractController
|
||||
{
|
||||
@@ -45,6 +44,11 @@ class ProjectController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_project')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -72,6 +76,10 @@ class ProjectController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_project')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -81,6 +89,10 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -88,8 +100,6 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*
|
||||
|
||||
@@ -24,8 +24,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used for manage timesheet entries in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/team/timesheet")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
{
|
||||
@@ -41,10 +40,9 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* This route shows all users timesheet entries.
|
||||
*
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param $page
|
||||
* @param Request $request
|
||||
@@ -81,8 +79,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
*
|
||||
* @Route(path="/{id}/stop", name="admin_timesheet_stop", methods={"GET"})
|
||||
* @Security("is_granted('stop', entry)")
|
||||
*
|
||||
@@ -95,8 +91,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/edit", name="admin_timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
@@ -110,9 +104,8 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route(path="/create", name="admin_timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_other_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
@@ -123,8 +116,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
@@ -157,7 +148,8 @@ class TimesheetController extends AbstractController
|
||||
'action' => $this->generateUrl('admin_timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'include_user' => true
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_user' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -175,7 +167,8 @@ class TimesheetController extends AbstractController
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'include_user' => true
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_user' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* Controller used to manage users in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/user")
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
@@ -54,7 +53,8 @@ class UserController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
|
||||
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -84,7 +84,10 @@ class UserController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create', user)")
|
||||
* @Security("is_granted('create_user')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -124,8 +127,6 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing user.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', userToDelete)")
|
||||
*
|
||||
@@ -136,6 +137,7 @@ class UserController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(User $userToDelete, Request $request)
|
||||
{
|
||||
// $userToDelete MUST not be called $user, as $user is always the current user!
|
||||
$stats = $this->getDoctrine()->getRepository(Timesheet::class)->getUserStatistics($userToDelete);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
|
||||
@@ -28,7 +28,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage invoices.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -77,7 +77,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', 'invoice')")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -111,7 +111,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create', 'invoice')")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -212,7 +212,7 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', 'invoice_template')")
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -244,7 +244,7 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create', 'invoice_template')")
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $template
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\UserApiTokenType;
|
||||
use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
@@ -19,6 +20,7 @@ use App\Form\UserRolesType;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Voter\UserVoter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -28,10 +30,15 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* User profile controller
|
||||
*
|
||||
* @Route(path="/profile")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('view_own_profile') or is_granted('view_other_profile')")
|
||||
*/
|
||||
class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
@@ -40,9 +47,10 @@ class ProfileController extends AbstractController
|
||||
/**
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +170,10 @@ class ProfileController extends AbstractController
|
||||
*/
|
||||
public function savePreferencesAction(User $profile, Request $request)
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
$original[$preference->getName()] = $preference;
|
||||
@@ -194,8 +206,11 @@ class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale if neccessary
|
||||
$locale = $profile->getPreferenceValue('language', $request->getLocale());
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
@@ -269,6 +284,10 @@ class ProfileController extends AbstractController
|
||||
*/
|
||||
private function createPreferencesForm(User $user)
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
|
||||
return $this->createForm(
|
||||
UserPreferencesForm::class,
|
||||
$user,
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -24,7 +23,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
* Controller used to manage timesheets.
|
||||
*
|
||||
* @Route(path="/timesheet")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
{
|
||||
@@ -41,7 +40,7 @@ class TimesheetController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
@@ -81,6 +80,7 @@ class TimesheetController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="timesheet_export", methods={"GET"})
|
||||
* @Security("is_granted('export_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -114,8 +114,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The "main button and fly-out" for displaying (and stopping) active entries.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function activeEntriesAction()
|
||||
@@ -144,8 +142,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to re-start a timesheet entry.
|
||||
*
|
||||
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
|
||||
* @Security("is_granted('start', timesheet)")
|
||||
*
|
||||
@@ -182,8 +178,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/edit", name="timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
@@ -201,9 +195,8 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route(path="/create", name="timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
@@ -214,8 +207,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
@@ -247,6 +238,7 @@ class TimesheetController extends AbstractController
|
||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
]);
|
||||
}
|
||||
@@ -264,6 +256,7 @@ class TimesheetController extends AbstractController
|
||||
'page' => $page
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ class UserFixtures extends Fixture
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* AppFixtures constructor.
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
|
||||
@@ -40,11 +40,43 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
||||
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
||||
$container->setParameter('kimai.defaults', $config['defaults']);
|
||||
|
||||
$this->createPermissionParameter($config['permissions'], $container);
|
||||
$this->createThemeParameter($config['theme'], $container);
|
||||
$this->createUserParameter($config['user'], $container);
|
||||
$this->createTimesheetParameter($config['timesheet'], $container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs some pre-compilation on the configured permissions from kimai.yaml
|
||||
* to save us from constant array lookups from during runtime.
|
||||
*
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
protected function createPermissionParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
foreach ($config['maps'] as $role => $sets) {
|
||||
if (!isset($config['roles'][$role])) {
|
||||
$exception = new InvalidConfigurationException(
|
||||
'Configured permission set includes unknown role "' . $role . '"'
|
||||
);
|
||||
$exception->setPath('kimai.permissions.maps.' . $role);
|
||||
throw $exception;
|
||||
}
|
||||
foreach ($sets as $set) {
|
||||
if (!isset($config['sets'][$set])) {
|
||||
$exception = new InvalidConfigurationException(
|
||||
'Configured permission set "' . $set . '" for role "' . $role . '" is unknown'
|
||||
);
|
||||
$exception->setPath('kimai.permissions.maps.' . $role);
|
||||
throw $exception;
|
||||
}
|
||||
$config['roles'][$role] = array_unique(array_merge($config['roles'][$role], $config['sets'][$set]));
|
||||
}
|
||||
}
|
||||
$container->setParameter('kimai.permissions', $config['roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
|
||||
@@ -23,8 +23,6 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
*/
|
||||
protected $allowedEngines = [
|
||||
'mysql',
|
||||
'oracle',
|
||||
'postgres',
|
||||
'sqlite'
|
||||
];
|
||||
|
||||
@@ -37,15 +35,6 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
$engine = null;
|
||||
|
||||
// TODO - this does return the wrong connection. it used to be mysql, even if
|
||||
// TODO - getenv('DATABASE_URL') returned an sqlite:// connection string
|
||||
/*
|
||||
$dbConfig = $container->getExtensionConfig('doctrine');
|
||||
if (isset($dbConfig[0]['dbal']['driver'])) {
|
||||
$engine = str_replace('pdo_', '', $dbConfig[0]['dbal']['driver']);
|
||||
}
|
||||
*/
|
||||
|
||||
if (null === $engine) {
|
||||
$dbConfig = explode('://', getenv('DATABASE_URL'));
|
||||
$engine = $dbConfig['0'] ?: null;
|
||||
|
||||
@@ -40,6 +40,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
->append($this->getDefaultsNode())
|
||||
->append($this->getPermissionsNode())
|
||||
->end()
|
||||
->end();
|
||||
|
||||
@@ -329,4 +330,51 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getPermissionsNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('permissions');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('sets')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('maps')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('roles')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ class UserPreference
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = true;
|
||||
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
@@ -181,6 +186,25 @@ class UserPreference
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $enabled
|
||||
* @return UserPreference
|
||||
*/
|
||||
public function setEnabled(bool $enabled)
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the constraints which are used for validation of the value.
|
||||
*
|
||||
|
||||
42
src/Event/PrepareUserEvent.php
Normal file
42
src/Event/PrepareUserEvent.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if a user profile is loaded and we full data including dynamic user preferences
|
||||
*/
|
||||
class PrepareUserEvent extends Event
|
||||
{
|
||||
public const PREPARE = 'app.prepare_user';
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use App\Entity\UserPreference;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Class UserPreferenceEvent
|
||||
* This event should be used, if further user preferences should added dynamically
|
||||
*/
|
||||
class UserPreferenceEvent extends Event
|
||||
{
|
||||
@@ -30,7 +30,6 @@ class UserPreferenceEvent extends Event
|
||||
protected $preferences;
|
||||
|
||||
/**
|
||||
* UserPreferenceEvent constructor.
|
||||
* @param User $user
|
||||
* @param UserPreference[] $preferences
|
||||
*/
|
||||
@@ -41,6 +40,7 @@ class UserPreferenceEvent extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not set the preferences directly to the user object, but ONLY via addUserPreference()
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
|
||||
@@ -53,26 +53,23 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$isUser = $isLoggedIn && $auth->isGranted('ROLE_USER');
|
||||
$isTeamlead = $isLoggedIn && $auth->isGranted('ROLE_TEAMLEAD');
|
||||
|
||||
if (!$isLoggedIn || !$isUser) {
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getMenu();
|
||||
$menu->addItem(
|
||||
new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], 'far fa-clock')
|
||||
);
|
||||
|
||||
if (!$isTeamlead) {
|
||||
return;
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
);
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,33 +77,42 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
|
||||
{
|
||||
$menu = $event->getAdminMenu();
|
||||
$auth = $this->security;
|
||||
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED') || !$auth->isGranted('ROLE_TEAMLEAD')) {
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if (!$auth->isGranted('ROLE_ADMIN')) {
|
||||
return;
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-user')
|
||||
);
|
||||
}
|
||||
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
)->addChild(
|
||||
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fas fa-project-diagram')
|
||||
)->addChild(
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
if ($auth->isGranted('view_customer')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fas fa-project-diagram')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Event\UserPreferenceEvent;
|
||||
use App\Form\Type\CalendarViewType;
|
||||
use App\Form\Type\LanguageType;
|
||||
@@ -19,15 +20,10 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
/**
|
||||
* Class UserPreferenceSubscriber
|
||||
*/
|
||||
class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
@@ -35,20 +31,26 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $voter;
|
||||
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* UserPreferenceSubscriber constructor.
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param TokenStorageInterface $storage
|
||||
* @param AuthorizationCheckerInterface $voter
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage, AuthorizationCheckerInterface $voter)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
$this->voter = $voter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,27 +59,37 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::CONTROLLER => ['loadUserPreferences', 200]
|
||||
PrepareUserEvent::PREPARE => ['loadUserPreferences', 200]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return UserPreference[]
|
||||
*/
|
||||
public function getDefaultPreferences()
|
||||
public function getDefaultPreferences(User $user)
|
||||
{
|
||||
$enableHourlyRate = false;
|
||||
|
||||
if ($this->voter->isGranted('hourly-rate', $user)) {
|
||||
$enableHourlyRate = true;
|
||||
}
|
||||
|
||||
/*
|
||||
(new UserPreference())
|
||||
->setName('timezone')
|
||||
->setValue(date_default_timezone_get())
|
||||
->setType(TimezoneType::class),
|
||||
*/
|
||||
|
||||
return [
|
||||
(new UserPreference())
|
||||
->setName(UserPreference::HOURLY_RATE)
|
||||
->setValue(0)
|
||||
->setType(IntegerType::class)
|
||||
->setEnabled($enableHourlyRate)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
/*
|
||||
(new UserPreference())
|
||||
->setName('timezone')
|
||||
->setValue(date_default_timezone_get())
|
||||
->setType(TimezoneType::class),
|
||||
*/
|
||||
|
||||
(new UserPreference())
|
||||
->setName('language')
|
||||
->setValue('en') // TODO fetch from services.yaml
|
||||
@@ -116,31 +128,32 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @param PrepareUserEvent $event
|
||||
*/
|
||||
public function loadUserPreferences(KernelEvent $event)
|
||||
public function loadUserPreferences(PrepareUserEvent $event)
|
||||
{
|
||||
if (!$this->canHandleEvent($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
$user = $event->getUser();
|
||||
|
||||
$prefs = [];
|
||||
foreach ($user->getPreferences() as $preference) {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
}
|
||||
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences());
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
||||
$this->eventDispatcher->dispatch(UserPreferenceEvent::CONFIGURE, $event);
|
||||
|
||||
foreach ($event->getPreferences() as $preference) {
|
||||
/* @var UserPreference[] $prefs */
|
||||
if (isset($prefs[$preference->getName()])) {
|
||||
/* @var UserPreference $pref */
|
||||
$prefs[$preference->getName()]
|
||||
->setType($preference->getType())
|
||||
->setConstraints($preference->getConstraints())
|
||||
->setEnabled($preference->isEnabled())
|
||||
;
|
||||
} else {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
@@ -151,24 +164,15 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @param PrepareUserEvent $event
|
||||
* @return bool
|
||||
*/
|
||||
protected function canHandleEvent(KernelEvent $event): bool
|
||||
protected function canHandleEvent(PrepareUserEvent $event): bool
|
||||
{
|
||||
// Ignore sub-requests
|
||||
if (!$event->isMasterRequest()) {
|
||||
if (null === ($user = $event->getUser())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === $this->storage->getToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
return ($user instanceof User);
|
||||
}
|
||||
}
|
||||
|
||||
89
src/EventSubscriber/UserProfileSubscriber.php
Normal file
89
src/EventSubscriber/UserProfileSubscriber.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class UserProfileSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param TokenStorageInterface $storage
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::CONTROLLER => ['prepareUserProfile', 200]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
*/
|
||||
public function prepareUserProfile(KernelEvent $event)
|
||||
{
|
||||
if (!$this->canHandleEvent($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->eventDispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @return bool
|
||||
*/
|
||||
protected function canHandleEvent(KernelEvent $event): bool
|
||||
{
|
||||
// Ignore sub-requests
|
||||
if (!$event->isMasterRequest()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === $this->storage->getToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
return ($user instanceof User);
|
||||
}
|
||||
}
|
||||
@@ -133,15 +133,20 @@ class TimesheetEditForm extends AbstractType
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
|
||||
if ($options['include_rate']) {
|
||||
$builder
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
$builder->get('customer')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
@@ -162,6 +167,7 @@ class TimesheetEditForm extends AbstractType
|
||||
}
|
||||
);
|
||||
*/
|
||||
|
||||
$builder->get('project')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
@@ -192,6 +198,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'csrf_token_id' => 'timesheet_edit',
|
||||
'duration_only' => false,
|
||||
'include_user' => false,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,23 +35,31 @@ class UserPreferenceType extends AbstractType
|
||||
/** @var UserPreference $preference */
|
||||
$preference = $event->getData();
|
||||
|
||||
if ($preference instanceof UserPreference) {
|
||||
// prevents unconfigured values from showing up in the form
|
||||
if ($preference->getType() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = true;
|
||||
if (CheckboxType::class == $preference->getType()) {
|
||||
$required = false;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $preference->getType(), [
|
||||
'label' => 'label.' . $preference->getName(),
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
]);
|
||||
if (!($preference instanceof UserPreference)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevents unconfigured values from showing up in the form
|
||||
if ($preference->getType() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = true;
|
||||
if (CheckboxType::class == $preference->getType()) {
|
||||
$required = false;
|
||||
}
|
||||
|
||||
$type = $preference->getType();
|
||||
if (!$preference->isEnabled()) {
|
||||
$type = HiddenType::class;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $type, [
|
||||
'label' => 'label.' . $preference->getName(),
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
'disabled' => !$preference->isEnabled(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
$builder->add('name', HiddenType::class);
|
||||
|
||||
@@ -27,6 +27,18 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to fetch preferences when using the Profile controller actions.
|
||||
* Depends on the query, some magic mechanisms like the ParamConverter will use this method to fetch the user.
|
||||
*/
|
||||
public function findOneBy(array $criteria, array $orderBy = null)
|
||||
{
|
||||
if (count($criteria) == 1 && isset($criteria['username'])) {
|
||||
return $this->loadUserByUsername($criteria['username']);
|
||||
}
|
||||
|
||||
return parent::findOneBy($criteria, $orderBy);
|
||||
}
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -43,7 +55,6 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
// if we join activities, the maxperpage limit will limit the list to the amount or projects + activties
|
||||
$qb->select('u')
|
||||
->from(User::class, 'u')
|
||||
->orderBy('u.' . $query->getOrderBy(), $query->getOrder());
|
||||
@@ -77,6 +88,8 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->where('u.username = :username')
|
||||
->orWhere('u.email = :username')
|
||||
->setParameter('username', $username)
|
||||
|
||||
67
src/Security/RolePermissionManager.php
Normal file
67
src/Security/RolePermissionManager.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
class RolePermissionManager
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $knownPermissions = [];
|
||||
|
||||
/**
|
||||
* @param array $permissions
|
||||
*/
|
||||
public function __construct(array $permissions)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
|
||||
foreach ($permissions as $role => $perms) {
|
||||
$this->knownPermissions = array_merge($this->knownPermissions, $perms);
|
||||
}
|
||||
$this->knownPermissions = array_unique($this->knownPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission($permission)
|
||||
{
|
||||
return in_array($permission, $this->knownPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @return bool
|
||||
*/
|
||||
public function roleHasPermission($role)
|
||||
{
|
||||
return isset($this->permissions[$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($role, $permission)
|
||||
{
|
||||
if (!isset($this->permissions[$role])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, $this->permissions[$role]);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Security\RolePermissionManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* Abstract voter to help with checking user roles.
|
||||
* Abstract voter to help with checking user permissions.
|
||||
*/
|
||||
abstract class AbstractVoter extends Voter
|
||||
{
|
||||
@@ -22,14 +24,19 @@ abstract class AbstractVoter extends Voter
|
||||
* @var AclDecisionManager
|
||||
*/
|
||||
protected $decisionManager;
|
||||
/**
|
||||
* @var RolePermissionManager
|
||||
*/
|
||||
protected $roleManager;
|
||||
|
||||
/**
|
||||
* AbstractVoter constructor.
|
||||
* @param AclDecisionManager $decisionManager
|
||||
* @param RolePermissionManager $roleManager
|
||||
*/
|
||||
public function __construct(AclDecisionManager $decisionManager)
|
||||
public function __construct(AclDecisionManager $decisionManager, RolePermissionManager $roleManager)
|
||||
{
|
||||
$this->decisionManager = $decisionManager;
|
||||
$this->roleManager = $roleManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,4 +57,39 @@ abstract class AbstractVoter extends Voter
|
||||
{
|
||||
return $this->decisionManager->hasRole($token, [$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasPermission($role, $permission)
|
||||
{
|
||||
return $this->roleManager->hasPermission($role, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasRolePermission(User $user, $permission)
|
||||
{
|
||||
foreach ($user->getRoles() as $role) {
|
||||
if ($this->hasPermission($role, $permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission($permission)
|
||||
{
|
||||
return $this->roleManager->isRegisteredPermission($permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ class ActivityVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Activity)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class ActivityVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Activity $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Activity) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class ActivityVoter extends AbstractVoter
|
||||
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);
|
||||
if ($subject instanceof Activity) {
|
||||
return $this->hasRolePermission($user, $attribute . '_activity');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @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
|
||||
* @param TokenInterface $token
|
||||
* @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->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ class CustomerVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Customer)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class CustomerVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Customer $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Customer) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class CustomerVoter extends AbstractVoter
|
||||
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);
|
||||
if ($subject instanceof Customer) {
|
||||
return $this->hasRolePermission($user, $attribute . '_customer');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @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
|
||||
* @param TokenInterface $token
|
||||
* @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->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
72
src/Voter/InvoiceTemplateVoter.php
Normal file
72
src/Voter/InvoiceTemplateVoter.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on InvoiceTemplateVote.
|
||||
*/
|
||||
class InvoiceTemplateVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: InvoiceTemplate)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!$subject instanceof InvoiceTemplate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject instanceof InvoiceTemplate) {
|
||||
return $this->hasRolePermission($user, $attribute . '_invoice_template');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on Invoices.
|
||||
*/
|
||||
class InvoiceVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const CREATE = 'create';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::CREATE,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
public const ALLOWED_SUBJECTS = [
|
||||
'invoice',
|
||||
'invoice_template'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof InvoiceTemplate) {
|
||||
if (!is_string($subject) || !in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param string|InvoiceTemplate $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($user, $token);
|
||||
case self::CREATE:
|
||||
return $this->canCreate($user, $token);
|
||||
case self::EDIT:
|
||||
return $this->canEdit($user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($token);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($user, $token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canCreate(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_TEAMLEAD', $token);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ class ProjectVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Project)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class ProjectVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Project $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Project) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class ProjectVoter extends AbstractVoter
|
||||
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);
|
||||
if ($subject instanceof Project) {
|
||||
return $this->hasRolePermission($user, $attribute . '_project');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @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
|
||||
* @param TokenInterface $token
|
||||
* @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->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Voter/RolePermissionVoter.php
Normal file
64
src/Voter/RolePermissionVoter.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check the free-configurable permission from "kimai.permissions".
|
||||
*/
|
||||
class RolePermissionVoter extends AbstractVoter
|
||||
{
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
if (null !== $subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// and which is not neither a user role like USER_ADMIN
|
||||
// nor an implicit role like IS_REMEMBERED / IS_FULLY_AUTHENTICATED
|
||||
if (strpos($attribute, 'ROLE_') === false && strpos($attribute, 'IS_') === false) {
|
||||
return $this->isRegisteredPermission($attribute);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
foreach ($user->getRoles() as $role) {
|
||||
if ($this->hasPermission($role, $attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,21 @@ class TimesheetVoter extends AbstractVoter
|
||||
{
|
||||
public const START = 'start';
|
||||
public const STOP = 'stop';
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
public const VIEW_RATE = 'view_rate';
|
||||
public const EDIT_RATE = 'edit_rate';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Timesheet)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::START,
|
||||
self::STOP,
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
self::DELETE,
|
||||
self::VIEW_RATE,
|
||||
self::EDIT_RATE,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -40,11 +45,11 @@ class TimesheetVoter extends AbstractVoter
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Timesheet) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,40 +66,48 @@ class TimesheetVoter extends AbstractVoter
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
if (!($user instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::STOP:
|
||||
return $this->canStop($subject, $user, $token);
|
||||
|
||||
case self::START:
|
||||
return $this->canStart($subject, $user, $token);
|
||||
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
|
||||
case self::EDIT:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
|
||||
case self::DELETE:
|
||||
return $this->canDelete($subject, $user, $token);
|
||||
if (!($subject instanceof Timesheet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$permission = '';
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canStop(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
// if a teamlead stops an entry for another user, check that this user is part of his team
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
switch ($attribute) {
|
||||
case self::START:
|
||||
if (!$this->canStart($subject, $user, $token)) {
|
||||
return false;
|
||||
}
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
case self::VIEW_RATE:
|
||||
case self::EDIT_RATE:
|
||||
case self::STOP:
|
||||
case self::EDIT:
|
||||
case self::DELETE:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
$permission .= '_';
|
||||
|
||||
// extend me for "team" support later on
|
||||
if ($subject->getUser()->getId() == $user->getId()) {
|
||||
$permission .= 'own';
|
||||
} else {
|
||||
$permission .= 'other';
|
||||
}
|
||||
|
||||
$permission .= '_timesheet';
|
||||
|
||||
return $this->hasRolePermission($user, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,65 +131,4 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if (!$this->isFullyAuthenticated($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isOwnOrAdmin($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function isOwnOrTeamlead(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($timesheet->getUser()->getId() == $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasRole('ROLE_TEAMLEAD', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function isOwnOrAdmin(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($timesheet->getUser()->getId() == $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,22 +19,22 @@ class UserVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const CREATE = 'create';
|
||||
public const DELETE = 'delete';
|
||||
public const PASSWORD = 'password';
|
||||
public const ROLES = 'roles';
|
||||
public const PREFERENCES = 'preferences';
|
||||
public const API_TOKEN = 'api-token';
|
||||
public const HOURLY_RATE = 'hourly-rate';
|
||||
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::CREATE,
|
||||
self::ROLES,
|
||||
self::PASSWORD,
|
||||
self::DELETE,
|
||||
self::PREFERENCES,
|
||||
self::API_TOKEN,
|
||||
self::HOURLY_RATE,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ class UserVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof User) {
|
||||
if (!($subject instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,66 +65,48 @@ class UserVoter extends AbstractVoter
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
if (!($user instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$permission = '';
|
||||
|
||||
switch ($attribute) {
|
||||
// special case for the UserController
|
||||
case self::DELETE:
|
||||
if (!$this->canDelete($subject, $user, $token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, 'delete_user');
|
||||
|
||||
// used in templates and ProfileController
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
case self::EDIT:
|
||||
case self::API_TOKEN:
|
||||
case self::PASSWORD:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($subject, $user, $token);
|
||||
case self::CREATE: // create actually passes in the current user as $subject, not the new one
|
||||
case self::ROLES:
|
||||
return $this->canAdminUsers($token);
|
||||
case self::PREFERENCES:
|
||||
return $this->canEditPreferences($subject, $user, $token);
|
||||
case self::HOURLY_RATE:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$permission .= '_';
|
||||
|
||||
/**
|
||||
* @param User $profile
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEditPreferences(User $profile, User $user, TokenInterface $token)
|
||||
{
|
||||
return $profile->getId() === $user->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $profile
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(User $profile, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($profile, $user, $token)) {
|
||||
return true;
|
||||
// extend me for "team" support later on
|
||||
if ($subject->getId() == $user->getId()) {
|
||||
$permission .= 'own';
|
||||
} else {
|
||||
$permission .= 'other';
|
||||
}
|
||||
|
||||
return $profile->getId() === $user->getId();
|
||||
}
|
||||
$permission .= '_profile';
|
||||
|
||||
/**
|
||||
* @param User $profile
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(User $profile, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canAdminUsers($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $profile->getId() === $user->getId();
|
||||
return $this->hasRolePermission($user, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,19 +116,6 @@ class UserVoter extends AbstractVoter
|
||||
*/
|
||||
protected function canDelete(User $profile, User $user, TokenInterface $token)
|
||||
{
|
||||
if (!$this->canAdminUsers($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $profile->getId() !== $user->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canAdminUsers(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole(User::ROLE_SUPER_ADMIN, $token);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user