added configurable permission system (#424)

This commit is contained in:
Kevin Papst
2018-11-26 13:20:32 +01:00
committed by GitHub
parent 0334f6ce86
commit 8fddf627bf
62 changed files with 1831 additions and 794 deletions

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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)")
*

View File

@@ -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

View File

@@ -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)")
*

View File

@@ -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,
]);
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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,

View File

@@ -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(),
]);
}

View File

@@ -47,7 +47,6 @@ class UserFixtures extends Fixture
private $encoder;
/**
* AppFixtures constructor.
* @param UserPasswordEncoderInterface $encoder
*/
public function __construct(UserPasswordEncoderInterface $encoder)

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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.
*

View 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;
}
}

View File

@@ -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()

View File

@@ -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')
);
}
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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',
]);
}

View File

@@ -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);

View File

@@ -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)

View 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]);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}