added configurable permission system (#424)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Settings for the user management and login forms
|
||||
user:
|
||||
registration: true
|
||||
password_reset: true
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All configs related to timesheet and record management
|
||||
timesheet:
|
||||
|
||||
@@ -33,6 +35,7 @@ kimai:
|
||||
# days: ['saturday','sunday']
|
||||
# factor: 1.5
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Invoice management
|
||||
#invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
@@ -40,6 +43,45 @@ kimai:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Default settings used to populate forms
|
||||
#defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Free configurable permission system, see var/docs/permissions.md
|
||||
permissions:
|
||||
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
|
||||
sets:
|
||||
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity]
|
||||
PROJECTS: [view_project,create_project,edit_project,delete_project]
|
||||
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer]
|
||||
INVOICE: [view_invoice,create_invoice]
|
||||
INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_invoice_template]
|
||||
TIMESHEET: [view_own_timesheet,start_own_timesheet,stop_own_timesheet,create_own_timesheet,edit_own_timesheet,export_own_timesheet,delete_own_timesheet]
|
||||
TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,delete_other_timesheet]
|
||||
PROFILE: [view_own_profile,edit_own_profile,password_own_profile,preferences_own_profile,api-token_own_profile]
|
||||
PROFILE_OTHER: [view_other_profile,edit_other_profile,delete_other_profile,password_other_profile,roles_other_profile,preferences_other_profile,api-token_other_profile]
|
||||
USER: [view_user,create_user,delete_user]
|
||||
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet]
|
||||
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet]
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
maps:
|
||||
ROLE_USER: [TIMESHEET,PROFILE]
|
||||
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE]
|
||||
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER]
|
||||
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER]
|
||||
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
|
||||
roles:
|
||||
ROLE_USER: []
|
||||
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
||||
ROLE_ADMIN: [hourly-rate_own_profile]
|
||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Language specific settings, like the date formats
|
||||
languages:
|
||||
de:
|
||||
@@ -75,6 +117,7 @@ kimai:
|
||||
duration: '%%h:%%m óra'
|
||||
duration_short: '%%h:%%m h'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# timesheet calendar configuration
|
||||
calendar:
|
||||
week_numbers: true
|
||||
@@ -93,6 +136,7 @@ kimai:
|
||||
# id: 'de.german#holiday@group.v.calendar.google.com'
|
||||
# color: '#ccc'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# theme related settings, will be available as twig globals at "kimai_context.*"
|
||||
# please see documentation at var/docs/theme.md
|
||||
theme:
|
||||
@@ -105,6 +149,7 @@ kimai:
|
||||
# possible options: blue, black, purple, yellow, red, green
|
||||
box_color: 'green'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Dashboard widget sections, please see documentation at var/docs/dashboard.md
|
||||
dashboard:
|
||||
user_duration:
|
||||
@@ -133,6 +178,7 @@ kimai:
|
||||
permission: ROLE_ADMIN
|
||||
widgets: [amountToday, amountWeek, amountMonth, amountYear]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All available widgets, please see documentation at var/docs/dashboard.md
|
||||
widgets:
|
||||
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
@@ -161,10 +207,3 @@ kimai:
|
||||
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
|
||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||
|
||||
# Default settings used to populate forms
|
||||
#defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
@@ -47,5 +47,5 @@ security:
|
||||
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
|
||||
@@ -118,6 +118,13 @@ services:
|
||||
App\Timesheet\Calculator\RateCalculator:
|
||||
arguments: ["%kimai.timesheet.rates%"]
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ["%kimai.permissions%"]
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
# ================================================================================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_activity.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseActivityAdmin', 'visibility': '#modal_activity_admin', 'create': path('admin_activity_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseActivityAdmin', 'visibility': '#modal_activity_admin'} %}
|
||||
{% if is_granted('create_activity') %}
|
||||
{% set actions = actions|merge({'create': path('admin_activity_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseActivityAdmin', showFilter) }}
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseCustomerAdmin', 'visibility': '#modal_customer_admin', 'create': path('admin_customer_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseCustomerAdmin', 'visibility': '#modal_customer_admin'} %}
|
||||
{% if is_granted('create_customer') %}
|
||||
{% set actions = actions|merge({'create': path('admin_customer_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseCustomerAdmin', showFilter) }}
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_project.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseProjectAdmin', 'visibility': '#modal_project_admin', 'create': path('admin_project_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseProjectAdmin', 'visibility': '#modal_project_admin'} %}
|
||||
{% if is_granted('create_project') %}
|
||||
{% set actions = actions|merge({'create': path('admin_project_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseProjectAdmin', showFilter) }}
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseTimesheetAdmin', 'visibility': '#modal_timesheet_admin', 'create': path('admin_timesheet_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseTimesheetAdmin', 'visibility': '#modal_timesheet_admin'} %}
|
||||
{% if is_granted('create_other_timesheet') %}
|
||||
{% set actions = actions|merge({'create': path('admin_timesheet_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseTimesheetAdmin', showFilter) }}
|
||||
@@ -50,15 +56,19 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||
{% else %}
|
||||
{% if not duration_only %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">‐</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
<a href="{{ path('admin_customer_edit', {'id': entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||
</td>
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_user.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseUserAdmin', 'visibility': '#modal_user_admin', 'create': path('admin_user_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseUserAdmin', 'visibility': '#modal_user_admin'} %}
|
||||
{% if is_granted('create_user') %}
|
||||
{% set actions = actions|merge({'create': path('admin_user_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseUserAdmin', showFilter) }}
|
||||
|
||||
@@ -4,7 +4,17 @@
|
||||
|
||||
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseInvoice', 'create': path('admin_invoice_template_create'), 'list': path('admin_invoice_template')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseInvoice'} %}
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_invoice_template') %}
|
||||
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content_class %}{{ parent() }} invoice{% endblock %}
|
||||
|
||||
@@ -21,23 +31,24 @@
|
||||
{% if model.entries is empty %}
|
||||
{{ widgets.callout('warning', 'invoice.select_filter') }}
|
||||
{% else %}
|
||||
|
||||
{{ include('invoice/preview.html.twig') }}
|
||||
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" id="print-invoice-button" class="btn btn-success pull-right">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
{% if is_granted('create_invoice') %}
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" id="print-invoice-button" class="btn btn-success pull-right">
|
||||
<i class="{{ 'print'|icon }}"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% if is_granted('create_invoice') %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
$( "#print-invoice-button" ).click(function() {
|
||||
@@ -48,4 +59,5 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
|
||||
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'create': path('admin_invoice_template_create'), 'invoice': path('invoice')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {} %}
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_invoice') %}
|
||||
{% set actions = actions|merge({'invoice': path('invoice')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% if entries.count == 0 %}
|
||||
@@ -26,9 +35,16 @@
|
||||
<td class="hidden-xs hidden-sm">{{ entry.dueDays }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ entry.vat }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('admin_invoice_template_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{% set actionButtons = actionButtons|merge({'copy': path('admin_invoice_template_copy', {'id' : entry.id, 'page': page})}) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_invoice_template_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_invoice_template_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{% endif %}
|
||||
{% if is_granted('create_invoice_template') %}
|
||||
{% set actionButtons = actionButtons|merge({'copy': path('admin_invoice_template_copy', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_invoice_template_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set linkUserProfile = path('user_profile', {'username' : user.username}) %}
|
||||
{% if not is_granted('view_own_profile') %}
|
||||
{% set linkUserProfile = '#' %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="control-sidebar-menu">
|
||||
<li>
|
||||
<a href="{{ path('user_profile', {'username' : user.username}) }}">
|
||||
<a href="{{ linkUserProfile }}">
|
||||
<div class="pull-left image">
|
||||
{{ macro.avatar(user.avatar, user.username, 'img-circle') }}
|
||||
</div>
|
||||
@@ -17,6 +21,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="control-sidebar-menu">
|
||||
{% if is_granted('edit_own_profile') %}
|
||||
<li>
|
||||
<a href="{{ path('user_profile_edit', {'username' : user.username}) }}">
|
||||
<div class="pull-left image">
|
||||
@@ -27,8 +32,10 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('preferences_own_profile') %}
|
||||
<li>
|
||||
<a href="{{ path('user_profile_preferences', {'username' : user.username}) }}">
|
||||
<a href="{{ path('user_profile_preferences', {'username' : user.username}) }}">
|
||||
<div class="pull-left image">
|
||||
<i class="{{ 'settings'|icon }} text-gray"></i>
|
||||
</div>
|
||||
@@ -37,6 +44,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('help') }}">
|
||||
<div class="pull-left image">
|
||||
|
||||
@@ -5,7 +5,18 @@
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseTimesheet', 'download': 'onclick:return exportTimesheet()', 'visibility': '#modal_timesheet', 'calendar': path('calendar'), 'create': path('timesheet_create')}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseTimesheet'} %}
|
||||
{% if is_granted('export_own_timesheet') %}
|
||||
{% set actions = actions|merge({'download': 'onclick:return exportTimesheet()'}) %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'visibility': '#modal_timesheet'}) %}
|
||||
{% set actions = actions|merge({'calendar': path('calendar')}) %}
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
{% set actions = actions|merge({'create': path('timesheet_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseTimesheet', showFilter) }}
|
||||
@@ -23,9 +34,13 @@
|
||||
{% set columns = columns|merge({'starttime': '', 'endtime': 'hidden-xs'}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set columns = columns|merge({'duration': ''}) %}
|
||||
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
{% set columns = columns|merge({'rate': 'hidden-xs'}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set columns = columns|merge({
|
||||
'duration': '',
|
||||
'rate': 'hidden-xs',
|
||||
'customer': 'hidden-xs hidden-sm',
|
||||
'project': 'hidden-xs hidden-sm',
|
||||
'activity': 'hidden-xs hidden-sm',
|
||||
@@ -50,13 +65,17 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||
{% if is_granted('view_rate', entry) %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not duration_only %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">‐</td>
|
||||
{% if is_granted('view_rate', entry) %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">‐</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.project.customer) }}</td>
|
||||
@@ -64,7 +83,10 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">{{ entry.description|desc2html }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if entry.end %}
|
||||
{% if is_granted('start', entry) %}
|
||||
@@ -76,7 +98,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -51,7 +51,7 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
||||
$client->followRedirect();
|
||||
|
||||
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#roles'];
|
||||
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#roles', '#preferences'];
|
||||
|
||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||
$this->assertEquals(count($expectedTabs), $tabs->count());
|
||||
|
||||
@@ -225,7 +225,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testDeleteTemplateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new InvoiceFixtures();
|
||||
|
||||
@@ -25,13 +25,24 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER);
|
||||
}
|
||||
|
||||
public function testIndexAction()
|
||||
public function getTabTestData()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$userTabs = ['#charts', '#settings', '#password', '#api-token', '#preferences'];
|
||||
|
||||
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#preferences'];
|
||||
return [
|
||||
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#charts', '#settings', '#password', '#api-token', '#preferences']],
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, array_merge($userTabs, ['#roles'])],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTabTestData
|
||||
*/
|
||||
public function testIndexAction($role, $username, $expectedTabs)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, '/profile/' . $username);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||
$this->assertEquals(count($expectedTabs), $tabs->count());
|
||||
@@ -208,16 +219,31 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(['ROLE_TEAMLEAD', 'ROLE_SUPER_ADMIN', 'ROLE_USER'], $user->getRoles());
|
||||
}
|
||||
|
||||
public function testPreferencesAction()
|
||||
public function getPreferencesTestData()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/prefs');
|
||||
return [
|
||||
// assert that the user doesn't have the "hourly-rate_own_profile" permission
|
||||
[User::ROLE_USER, UserFixtures::USERNAME_USER, 82, 82, 'ar'],
|
||||
// admins are allowed to update their own hourly rate
|
||||
[User::ROLE_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37, 'ar'],
|
||||
// admins are allowed to update other peoples hourly rate
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_USER, 82, 37, 'en'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getPreferencesTestData
|
||||
*/
|
||||
public function testPreferencesAction($role, $username, $hourlyRateOriginal, $hourlyRate, $expectedLocale)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, '/profile/' . $username . '/prefs');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
$user = $this->getUserByName($em, $username);
|
||||
|
||||
$this->assertEquals(82, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
$this->assertEquals($hourlyRateOriginal, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
$this->assertEquals('green', $user->getPreferenceValue(UserPreference::SKIN));
|
||||
$this->assertEquals(true, $user->getPreferenceValue('theme.fixed_layout'));
|
||||
$this->assertEquals(false, $user->getPreferenceValue('theme.boxed_layout'));
|
||||
@@ -242,7 +268,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
]
|
||||
]);
|
||||
|
||||
$targetUrl = '/ar/profile/' . urlencode(UserFixtures::USERNAME_USER) . '/prefs';
|
||||
$targetUrl = '/' . $expectedLocale . '/profile/' . urlencode($username) . '/prefs';
|
||||
|
||||
$this->assertIsRedirect($client, $targetUrl);
|
||||
$client->followRedirect();
|
||||
@@ -251,9 +277,9 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertHasFlashSuccess($client);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
$user = $this->getUserByName($em, $username);
|
||||
|
||||
$this->assertEquals(37, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
$this->assertEquals($hourlyRate, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
//$this->assertEquals('', $user->getPreferenceValue('America/Creston'));
|
||||
$this->assertEquals('ar', $user->getPreferenceValue('language'));
|
||||
$this->assertEquals('blue', $user->getPreferenceValue(UserPreference::SKIN));
|
||||
|
||||
@@ -183,12 +183,23 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(1, $timesheet->getProject()->getId());
|
||||
}
|
||||
|
||||
public function testStopAction()
|
||||
public function testStopActionDoesNotShowRateFieldsForUser()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$this->request($client, '/timesheet/create');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||
$this->assertFalse($form->has('hourlyRate'));
|
||||
$this->assertFalse($form->has('fixedRate'));
|
||||
}
|
||||
|
||||
public function testStopAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/timesheet/create');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
@@ -222,7 +233,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testCreateActionWithFromAndToValues()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/timesheet/create?from=2018-08-02T20%3A00%3A00&to=2018-08-02T20%3A30%3A00');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
@@ -256,7 +267,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testCreateActionWithBeginAndEndValues()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/timesheet/create?begin=2018-08-02&end=2018-08-02');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ trait KernelTestTrait
|
||||
$executor->execute($loader->getFixtures(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EntityManager $em
|
||||
* @param string $username
|
||||
* @return User|null
|
||||
*/
|
||||
protected function getUserByName(EntityManager $em, string $username)
|
||||
{
|
||||
return $em->getRepository(User::class)->findOneBy(['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EntityManager $em
|
||||
* @param string $role
|
||||
@@ -64,6 +74,6 @@ trait KernelTestTrait
|
||||
return null;
|
||||
}
|
||||
|
||||
return $em->getRepository(User::class)->findOneBy(['username' => $name]);
|
||||
return $this->getUserByName($em, $name);
|
||||
}
|
||||
}
|
||||
|
||||
95
tests/Voter/AbstractVoterTest.php
Normal file
95
tests/Voter/AbstractVoterTest.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Voter\AbstractVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractVoterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param string $voterClass
|
||||
* @param User $user
|
||||
* @return AbstractVoter
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function getVoter(string $voterClass, User $user)
|
||||
{
|
||||
$isAuthenticated = empty($user->getRoles());
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($isAuthenticated);
|
||||
$accessManager->method('hasRole')->willReturnCallback(function ($role) use ($user) {
|
||||
return in_array($role, $user->getRoles());
|
||||
});
|
||||
|
||||
$class = new \ReflectionClass($voterClass);
|
||||
|
||||
return $class->newInstance($accessManager, $this->getRolePermissionManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $role
|
||||
* @return User
|
||||
*/
|
||||
protected function getUser($id, $role)
|
||||
{
|
||||
$roles = [];
|
||||
if (!empty($role)) {
|
||||
$roles[] = $role;
|
||||
}
|
||||
|
||||
$user = $this->getMockBuilder(User::class)->getMock();
|
||||
$user->method('getId')->willReturn($id);
|
||||
$user->method('getRoles')->willReturn($roles);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $permissions
|
||||
* @param bool $overwrite
|
||||
* @return RolePermissionManager
|
||||
*/
|
||||
protected function getRolePermissionManager(array $permissions = [], bool $overwrite = false)
|
||||
{
|
||||
if (!$overwrite) {
|
||||
$activities = ['view_activity', 'edit_activity', 'delete_activity', 'create_activity'];
|
||||
$projects = ['view_project', 'edit_project', 'delete_project', 'create_project'];
|
||||
$customers = ['view_customer', 'edit_customer', 'delete_customer', 'create_customer'];
|
||||
$invoice = ['view_invoice', 'create_invoice'];
|
||||
$invoiceTemplate = ['view_invoice_template', 'create_invoice_template', 'edit_invoice_template', 'delete_invoice_template'];
|
||||
$timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet'];
|
||||
$timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'delete_other_timesheet'];
|
||||
$profile = ['view_own_profile', 'edit_own_profile', 'password_own_profile', 'preferences_own_profile', 'api-token_own_profile'];
|
||||
$profileOther = ['view_other_profile', 'edit_other_profile', 'delete_other_profile', 'password_other_profile', 'roles_other_profile', 'preferences_other_profile', 'api-token_other_profile'];
|
||||
$user = ['view_user', 'create_user', 'delete_user'];
|
||||
$rate = ['view_rate_own_timesheet', 'edit_rate_own_timesheet'];
|
||||
$rateOther = ['view_rate_other_timesheet', 'edit_rate_other_timesheet'];
|
||||
|
||||
$roleUser = [];
|
||||
$roleTeamlead = ['view_rate_own_timesheet', 'view_rate_other_timesheet', 'hourly-rate_own_profile'];
|
||||
$roleAdmin = ['hourly-rate_own_profile'];
|
||||
$roleSuperAdmin = ['hourly-rate_own_profile', 'hourly-rate_other_profile', 'delete_own_profile', 'roles_own_profile'];
|
||||
|
||||
$permissions = [
|
||||
'ROLE_USER' => array_merge($timesheet, $profile, $roleUser),
|
||||
'ROLE_TEAMLEAD' => array_merge($invoice, $timesheet, $timesheetOthers, $profile, $roleTeamlead),
|
||||
'ROLE_ADMIN' => array_merge($activities, $projects, $customers, $invoice, $invoiceTemplate, $timesheet, $timesheetOthers, $profile, $rate, $rateOther, $roleAdmin),
|
||||
'ROLE_SUPER_ADMIN' => array_merge($activities, $projects, $customers, $invoice, $invoiceTemplate, $timesheet, $timesheetOthers, $profile, $profileOther, $user, $rate, $rateOther, $roleSuperAdmin),
|
||||
];
|
||||
}
|
||||
|
||||
return new RolePermissionManager($permissions);
|
||||
}
|
||||
}
|
||||
66
tests/Voter/ActivityVoterTest.php
Normal file
66
tests/Voter/ActivityVoterTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\User;
|
||||
use App\Voter\ActivityVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\ActivityVoter
|
||||
*/
|
||||
class ActivityVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(ActivityVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new Activity(), 'view', $result];
|
||||
yield [$user, new Activity(), 'edit', $result];
|
||||
yield [$user, new Activity(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Activity(), 'view', $result];
|
||||
yield [$user, new Activity(), 'edit', $result];
|
||||
yield [$user, new Activity(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Activity(), 'view_activity', $result];
|
||||
yield [$user, new Activity(), 'edit_activity', $result];
|
||||
yield [$user, new Activity(), 'delete_activity', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
66
tests/Voter/CustomerVoterTest.php
Normal file
66
tests/Voter/CustomerVoterTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Voter\CustomerVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\CustomerVoter
|
||||
*/
|
||||
class CustomerVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(CustomerVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new Customer(), 'view', $result];
|
||||
yield [$user, new Customer(), 'edit', $result];
|
||||
yield [$user, new Customer(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Customer(), 'view', $result];
|
||||
yield [$user, new Customer(), 'edit', $result];
|
||||
yield [$user, new Customer(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Customer(), 'view_customer', $result];
|
||||
yield [$user, new Customer(), 'edit_customer', $result];
|
||||
yield [$user, new Customer(), 'delete_customer', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
66
tests/Voter/InvoiceTemplateVoterTest.php
Normal file
66
tests/Voter/InvoiceTemplateVoterTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use App\Voter\InvoiceTemplateVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\InvoiceTemplateVoter
|
||||
*/
|
||||
class InvoiceTemplateVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(InvoiceTemplateVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view_invoice_template', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit_invoice_template', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete_invoice_template', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +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\Tests\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Voter\InvoiceVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\InvoiceVoter
|
||||
*/
|
||||
class InvoiceVoterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $isAuthenticated, $hasRole, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($isAuthenticated);
|
||||
$accessManager->method('hasRole')->willReturn($hasRole);
|
||||
|
||||
$sut = new InvoiceVoter($accessManager);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, User::ROLE_CUSTOMER);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(1, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(1, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(1, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$users = [$user0, $user1, $user2, $user3, $user4];
|
||||
$attributes = [InvoiceVoter::VIEW, InvoiceVoter::CREATE, InvoiceVoter::EDIT, InvoiceVoter::DELETE];
|
||||
$subjects = ['invoice', 'invoice_template', new InvoiceTemplate()];
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
foreach ($users as $user) {
|
||||
foreach ($subjects as $subject) {
|
||||
yield [$user, false, false, $subject, $attribute, VoterInterface::ACCESS_DENIED];
|
||||
yield [$user, true, false, $subject, $attribute, VoterInterface::ACCESS_DENIED];
|
||||
yield [$user, false, true, $subject, $attribute, VoterInterface::ACCESS_DENIED];
|
||||
yield [$user, true, true, $subject, $attribute, VoterInterface::ACCESS_GRANTED];
|
||||
yield [$user, true, true, $subject, 'something', VoterInterface::ACCESS_ABSTAIN];
|
||||
}
|
||||
yield [$user, true, true, new \stdClass(), $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
yield [$user, false, true, new \stdClass(), $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
yield [$user, true, false, new \stdClass(), $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
yield [$user, false, false, new \stdClass(), $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
yield [$user, true, true, null, $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
yield [$user, true, true, 'foo', $attribute, VoterInterface::ACCESS_ABSTAIN];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUser($id, $role)
|
||||
{
|
||||
$user = $this->getMockBuilder(User::class)->getMock();
|
||||
$user->method('getId')->willReturn($id);
|
||||
$user->method('getRoles')->willReturn([$role]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
67
tests/Voter/ProjectVoterTest.php
Normal file
67
tests/Voter/ProjectVoterTest.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\Tests\Voter;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Voter\ProjectVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\ProjectVoter
|
||||
*/
|
||||
class ProjectVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(ProjectVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new Project(), 'view', $result];
|
||||
yield [$user, new Project(), 'edit', $result];
|
||||
yield [$user, new Project(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Project(), 'view', $result];
|
||||
yield [$user, new Project(), 'edit', $result];
|
||||
yield [$user, new Project(), 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new Project(), 'create_project', $result];
|
||||
yield [$user, new Project(), 'view_project', $result];
|
||||
yield [$user, new Project(), 'edit_project', $result];
|
||||
yield [$user, new Project(), 'delete_project', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
133
tests/Voter/RolePermissionVoterTest.php
Normal file
133
tests/Voter/RolePermissionVoterTest.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\User;
|
||||
use App\Voter\RolePermissionVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\RolePermissionVoter
|
||||
*/
|
||||
class RolePermissionVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(RolePermissionVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$invoice = [
|
||||
'create_invoice_template' => null,
|
||||
'view_invoice' => null,
|
||||
'create_invoice' => null,
|
||||
];
|
||||
|
||||
$timesheet = [
|
||||
'view_own_timesheet' => null,
|
||||
'create_own_timesheet' => null,
|
||||
'export_own_timesheet' => null,
|
||||
];
|
||||
|
||||
$timesheetOther = [
|
||||
'view_other_timesheet' => null,
|
||||
'create_other_timesheet' => null,
|
||||
];
|
||||
|
||||
$others = [
|
||||
'create_activity' => null,
|
||||
'create_customer' => null,
|
||||
'create_project' => null,
|
||||
];
|
||||
|
||||
$users = [
|
||||
'create_user' => null,
|
||||
'view_user' => null,
|
||||
];
|
||||
|
||||
// ================== GRANTED ==================
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
|
||||
$entries = array_merge($timesheet);
|
||||
foreach ([$user1, $user2, $user3, $user4] as $user) {
|
||||
foreach ($entries as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
yield [$user, null, $permission, $result];
|
||||
}
|
||||
}
|
||||
|
||||
$entriesAdmin = array_merge($others, $timesheetOther, $invoice, $timesheet);
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
foreach ($entriesAdmin as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
yield [$user, null, $permission, $result];
|
||||
}
|
||||
}
|
||||
|
||||
$entriesSuperAdmin = array_merge($users);
|
||||
foreach ($entriesSuperAdmin as $permission => $entity) {
|
||||
yield [$user4, $entity, $permission, $result];
|
||||
yield [$user4, null, $permission, $result];
|
||||
}
|
||||
|
||||
// ================== DENIED ==================
|
||||
// this test might fail in the future due to the role permissions
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
foreach ($others as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
}
|
||||
}
|
||||
foreach ([$user0, $user1] as $user) {
|
||||
foreach ($invoice as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
}
|
||||
}
|
||||
foreach ([$user0] as $user) {
|
||||
foreach ($timesheet as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
}
|
||||
}
|
||||
foreach ([$user0, $user1] as $user) {
|
||||
foreach ($timesheetOther as $permission => $entity) {
|
||||
yield [$user, $entity, $permission, $result];
|
||||
}
|
||||
}
|
||||
|
||||
// ================== ABSTAIN ==================
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user3, $user4] as $user) {
|
||||
yield [$user, new Activity(), 'view', $result];
|
||||
yield [$user, new Activity(), 'edit', $result];
|
||||
yield [$user, new Activity(), 'delete', $result];
|
||||
yield [$user, new Activity(), 'ROLE_USER', $result];
|
||||
yield [$user, new Activity(), 'ROLE_ADMIN', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, $user, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,51 +11,69 @@ namespace App\Tests\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Voter\TimesheetVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\TimesheetVoter
|
||||
*/
|
||||
class TimesheetVoterTest extends TestCase
|
||||
class TimesheetVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote($user, $roles, $allow, $subject, $attributes, $result)
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $roles);
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(TimesheetVoter::class, $user);
|
||||
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($allow);
|
||||
$accessManager->method('hasRole')->willReturn($allow);
|
||||
|
||||
$sut = new TimesheetVoter($accessManager);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, $attributes));
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, User::ROLE_CUSTOMER);
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
return [
|
||||
[$user0, $user0->getRoles(), false, new Customer(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user0), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
|
||||
[$user2, $user2->getRoles(), true, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
['foo', [], false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
|
||||
[$user2, $user2->getRoles(), true, new Activity(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
|
||||
[$user2, $user2->getRoles(), true, $this->getTimesheet($user2), [TimesheetVoter::VIEW], VoterInterface::ACCESS_GRANTED],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user2), [TimesheetVoter::VIEW], VoterInterface::ACCESS_DENIED],
|
||||
$timesheet1 = $this->getTimesheet($user1);
|
||||
$timesheet2 = $this->getTimesheet($user2);
|
||||
$timesheet3 = $this->getTimesheet($user3);
|
||||
$timesheet4 = $this->getTimesheet($user4);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
$times = [
|
||||
[$user1, $timesheet1],
|
||||
[$user2, $timesheet2],
|
||||
[$user3, $timesheet3],
|
||||
[$user4, $timesheet4],
|
||||
[$user2, $timesheet1],
|
||||
[$user3, $timesheet2],
|
||||
[$user4, $timesheet3],
|
||||
];
|
||||
foreach ($times as $timeEntry) {
|
||||
yield [$timeEntry[0], $timeEntry[1], 'start', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'stop', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'edit', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'delete', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
$times = [
|
||||
[$user1, $timesheet4],
|
||||
];
|
||||
foreach ($times as $timeEntry) {
|
||||
yield [$timeEntry[0], $timeEntry[1], 'start', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'stop', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'edit', $result];
|
||||
yield [$timeEntry[0], $timeEntry[1], 'delete', $result];
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTimesheet($user)
|
||||
@@ -63,6 +81,14 @@ class TimesheetVoterTest extends TestCase
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setUser($user);
|
||||
|
||||
$activity = new Activity();
|
||||
$project = new Project();
|
||||
$activity->setProject($project);
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setActivity($activity);
|
||||
$customer = new Customer();
|
||||
$project->setCustomer($customer);
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
|
||||
87
tests/Voter/UserVoterTest.php
Normal file
87
tests/Voter/UserVoterTest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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\Tests\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use App\Voter\UserVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Voter\UserVoter
|
||||
*/
|
||||
class UserVoterTest extends AbstractVoterTest
|
||||
{
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(UserVoter::class, $user);
|
||||
|
||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$user0 = $this->getUser(0, null);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
$user3 = $this->getUser(3, User::ROLE_ADMIN);
|
||||
$user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$result = VoterInterface::ACCESS_GRANTED;
|
||||
foreach ([$user1, $user2, $user3] as $user) {
|
||||
yield [$user4, $user, 'view', $result];
|
||||
yield [$user4, $user, 'edit', $result];
|
||||
yield [$user4, $user, 'password', $result];
|
||||
yield [$user4, $user, 'roles', $result];
|
||||
yield [$user4, $user, 'preferences', $result];
|
||||
yield [$user4, $user, 'api-token', $result];
|
||||
}
|
||||
|
||||
foreach ([$user1, $user2, $user3] as $user) {
|
||||
yield [$user4, $user, 'delete', $result];
|
||||
}
|
||||
|
||||
foreach ([$user1, $user2, $user3, $user4] as $user) {
|
||||
yield [$user, $user, 'view', $result];
|
||||
yield [$user, $user, 'edit', $result];
|
||||
yield [$user, $user, 'password', $result];
|
||||
yield [$user, $user, 'preferences', $result];
|
||||
yield [$user, $user, 'api-token', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_DENIED;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, $user, 'roles', $result];
|
||||
yield [$user3, $user, 'roles', $result];
|
||||
yield [$user, $user3, 'view', $result];
|
||||
yield [$user, $user3, 'edit', $result];
|
||||
yield [$user, $user3, 'delete', $result];
|
||||
yield [$user, $user4, 'view', $result];
|
||||
yield [$user, $user4, 'edit', $result];
|
||||
yield [$user, $user4, 'delete', $result];
|
||||
yield [$user, $user4, 'hourly-rate', $result];
|
||||
}
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
foreach ([$user0, $user1, $user2] as $user) {
|
||||
yield [$user, new InvoiceTemplate(), 'view', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'edit', $result];
|
||||
yield [$user, new InvoiceTemplate(), 'delete', $result];
|
||||
yield [$user, new \stdClass(), 'view', $result];
|
||||
yield [$user, null, 'edit', $result];
|
||||
yield [$user, null, 'delete', $result];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ For the most parts Kimai usage should be self-explanatory, so we will only cover
|
||||
- [Configurations](configurations.md) - intro into the global application configs
|
||||
- [Calendar](calendar.md) - the Timesheet calendar view
|
||||
- [Dashboard & widgets](dashboard.md) - how to configure widgets and dashboard sections
|
||||
- [Emails](emails.md) - transport configuration and handling of emails
|
||||
- [Emails](emails.md) - transport configuration and handling of emails
|
||||
- [Permissions](permissions.md) - flexible and configurable ROLE based permission system
|
||||
- [Developer](developers.md) - docs about extending Kimai's feature set
|
||||
- [API](developers_api.md) - how to use the JSON API
|
||||
- [Theme settings](theme.md) - theme related settings
|
||||
|
||||
115
var/docs/permissions.md
Normal file
115
var/docs/permissions.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Permissions
|
||||
|
||||
Kimai 2 provides a flexible permissions system, which can be adapted though your [local.yaml](configurations.md) config
|
||||
file and that is based on [user roles](users.md).
|
||||
|
||||
## Understanding permission structure
|
||||
|
||||
Before you learn to configure the permission system, you have to understand the three involved config types:
|
||||
|
||||
1. `Permission sets` define a re-usable name for a list of "permission names"
|
||||
2. `Permission maps` will apply a list of "permission sets" to a "user role"
|
||||
3. `Permissions` apply a list of "permission names" to a "user role"
|
||||
|
||||
An example and its explanation:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
sets:
|
||||
ACTIVITY: [view_activity,create_activity]
|
||||
TIMESHEET: [view_own_timesheet,start_own_timesheet]
|
||||
maps:
|
||||
ROLE_USER: [TIMESHEET]
|
||||
ROLE_ADMIN: [TIMESHEET,ACTIVITY]
|
||||
roles:
|
||||
ROLE_USER: [my_profile]
|
||||
ROLE_ADMIN: [my_profile,start_other_timesheet]
|
||||
```
|
||||
|
||||
In `sets` we define the two `permissions sets` names "ACTIVITY" and "TIMESHEET". In `maps` we apply the `permissions set` to the
|
||||
called "TIMESHEET" to the user-role "ROLE_USER" and the two `permissions set` called "TIMESHEET" and "ACTIVITY" to the user-role "ROLE_ADMIN".
|
||||
|
||||
At this step the role have the following permissions:
|
||||
|
||||
- `ROLE_USER`: view_own_timesheet,start_own_timesheet
|
||||
- `ROLE_ADMIN`: view_own_timesheet,start_own_timesheet,view_activity,create_activity
|
||||
|
||||
As last step, the list of `permission names` will be added to the list of calculated permissions.
|
||||
So we add the permission "my_profile" to the user-role "ROLE_USER" and the two permissions "my_profile" and "start_other_timesheet" to the user-role "ROLE_ADMIN".
|
||||
|
||||
At the end the system calculated the final list of permissions:
|
||||
|
||||
- `ROLE_USER`: view_own_timesheet,start_own_timesheet,my_profile
|
||||
- `ROLE_ADMIN`: view_own_timesheet,start_own_timesheet,view_activity,create_activity,my_profile,start_other_timesheet
|
||||
|
||||
## Existing permissions
|
||||
|
||||
The permission-names were chosen to be self-explanatory. In the hope that it worked, here is the full list of existing permissions:
|
||||
|
||||
| Permission name | Set name | API use | Description |
|
||||
|---|---|---|---|
|
||||
| view_activity | ACTIVITIES | | allows access to the activity administration |
|
||||
| create_activity | ACTIVITIES | | - |
|
||||
| edit_activity | ACTIVITIES | | - |
|
||||
| delete_activity | ACTIVITIES | | - |
|
||||
| view_project | PROJECTS | | allows access to the project administration |
|
||||
| create_project | PROJECTS | | - |
|
||||
| edit_project | PROJECTS | | - |
|
||||
| delete_project | PROJECTS | | - |
|
||||
| view_customer | CUSTOMERS | | allows access to the customer administration |
|
||||
| create_customer | CUSTOMERS | | - |
|
||||
| edit_customer | CUSTOMERS | | - |
|
||||
| delete_customer | CUSTOMERS | | - |
|
||||
| view_invoice | INVOICE | | allows access to the invoice section |
|
||||
| create_invoice | INVOICE | | - |
|
||||
| view_invoice_template | INVOICE_TEMPLATE | | allows access to the invoice and invoice template section |
|
||||
| create_invoice_template | INVOICE_TEMPLATE | | - |
|
||||
| edit_invoice_template | INVOICE_TEMPLATE | | - |
|
||||
| delete_invoice_template | INVOICE_TEMPLATE | | - |
|
||||
| view_own_timesheet | TIMESHEET | X | - |
|
||||
| start_own_timesheet | TIMESHEET | | - |
|
||||
| stop_own_timesheet | TIMESHEET | | - |
|
||||
| create_own_timesheet | TIMESHEET | X | - |
|
||||
| edit_own_timesheet | TIMESHEET | | - |
|
||||
| export_own_timesheet | TIMESHEET | | - |
|
||||
| delete_own_timesheet | TIMESHEET | | - |
|
||||
| view_other_timesheet | TIMESHEET_OTHER | | allows access to the complete timesheet view |
|
||||
| start_other_timesheet | TIMESHEET_OTHER | | - |
|
||||
| stop_other_timesheet | TIMESHEET_OTHER | | - |
|
||||
| create_other_timesheet | TIMESHEET_OTHER | | - |
|
||||
| edit_other_timesheet | TIMESHEET_OTHER | | - |
|
||||
| delete_other_timesheet | TIMESHEET_OTHER | | - |
|
||||
| view_rate_own_timesheet | RATE | | - |
|
||||
| edit_rate_own_timesheet | RATE | | - |
|
||||
| view_rate_other_timesheet | RATE_OTHER | | - |
|
||||
| edit_rate_other_timesheet | RATE_OTHER | | - |
|
||||
| view_own_profile | PROFILE | | Allows access to the own profile view. Without this permission, users cannot access any of their profile settings or passwords ... |
|
||||
| edit_own_profile | PROFILE | | - |
|
||||
| delete_own_profile | PROFILE | | - |
|
||||
| password_own_profile | PROFILE | | - |
|
||||
| roles_own_profile | PROFILE | | - |
|
||||
| preferences_own_profile | PROFILE | | - |
|
||||
| api-token_own_profile | PROFILE | | - |
|
||||
| view_other_profile | PROFILE_OTHER | | - |
|
||||
| edit_other_profile | PROFILE_OTHER | | - |
|
||||
| delete_other_profile | PROFILE_OTHER | | - |
|
||||
| password_other_profile | PROFILE_OTHER | | allows to change the password for another user |
|
||||
| roles_other_profile | PROFILE_OTHER | | allows to change roles for other users |
|
||||
| preferences_other_profile | PROFILE_OTHER | | allows to change the preferences for another user |
|
||||
| api-token_other_profile | PROFILE_OTHER | | allows to set the API login token for other users |
|
||||
| hourly-rate_own_profile | - | | allows to edit the own user specific hourly rate |
|
||||
| hourly-rate_other_profile | - | | allows to edit other users specific hourly rate |
|
||||
| view_user | USER | X | allows to access the User administration and see the list of all users |
|
||||
| create_user | USER | | allows to create new users |
|
||||
| delete_user | USER | | allows to delete existing users |
|
||||
|
||||
## Configure permissions
|
||||
|
||||
Knowing that many companies need a different combination of allowed permissions than the default ones, you might also
|
||||
want to change the pre-configured permission.
|
||||
|
||||
You can do that in your [local.yaml](configurations.md). Define the permissions like we did in the above mentioned example,
|
||||
you might start by copying the default permissions from `kimai.yaml`.
|
||||
|
||||
Be aware: if you configure your own permission definition, you have to overwrite the complete
|
||||
node (`sets`, `maps` or`roles`) that you edited and define all SETS and/or ROLES.
|
||||
@@ -1,16 +1,20 @@
|
||||
# Users
|
||||
|
||||
## Roles & Permissions
|
||||
## Roles
|
||||
|
||||
There are multiple pre-defined roles in Kimai, which define the ACLs. A user can only inherit one role, where the roles extend each user.
|
||||
There are multiple pre-defined roles in Kimai, which define the ACLs/permissions.
|
||||
|
||||
| Role name | extends | Gives permission for |
|
||||
| Role name | extends | Description |
|
||||
|---|---|---|
|
||||
| ROLE_CUSTOMER | - | Currently has no permissions, but was reserved for future functionality |
|
||||
| ROLE_USER | ROLE_CUSTOMER | Time-tracking |
|
||||
| ROLE_TEAMLEAD | ROLE_USER | All of the above, plus: editing other users timesheets |
|
||||
| ROLE_ADMIN | ROLE_TEAMLEAD | All of the above, plus: editing customers, editing projects, editing activities |
|
||||
| ROLE_SUPER_ADMIN | ROLE_ADMIN | All of the above, plus: editing users |
|
||||
| ROLE_CUSTOMER | - | Currently not used, reserved for future features |
|
||||
| ROLE_USER | ROLE_CUSTOMER | Normal user that wants to track working times |
|
||||
| ROLE_TEAMLEAD | ROLE_USER | This role manages teams of ROLE_USER (this feature is not yet implemented, but planned for the future) and has further permissions on invoices |
|
||||
| ROLE_ADMIN | ROLE_TEAMLEAD | Admins can do almost everything in Kimai, except some user specific tasks |
|
||||
| ROLE_SUPER_ADMIN | ROLE_ADMIN | Evey Super-Admin can do anything Kimai |
|
||||
|
||||
### Permissions
|
||||
|
||||
The permission system is configurable through a configuration file. You can find further information in the [permissions](permissions.md) chapter.
|
||||
|
||||
## Login
|
||||
|
||||
|
||||
Reference in New Issue
Block a user