added new permission to separate time and money budget (#3352)

This commit is contained in:
Kevin Papst
2022-06-11 12:23:02 +02:00
committed by GitHub
parent 3f827b3104
commit b46fdcefad
47 changed files with 853 additions and 346 deletions

View File

@@ -212,6 +212,7 @@ class ActivityController extends BaseApiController
$form = $this->createForm(ActivityApiEditForm::class, $activity, [
'include_budget' => $this->isGranted('budget', $activity),
'include_time' => $this->isGranted('time', $activity),
]);
$form->submit($request->request->all());
@@ -276,6 +277,7 @@ class ActivityController extends BaseApiController
$form = $this->createForm(ActivityApiEditForm::class, $activity, [
'include_budget' => $this->isGranted('budget', $activity),
'include_time' => $this->isGranted('time', $activity),
]);
$form->setData($activity);

View File

@@ -182,6 +182,7 @@ class CustomerController extends BaseApiController
$form = $this->createForm(CustomerApiEditForm::class, $customer, [
'include_budget' => $this->isGranted('budget', $customer),
'include_time' => $this->isGranted('time', $customer),
]);
$form->submit($request->request->all());
@@ -246,6 +247,7 @@ class CustomerController extends BaseApiController
$form = $this->createForm(CustomerApiEditForm::class, $customer, [
'include_budget' => $this->isGranted('budget', $customer),
'include_time' => $this->isGranted('time', $customer),
]);
$form->setData($customer);

View File

@@ -226,6 +226,7 @@ class ProjectController extends BaseApiController
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'date_format' => self::DATE_FORMAT,
'include_budget' => $this->isGranted('budget', $project),
'include_time' => $this->isGranted('time', $project),
]);
$form->submit($request->request->all());
@@ -292,6 +293,7 @@ class ProjectController extends BaseApiController
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'date_format' => self::DATE_FORMAT,
'include_budget' => $this->isGranted('budget', $project),
'include_time' => $this->isGranted('time', $project),
]);
$form->setData($project);

View File

@@ -133,7 +133,7 @@ final class ActivityController extends AbstractController
$rates = $rateRepository->getRatesForActivity($activity);
}
if ($this->isGranted('budget', $activity)) {
if ($this->isGranted('budget', $activity) || $this->isGranted('time', $activity)) {
$stats = $statisticService->getBudgetStatisticModel($activity, $now);
}
@@ -424,7 +424,8 @@ final class ActivityController extends AbstractController
'action' => $url,
'method' => 'POST',
'currency' => $currency,
'include_budget' => $this->isGranted('budget', $activity)
'include_budget' => $this->isGranted('budget', $activity),
'include_time' => $this->isGranted('time', $activity),
]);
}
}

View File

@@ -315,7 +315,7 @@ final class CustomerController extends AbstractController
$timezone = new \DateTimeZone($customer->getTimezone());
}
if ($this->isGranted('budget', $customer)) {
if ($this->isGranted('budget', $customer) || $this->isGranted('time', $customer)) {
$stats = $statisticService->getBudgetStatisticModel($customer, $now);
}
@@ -530,7 +530,8 @@ final class CustomerController extends AbstractController
return $this->createForm(CustomerEditForm::class, $customer, [
'action' => $url,
'method' => 'POST',
'include_budget' => $this->isGranted('budget', $customer)
'include_budget' => $this->isGranted('budget', $customer),
'include_time' => $this->isGranted('time', $customer),
]);
}
}

View File

@@ -333,7 +333,7 @@ final class ProjectController extends AbstractController
$rates = $rateRepository->getRatesForProject($project);
}
if ($this->isGranted('budget', $project)) {
if ($this->isGranted('budget', $project) || $this->isGranted('time', $project)) {
$stats = $statisticService->getBudgetStatisticModel($project, $now);
}
@@ -563,6 +563,7 @@ final class ProjectController extends AbstractController
'currency' => $currency,
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
'include_budget' => $this->isGranted('budget', $project),
'include_time' => $this->isGranted('time', $project),
'time_increment' => 15,
]);
}

View File

@@ -22,7 +22,7 @@ final class ProjectDateRangeController extends AbstractController
{
/**
* @Route(path="/reporting/project_daterange", name="report_project_daterange", methods={"GET","POST"})
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
*/
public function __invoke(Request $request, ProjectStatisticService $service)
{

View File

@@ -22,7 +22,7 @@ final class ProjectDetailsController extends AbstractController
{
/**
* @Route(path="/reporting/project_details", name="report_project_details", methods={"GET"})
* @Security("is_granted('view_reporting') and (is_granted('details_project') or is_granted('details_teamlead_project') or is_granted('details_team_project'))")
* @Security("is_granted('view_reporting') and is_granted('details', 'project')")
*/
public function __invoke(Request $request, ProjectStatisticService $service)
{

View File

@@ -21,7 +21,7 @@ final class ProjectInactiveController extends AbstractController
{
/**
* @Route(path="/reporting/project_inactive", name="report_project_inactive", methods={"GET","POST"})
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
*/
public function __invoke(Request $request, ProjectStatisticService $service)
{

View File

@@ -21,7 +21,7 @@ final class ProjectViewController extends AbstractController
{
/**
* @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"})
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
* @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')")
*/
public function __invoke(Request $request, ProjectStatisticService $service)
{

View File

@@ -78,7 +78,7 @@ class CustomerSubscriber extends AbstractActionsSubscriber
$event->addDelete($this->path('admin_customer_delete', ['id' => $customer->getId()]));
}
if ($this->isGranted('view_reporting') && $this->isGranted('budget_project')) {
if ($this->isGranted('view_reporting') && $this->isGranted('budget_any', 'project')) {
$event->addAction('report_project_view', ['url' => $this->path('report_project_view', ['customer' => $customer->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']);
}
}

View File

@@ -97,6 +97,7 @@ class ActivityEditForm extends AbstractType
'customer' => false,
'currency' => Customer::DEFAULT_CURRENCY,
'include_budget' => false,
'include_time' => false,
'attr' => [
'data-form-event' => 'kimai.activityUpdate'
],

View File

@@ -114,6 +114,7 @@ class CustomerEditForm extends AbstractType
'csrf_token_id' => 'admin_customer_edit',
'currency' => Customer::DEFAULT_CURRENCY,
'include_budget' => false,
'include_time' => false,
'attr' => [
'data-form-event' => 'kimai.customerUpdate'
],

View File

@@ -26,22 +26,30 @@ trait EntityFormTrait
{
$this->addColor($builder);
if ($options['include_budget']) {
$builder
->add('budget', MoneyType::class, [
'empty_data' => '0.00',
'label' => 'label.budget',
'required' => false,
'currency' => $options['currency'],
])
->add('timeBudget', DurationType::class, [
'empty_data' => 0,
'label' => 'label.timeBudget',
'icon' => 'clock',
'required' => false,
])
->add('budgetType', BudgetType::class)
;
$showMoney = $options['include_budget'];
$showTime = $options['include_time'];
$showBudget = $showMoney || $showTime;
if ($showMoney) {
$builder->add('budget', MoneyType::class, [
'empty_data' => '0.00',
'label' => 'label.budget',
'required' => false,
'currency' => $options['currency'],
]);
}
if ($showTime) {
$builder->add('timeBudget', DurationType::class, [
'empty_data' => 0,
'label' => 'label.timeBudget',
'icon' => 'clock',
'required' => false,
]);
}
if ($showBudget) {
$builder->add('budgetType', BudgetType::class);
}
$builder->add('metaFields', MetaFieldsCollectionType::class);

View File

@@ -112,6 +112,7 @@ class ProjectEditForm extends AbstractType
'currency' => Customer::DEFAULT_CURRENCY,
'date_format' => null,
'include_budget' => false,
'include_time' => false,
'timezone' => date_default_timezone_get(),
'time_increment' => 1,
'attr' => [

View File

@@ -42,25 +42,28 @@ final class ReportingService
$event = new ReportingEvent($user);
if ($this->security->isGranted('view_reporting')) {
$showBudget = $this->security->isGranted('budget_any', 'project');
$details = $this->security->isGranted('details', 'project');
$viewOther = $this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet');
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user'));
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user'));
$event->addReport(new Report('year_by_user', 'report_user_year', 'report_user_year', 'user'));
if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) {
if ($viewOther) {
$event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'users'));
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'users'));
$event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users'));
}
if ($this->security->isGranted('budget_project')) {
if ($showBudget) {
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
}
if ($this->security->isGranted('details_project') || $this->security->isGranted('details_teamlead_project') || $this->security->isGranted('details_team_project')) {
if ($details) {
$event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project'));
}
if ($this->security->isGranted('budget_project')) {
if ($showBudget) {
$event->addReport(new Report('daterange_projects', 'report_project_daterange', 'report_project_daterange', 'project'));
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
}
if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) {
if ($viewOther) {
$event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer'));
}

View File

@@ -14,4 +14,5 @@ final class TimesheetBudgetUsed extends TimesheetConstraint
// same messages, so we can re-use the validation translation!
public $messageRate = 'The budget is completely used.';
public $messageTime = 'The budget is completely used.';
public $messagePermission = 'Sorry, the budget is used up.';
}

View File

@@ -20,6 +20,7 @@ use App\Timesheet\RateServiceInterface;
use App\Utils\Duration;
use App\Utils\LocaleHelper;
use DateTime;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -32,8 +33,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
private $timesheetRepository;
private $rateService;
private $configuration;
private $security;
public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService)
public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService, AuthorizationCheckerInterface $security)
{
$this->configuration = $configuration;
$this->customerStatisticService = $customerStatisticService;
@@ -41,6 +43,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$this->activityStatisticService = $activityStatisticService;
$this->timesheetRepository = $timesheetRepository;
$this->rateService = $rateService;
$this->security = $security;
}
/**
@@ -181,7 +184,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$free = $budget - $rate;
$free = max($free, 0);
$this->context->buildViolation($constraint->messageRate)
$message = $constraint->messageRate;
if (!$this->security->isGranted('budget_money', $field)) {
$message = $constraint->messagePermission;
}
$this->context->buildViolation($message)
->atPath($field)
->setTranslationDomain('validators')
->setParameters([
@@ -200,7 +208,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
$free = $budget - $duration;
$free = max($free, 0);
$this->context->buildViolation($constraint->messageTime)
$message = $constraint->messageTime;
if (!$this->security->isGranted('budget_time', $field)) {
$message = $constraint->messagePermission;
}
$this->context->buildViolation($message)
->atPath($field)
->setTranslationDomain('validators')
->setParameters([

View File

@@ -28,6 +28,7 @@ final class ActivityVoter extends Voter
'view',
'edit',
'budget',
'time',
'delete',
'permissions',
];

View File

@@ -29,6 +29,7 @@ final class CustomerVoter extends Voter
'create',
'edit',
'budget',
'time',
'delete',
'permissions',
'comments',

View File

@@ -0,0 +1,121 @@
<?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\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Security\RolePermissionManager;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class EntityMultiRoleVoter extends Voter
{
/**
* support rules based on the given activity
*/
private const ALLOWED_ATTRIBUTES = [
'budget_money',
'budget_time',
'budget_any',
'details',
];
private const ALLOWED_SUBJECTS = [
'customer',
'project',
'activity',
];
private $permissionManager;
public function __construct(RolePermissionManager $permissionManager)
{
$this->permissionManager = $permissionManager;
}
/**
* @param string $attribute
* @param Activity|Project|Customer|string $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
return true;
}
if ($subject instanceof Activity || $subject instanceof Project || $subject instanceof Customer) {
return true;
}
return false;
}
/**
* @param string $attribute
* @param Activity|Project|Customer|string $subject
* @param TokenInterface $token
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
$suffix = null;
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
$suffix = $subject;
} elseif ($subject instanceof Activity) {
$suffix = 'activity';
} elseif ($subject instanceof Project) {
$suffix = 'project';
} elseif ($subject instanceof Customer) {
$suffix = 'customer';
}
if ($suffix === null) {
return false;
}
$permissions = [];
if ($attribute === 'details') {
$permissions[] = 'details';
}
if ($attribute === 'budget_money' || $attribute === 'budget_any') {
$permissions[] = 'budget';
$permissions[] = 'budget_teamlead';
$permissions[] = 'budget_team';
}
if ($attribute === 'budget_time' || $attribute === 'budget_any') {
$permissions[] = 'time';
$permissions[] = 'time_teamlead';
$permissions[] = 'time_team';
}
foreach ($permissions as $permission) {
if ($this->permissionManager->hasRolePermission($user, $permission . '_' . $suffix)) {
return true;
}
}
return false;
}
}

View File

@@ -28,6 +28,7 @@ final class ProjectVoter extends Voter
'view',
'edit',
'budget',
'time',
'delete',
'permissions',
'comments',

View File

@@ -83,7 +83,10 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWid
*/
public function getPermissions(): array
{
return ['budget_team_project', 'budget_teamlead_project', 'budget_project'];
return [
'budget_team_project', 'budget_teamlead_project', 'budget_project',
'time_team_project', 'time_teamlead_project', 'time_project',
];
}
public function setUser(User $user): void