improve permission checks for timesheets with activated lockdown (#2271)
This commit is contained in:
@@ -90,6 +90,17 @@ final class RolePermissionManager
|
|||||||
return \in_array($permission, $this->permissions[$role]);
|
return \in_array($permission, $this->permissions[$role]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasRolePermission(User $user, string $permission)
|
||||||
|
{
|
||||||
|
foreach ($user->getRoles() as $role) {
|
||||||
|
if ($this->hasPermission($role, $permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only permissions which were registered through the Symfony configuration stack will be returned here.
|
* Only permissions which were registered through the Symfony configuration stack will be returned here.
|
||||||
*
|
*
|
||||||
|
|||||||
96
src/Timesheet/LockdownService.php
Normal file
96
src/Timesheet/LockdownService.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?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\Timesheet;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
|
final class LockdownService
|
||||||
|
{
|
||||||
|
private $configuration;
|
||||||
|
private $isActive;
|
||||||
|
|
||||||
|
public function __construct(SystemConfiguration $configuration)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLockdownActive(): bool
|
||||||
|
{
|
||||||
|
if ($this->isActive === null) {
|
||||||
|
$this->isActive = $this->configuration->isTimesheetLockdownActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does not check if the current user is allowed to edit timesheets in lockdown situations.
|
||||||
|
* This needs to be performed earlier by yourself (see TimesheetVoter or LockdownValidator).
|
||||||
|
*
|
||||||
|
* @param Timesheet $timesheet
|
||||||
|
* @param \DateTime $now
|
||||||
|
* @param bool $allowEditInGracePeriod
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEditable(Timesheet $timesheet, \DateTime $now, bool $allowEditInGracePeriod = false)
|
||||||
|
{
|
||||||
|
if (!$this->isLockdownActive()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheetStart = $timesheet->getBegin();
|
||||||
|
|
||||||
|
if (null === $timesheetStart) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lockedStart = $this->configuration->getTimesheetLockdownPeriodStart();
|
||||||
|
$lockedEnd = $this->configuration->getTimesheetLockdownPeriodEnd();
|
||||||
|
|
||||||
|
$gracePeriod = $this->configuration->getTimesheetLockdownGracePeriod();
|
||||||
|
if (!empty($gracePeriod)) {
|
||||||
|
$gracePeriod = $gracePeriod . ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lockdownStart = new \DateTime($lockedStart, $timesheetStart->getTimezone());
|
||||||
|
$lockdownEnd = new \DateTime($lockedEnd, $timesheetStart->getTimezone());
|
||||||
|
$lockdownGrace = new \DateTime($gracePeriod . $lockdownEnd->format('Y-m-d'), $timesheetStart->getTimezone());
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
// should not happen, but ... if parsing of datetimes fails: skip validation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// misconfiguration detected, skip validation
|
||||||
|
if ($lockdownEnd < $lockdownStart) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate only entries added before the end of lockdown period
|
||||||
|
if ($timesheetStart > $lockdownEnd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// further validate entries inside of the most recent lockdown
|
||||||
|
if ($timesheetStart > $lockdownStart && $timesheetStart < $lockdownEnd) {
|
||||||
|
// if grace period is still in effect, validation succeeds
|
||||||
|
if ($now < $lockdownGrace) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allowEditInGracePeriod) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use App\Security\CurrentUser;
|
|||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
* @deprecated will be removed with 2.0
|
* @deprecated will be removed with 2.0
|
||||||
*/
|
*/
|
||||||
class UserDateTimeFactory extends DateTimeFactory
|
class UserDateTimeFactory extends DateTimeFactory
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
namespace App\Validator\Constraints;
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
|
||||||
use App\Entity\Timesheet as TimesheetEntity;
|
use App\Entity\Timesheet as TimesheetEntity;
|
||||||
|
use App\Timesheet\LockdownService;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\ConstraintValidator;
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
@@ -18,19 +18,13 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
|||||||
|
|
||||||
final class TimesheetLockdownValidator extends ConstraintValidator
|
final class TimesheetLockdownValidator extends ConstraintValidator
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var AuthorizationCheckerInterface
|
|
||||||
*/
|
|
||||||
private $auth;
|
private $auth;
|
||||||
/**
|
private $lockdownService;
|
||||||
* @var SystemConfiguration
|
|
||||||
*/
|
|
||||||
private $configuration;
|
|
||||||
|
|
||||||
public function __construct(AuthorizationCheckerInterface $auth, SystemConfiguration $configuration)
|
public function __construct(AuthorizationCheckerInterface $auth, LockdownService $lockdownService)
|
||||||
{
|
{
|
||||||
$this->auth = $auth;
|
$this->auth = $auth;
|
||||||
$this->configuration = $configuration;
|
$this->lockdownService = $lockdownService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,40 +41,11 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
|||||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
$timesheetStart = $timesheet->getBegin();
|
if (!$this->lockdownService->isLockdownActive()) {
|
||||||
|
|
||||||
if (null === $timesheetStart) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->configuration->isTimesheetLockdownActive()) {
|
if (null === ($timesheetStart = $timesheet->getBegin())) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lockedStart = $this->configuration->getTimesheetLockdownPeriodStart();
|
|
||||||
$lockedEnd = $this->configuration->getTimesheetLockdownPeriodEnd();
|
|
||||||
|
|
||||||
$gracePeriod = $this->configuration->getTimesheetLockdownGracePeriod();
|
|
||||||
if (!empty($gracePeriod)) {
|
|
||||||
$gracePeriod = $gracePeriod . ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$lockdownStart = new \DateTime($lockedStart, $timesheetStart->getTimezone());
|
|
||||||
$lockdownEnd = new \DateTime($lockedEnd, $timesheetStart->getTimezone());
|
|
||||||
$lockdownGrace = new \DateTime($gracePeriod . $lockdownEnd->format('Y-m-d'), $timesheetStart->getTimezone());
|
|
||||||
} catch (\Exception $ex) {
|
|
||||||
// should not happen, but ... if parsing of datetimes fails: skip validation
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// misconfiguration detected, skip validation
|
|
||||||
if ($lockdownEnd < $lockdownStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate only entries added before the end of lockdown period
|
|
||||||
if ($timesheetStart > $lockdownEnd) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +54,8 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$now = new \DateTime('now', $timesheetStart->getTimezone());
|
||||||
|
|
||||||
if (!empty($constraint->now)) {
|
if (!empty($constraint->now)) {
|
||||||
if ($constraint->now instanceof \DateTime) {
|
if ($constraint->now instanceof \DateTime) {
|
||||||
$now = $constraint->now;
|
$now = $constraint->now;
|
||||||
@@ -100,23 +67,12 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($now)) {
|
$allowEditInGracePeriod = $this->auth->isGranted('lockdown_grace_timesheet');
|
||||||
$now = new \DateTime('now', $timesheetStart->getTimezone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// further validate entries inside of the most recent lockdown
|
if ($this->lockdownService->isEditable($timesheet, $now, $allowEditInGracePeriod)) {
|
||||||
if ($timesheetStart > $lockdownStart && $timesheetStart < $lockdownEnd) {
|
|
||||||
// if grace period is still in effect, validation succeeds
|
|
||||||
if ($now < $lockdownGrace) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if user has special role, validation succeeds
|
|
||||||
if ($this->auth->isGranted('lockdown_grace_timesheet')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// raise a violation for all entries before the start of lockdown period
|
// raise a violation for all entries before the start of lockdown period
|
||||||
$this->context->buildViolation('This period is locked, please choose a later date.')
|
$this->context->buildViolation('This period is locked, please choose a later date.')
|
||||||
->atPath('begin')
|
->atPath('begin')
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract voter to help with checking user permissions.
|
* Abstract voter to help with checking user permissions.
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
* @deprecated since 1.13
|
||||||
*/
|
*/
|
||||||
abstract class AbstractVoter extends Voter
|
abstract class AbstractVoter extends Voter
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ namespace App\Voter;
|
|||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check permissions on Activities.
|
* A voter to check permissions on Activities.
|
||||||
*/
|
*/
|
||||||
class ActivityVoter extends AbstractVoter
|
class ActivityVoter extends Voter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* support rules based on the given activity
|
* support rules based on the given activity
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
'view',
|
'view',
|
||||||
'edit',
|
'edit',
|
||||||
'budget',
|
'budget',
|
||||||
@@ -30,6 +32,13 @@ class ActivityVoter extends AbstractVoter
|
|||||||
'permissions',
|
'permissions',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param Activity $subject
|
* @param Activity $subject
|
||||||
@@ -62,7 +71,7 @@ class ActivityVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasRolePermission($user, $attribute . '_activity')) {
|
if ($this->permissionManager->hasRolePermission($user, $attribute . '_activity')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +80,8 @@ class ActivityVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_activity');
|
$hasTeamleadPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_teamlead_activity');
|
||||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_activity');
|
$hasTeamPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_team_activity');
|
||||||
|
|
||||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ namespace App\Voter;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check authorization on Customers.
|
* A voter to check authorization on Customers.
|
||||||
*/
|
*/
|
||||||
class CustomerVoter extends AbstractVoter
|
class CustomerVoter extends Voter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* supported attributes/rules based on the given customer
|
* supported attributes/rules based on the given customer
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
'view',
|
'view',
|
||||||
'create',
|
'create',
|
||||||
'edit',
|
'edit',
|
||||||
@@ -34,6 +36,13 @@ class CustomerVoter extends AbstractVoter
|
|||||||
'details',
|
'details',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param Customer $subject
|
* @param Customer $subject
|
||||||
@@ -66,7 +75,7 @@ class CustomerVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasRolePermission($user, $attribute . '_customer')) {
|
if ($this->permissionManager->hasRolePermission($user, $attribute . '_customer')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +84,8 @@ class CustomerVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_customer');
|
$hasTeamleadPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_teamlead_customer');
|
||||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_customer');
|
$hasTeamPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_team_customer');
|
||||||
|
|
||||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ namespace App\Voter;
|
|||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check permissions on Projects.
|
* A voter to check permissions on Projects.
|
||||||
*/
|
*/
|
||||||
class ProjectVoter extends AbstractVoter
|
class ProjectVoter extends Voter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* support rules based on the given project
|
* support rules based on the given project
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
'view',
|
'view',
|
||||||
'edit',
|
'edit',
|
||||||
'budget',
|
'budget',
|
||||||
@@ -33,6 +35,13 @@ class ProjectVoter extends AbstractVoter
|
|||||||
'details',
|
'details',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param Project $subject
|
* @param Project $subject
|
||||||
@@ -65,7 +74,7 @@ class ProjectVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasRolePermission($user, $attribute . '_project')) {
|
if ($this->permissionManager->hasRolePermission($user, $attribute . '_project')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +83,8 @@ class ProjectVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_project');
|
$hasTeamleadPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_teamlead_project');
|
||||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_project');
|
$hasTeamPermission = $this->permissionManager->hasRolePermission($user, $attribute . '_team_project');
|
||||||
|
|
||||||
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,13 +11,22 @@ namespace App\Voter;
|
|||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check the free-configurable permission from "kimai.permissions".
|
* A voter to check the free-configurable permission from "kimai.permissions".
|
||||||
*/
|
*/
|
||||||
class RolePermissionVoter extends AbstractVoter
|
class RolePermissionVoter extends Voter
|
||||||
{
|
{
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param mixed $subject
|
* @param mixed $subject
|
||||||
@@ -30,7 +39,7 @@ class RolePermissionVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->isRegisteredPermission($attribute);
|
return $this->permissionManager->isRegisteredPermission($attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +56,6 @@ class RolePermissionVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $attribute);
|
return $this->permissionManager->hasRolePermission($user, $attribute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,28 @@ namespace App\Voter;
|
|||||||
|
|
||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
class TeamVoter extends AbstractVoter
|
class TeamVoter extends Voter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* support rules based on the given $subject (here: Team)
|
* support rules based on the given $subject (here: Team)
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
'view',
|
'view',
|
||||||
'edit',
|
'edit',
|
||||||
'delete',
|
'delete',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param Team $subject
|
* @param Team $subject
|
||||||
@@ -56,6 +65,6 @@ class TeamVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $attribute . '_team');
|
return $this->permissionManager->hasRolePermission($user, $attribute . '_team');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ namespace App\Voter;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
|
use App\Timesheet\LockdownService;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check permissions on Timesheets.
|
* A voter to check permissions on Timesheets.
|
||||||
*/
|
*/
|
||||||
class TimesheetVoter extends AbstractVoter
|
class TimesheetVoter extends Voter
|
||||||
{
|
{
|
||||||
public const VIEW = 'view';
|
public const VIEW = 'view';
|
||||||
public const START = 'start';
|
public const START = 'start';
|
||||||
@@ -31,7 +34,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
/**
|
/**
|
||||||
* support rules based on the given $subject (here: Timesheet)
|
* support rules based on the given $subject (here: Timesheet)
|
||||||
*/
|
*/
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
self::VIEW,
|
self::VIEW,
|
||||||
self::START,
|
self::START,
|
||||||
self::STOP,
|
self::STOP,
|
||||||
@@ -44,6 +47,20 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
'duplicate'
|
'duplicate'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
private $lockdownService;
|
||||||
|
|
||||||
|
private $lockdownGrace;
|
||||||
|
private $lockdownOverride;
|
||||||
|
private $editExported;
|
||||||
|
private $now;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager, LockdownService $lockdownService)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
$this->lockdownService = $lockdownService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param mixed $subject
|
* @param mixed $subject
|
||||||
@@ -101,6 +118,9 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
|
if (!$this->canDuplicate($user, $subject)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$permission = self::EDIT;
|
$permission = self::EDIT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -128,7 +148,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
|
|
||||||
$permission .= '_timesheet';
|
$permission .= '_timesheet';
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $permission);
|
return $this->permissionManager->hasRolePermission($user, $permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function canStart(Timesheet $timesheet): bool
|
protected function canStart(Timesheet $timesheet): bool
|
||||||
@@ -158,7 +178,11 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
|
|
||||||
protected function canEdit(User $user, Timesheet $timesheet): bool
|
protected function canEdit(User $user, Timesheet $timesheet): bool
|
||||||
{
|
{
|
||||||
if ($timesheet->isExported() && !$this->hasRolePermission($user, 'edit_exported_timesheet')) {
|
if (!$this->isAllowedExported($user, $timesheet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isAllowedInLockdown($user, $timesheet)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +191,61 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
|
|
||||||
protected function canDelete(User $user, Timesheet $timesheet): bool
|
protected function canDelete(User $user, Timesheet $timesheet): bool
|
||||||
{
|
{
|
||||||
if ($timesheet->isExported() && !$this->hasRolePermission($user, 'edit_exported_timesheet')) {
|
if (!$this->isAllowedExported($user, $timesheet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isAllowedInLockdown($user, $timesheet)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function canDuplicate(User $user, Timesheet $timesheet): bool
|
||||||
|
{
|
||||||
|
if (!$this->isAllowedInLockdown($user, $timesheet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowedExported(User $user, Timesheet $timesheet): bool
|
||||||
|
{
|
||||||
|
if (!$timesheet->isExported()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->editExported === null) {
|
||||||
|
$this->editExported = $this->permissionManager->hasRolePermission($user, 'edit_exported_timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->editExported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowedInLockdown(User $user, Timesheet $timesheet): bool
|
||||||
|
{
|
||||||
|
if (!$this->lockdownService->isLockdownActive()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lockdownOverride === null) {
|
||||||
|
$this->lockdownOverride = $this->permissionManager->hasRolePermission($user, 'lockdown_override_timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lockdownOverride) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lockdownGrace === null) {
|
||||||
|
$this->lockdownGrace = $this->permissionManager->hasRolePermission($user, 'lockdown_grace_timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->now === null) {
|
||||||
|
$this->now = new \DateTime('now', new \DateTimeZone($user->getTimezone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->lockdownService->isEditable($timesheet, $this->now, $this->lockdownGrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,16 @@
|
|||||||
namespace App\Voter;
|
namespace App\Voter;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Security\RolePermissionManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A voter to check permissions on user profiles.
|
* A voter to check permissions on user profiles.
|
||||||
*/
|
*/
|
||||||
class UserVoter extends AbstractVoter
|
class UserVoter extends Voter
|
||||||
{
|
{
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
private const ALLOWED_ATTRIBUTES = [
|
||||||
'view',
|
'view',
|
||||||
'edit',
|
'edit',
|
||||||
'roles',
|
'roles',
|
||||||
@@ -29,6 +31,13 @@ class UserVoter extends AbstractVoter
|
|||||||
'hourly-rate',
|
'hourly-rate',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $permissionManager;
|
||||||
|
|
||||||
|
public function __construct(RolePermissionManager $permissionManager)
|
||||||
|
{
|
||||||
|
$this->permissionManager = $permissionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
* @param mixed $subject
|
* @param mixed $subject
|
||||||
@@ -66,8 +75,10 @@ class UserVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, 'delete_user');
|
return $this->permissionManager->hasRolePermission($user, 'delete_user');
|
||||||
} elseif ($attribute === 'password') {
|
}
|
||||||
|
|
||||||
|
if ($attribute === 'password') {
|
||||||
if (!$subject->isInternalUser()) {
|
if (!$subject->isInternalUser()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -84,6 +95,6 @@ class UserVoter extends AbstractVoter
|
|||||||
|
|
||||||
$permission .= '_profile';
|
$permission .= '_profile';
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $permission);
|
return $this->permissionManager->hasRolePermission($user, $permission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Tests\Security;
|
namespace App\Tests\Security;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
use App\Repository\RolePermissionRepository;
|
use App\Repository\RolePermissionRepository;
|
||||||
use App\Security\RolePermissionManager;
|
use App\Security\RolePermissionManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -89,11 +90,17 @@ class RolePermissionManagerTest extends TestCase
|
|||||||
'USER_ROLE' => ['foo', 'bar']
|
'USER_ROLE' => ['foo', 'bar']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->addRole('TEST_ROLE');
|
||||||
|
$user->addRole('FFOOOOOO');
|
||||||
|
|
||||||
self::assertTrue($sut->isRegisteredPermission('foo'));
|
self::assertTrue($sut->isRegisteredPermission('foo'));
|
||||||
self::assertTrue($sut->isRegisteredPermission('bar'));
|
self::assertTrue($sut->isRegisteredPermission('bar'));
|
||||||
self::assertEquals(['role_permissions', 'view_user', 'create_user', 'foo2', 'foo', 'bar'], array_values($sut->getPermissions()));
|
self::assertEquals(['role_permissions', 'view_user', 'create_user', 'foo2', 'foo', 'bar'], array_values($sut->getPermissions()));
|
||||||
|
|
||||||
self::assertTrue($sut->hasPermission('TEST_ROLE', 'foo2'));
|
self::assertTrue($sut->hasPermission('TEST_ROLE', 'foo2'));
|
||||||
|
self::assertTrue($sut->hasRolePermission($user, 'foo2'));
|
||||||
|
self::assertFalse($sut->hasRolePermission($user, 'foo'));
|
||||||
self::assertFalse($sut->hasPermission('TEST_ROLE', 'foo'));
|
self::assertFalse($sut->hasPermission('TEST_ROLE', 'foo'));
|
||||||
self::assertFalse($sut->hasPermission('USER_ROLE', 'foo'));
|
self::assertFalse($sut->hasPermission('USER_ROLE', 'foo'));
|
||||||
self::assertTrue($sut->hasPermission('USER_ROLE', 'bar'));
|
self::assertTrue($sut->hasPermission('USER_ROLE', 'bar'));
|
||||||
|
|||||||
151
tests/Timesheet/LockdownServiceTest.php
Normal file
151
tests/Timesheet/LockdownServiceTest.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?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\Timesheet;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Timesheet\LockdownService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Timesheet\LockdownService
|
||||||
|
*/
|
||||||
|
class LockdownServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function createService(?string $start, ?string $end, ?string $grace)
|
||||||
|
{
|
||||||
|
$loader = $this->createMock(ConfigLoaderInterface::class);
|
||||||
|
$config = new SystemConfiguration($loader, [
|
||||||
|
'timesheet' => [
|
||||||
|
'rules' => [
|
||||||
|
'lockdown_period_start' => $start,
|
||||||
|
'lockdown_period_end' => $end,
|
||||||
|
'lockdown_grace_period' => $grace,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new LockdownService($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidatorWithoutNowConstraint()
|
||||||
|
{
|
||||||
|
$sut = $this->createService('first day of last month', 'last day of last month', '+10 days');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of last month');
|
||||||
|
$begin->modify('-5 days');
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
|
||||||
|
self::assertFalse($sut->isEditable($timesheet, new \DateTime(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidatorWithEmptyTimesheet()
|
||||||
|
{
|
||||||
|
$sut = $this->createService('first day of last month', 'last day of last month', '+10 days');
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEditable(new Timesheet(), new \DateTime(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidatorWithoutNowStringConstraint()
|
||||||
|
{
|
||||||
|
$sut = $this->createService('first day of last month', 'last day of last month', '+10 days');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of last month');
|
||||||
|
$begin->modify('+5 days');
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEditable($timesheet, new \DateTime('first day of this month'), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidatorWithEndBeforeStartPeriod()
|
||||||
|
{
|
||||||
|
$sut = $this->createService('first day of this month', 'last day of last month', '+10 days');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of last month');
|
||||||
|
$begin->modify('+5 days');
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
|
||||||
|
self::assertTrue($sut->isEditable($timesheet, new \DateTime('first day of this month'), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestData
|
||||||
|
*/
|
||||||
|
public function testLockdown(bool $allowOverwriteGrace, string $beginModifier, string $nowModifier, bool $isViolation)
|
||||||
|
{
|
||||||
|
$sut = $this->createService('first day of last month', 'last day of last month', '+10 days');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of last month');
|
||||||
|
$begin->modify($beginModifier);
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
|
||||||
|
$now = new \DateTime('first day of this month');
|
||||||
|
$now->modify($nowModifier);
|
||||||
|
|
||||||
|
$result = $sut->isEditable($timesheet, $now, $allowOverwriteGrace);
|
||||||
|
if ($isViolation) {
|
||||||
|
self::assertFalse($result);
|
||||||
|
} else {
|
||||||
|
self::assertTrue($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTestData()
|
||||||
|
{
|
||||||
|
// changing before last dockdown period is not allowed
|
||||||
|
yield [false, '-5 days', '+5 days', true];
|
||||||
|
// changing before last dockdown period is not allowed with grace permission
|
||||||
|
yield [true, '-5 days', '+5 days', true];
|
||||||
|
// changing a value in the last lockdown period is allowed during grace period
|
||||||
|
yield [false, '+5 days', '+5 days', false];
|
||||||
|
// changing outside grace period is not allowed
|
||||||
|
yield [false, '+5 days', '+11 days', true];
|
||||||
|
// changing outside grace period is allowed with grace and full permission
|
||||||
|
yield [true, '+5 days', '+11 days', false];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getConfigTestData
|
||||||
|
*/
|
||||||
|
public function testLockdownConfig(bool $allowOverwriteGrace, ?string $lockdownBegin, ?string $lockdownEnd, ?string $grace, bool $isViolation)
|
||||||
|
{
|
||||||
|
$sut = $this->createService($lockdownBegin, $lockdownEnd, $grace);
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of last month');
|
||||||
|
$begin->modify('+5 days');
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
|
||||||
|
$now = new \DateTime('first day of this month');
|
||||||
|
|
||||||
|
$result = $sut->isEditable($timesheet, $now, $allowOverwriteGrace);
|
||||||
|
|
||||||
|
if ($isViolation) {
|
||||||
|
self::assertFalse($result);
|
||||||
|
} else {
|
||||||
|
self::assertTrue($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfigTestData()
|
||||||
|
{
|
||||||
|
yield [false, null, null, null, false];
|
||||||
|
yield [false, '+5 days', null, null, false];
|
||||||
|
yield [false, null, '+5 days', null, false];
|
||||||
|
|
||||||
|
yield [true, 'öööö', '+11 days', null, false];
|
||||||
|
yield [true, '+5 days', '+5 of !!!!', null, false];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace App\Tests\Validator\Constraints;
|
|||||||
use App\Configuration\ConfigLoaderInterface;
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Timesheet\LockdownService;
|
||||||
use App\Validator\Constraints\TimesheetLockdown;
|
use App\Validator\Constraints\TimesheetLockdown;
|
||||||
use App\Validator\Constraints\TimesheetLockdownValidator;
|
use App\Validator\Constraints\TimesheetLockdownValidator;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
@@ -56,7 +57,7 @@ class TimesheetLockdownValidatorTest extends ConstraintValidatorTestCase
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new TimesheetLockdownValidator($auth, $config);
|
return new TimesheetLockdownValidator($auth, new LockdownService($config));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstraintIsInvalid()
|
public function testConstraintIsInvalid()
|
||||||
|
|||||||
@@ -11,29 +11,18 @@ namespace App\Tests\Voter;
|
|||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\RolePermissionRepository;
|
use App\Repository\RolePermissionRepository;
|
||||||
use App\Security\AclDecisionManager;
|
|
||||||
use App\Security\RolePermissionManager;
|
use App\Security\RolePermissionManager;
|
||||||
use App\Voter\AbstractVoter;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
abstract class AbstractVoterTest extends TestCase
|
abstract class AbstractVoterTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
protected function getVoter(string $voterClass): Voter
|
||||||
* @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);
|
|
||||||
|
|
||||||
$class = new \ReflectionClass($voterClass);
|
$class = new \ReflectionClass($voterClass);
|
||||||
/** @var AbstractVoter $voter */
|
/** @var Voter $voter */
|
||||||
$voter = $class->newInstance($accessManager, $this->getRolePermissionManager());
|
$voter = $class->newInstance($this->getRolePermissionManager());
|
||||||
self::assertInstanceOf(AbstractVoter::class, $voter);
|
self::assertInstanceOf(Voter::class, $voter);
|
||||||
|
|
||||||
return $voter;
|
return $voter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ActivityVoterTest extends AbstractVoterTest
|
|||||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(ActivityVoter::class, $user);
|
$sut = $this->getVoter(ActivityVoter::class);
|
||||||
|
|
||||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CustomerVoterTest extends AbstractVoterTest
|
|||||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(CustomerVoter::class, $user);
|
$sut = $this->getVoter(CustomerVoter::class);
|
||||||
|
|
||||||
$actual = $sut->vote($token, $subject, [$attribute]);
|
$actual = $sut->vote($token, $subject, [$attribute]);
|
||||||
$this->assertEquals($result, $actual, sprintf('Failed voting "%s" for User with roles %s.', $attribute, implode(', ', $user->getRoles())));
|
$this->assertEquals($result, $actual, sprintf('Failed voting "%s" for User with roles %s.', $attribute, implode(', ', $user->getRoles())));
|
||||||
|
|||||||
80
tests/Voter/DeprecatedAbstractVoterTest.php
Normal file
80
tests/Voter/DeprecatedAbstractVoterTest.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?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\Voter\AbstractVoter;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Voter\AbstractVoter
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
class DeprecatedAbstractVoterTest extends AbstractVoterTest
|
||||||
|
{
|
||||||
|
protected function getVoter(string $voterClass): Voter
|
||||||
|
{
|
||||||
|
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||||
|
$accessManager->method('isFullyAuthenticated')->willReturn(true);
|
||||||
|
|
||||||
|
$class = new \ReflectionClass($voterClass);
|
||||||
|
/** @var AbstractVoter $voter */
|
||||||
|
$voter = $class->newInstance($accessManager, $this->getRolePermissionManager());
|
||||||
|
self::assertInstanceOf(AbstractVoter::class, $voter);
|
||||||
|
|
||||||
|
return $voter;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||||
|
{
|
||||||
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
|
$sut = $this->getVoter(DeprecatedVoter::class);
|
||||||
|
|
||||||
|
$actual = $sut->vote($token, $subject, [$attribute]);
|
||||||
|
$this->assertEquals($result, $actual, sprintf('Failed voting "%s" for User with roles %s.', $attribute, implode(', ', $user->getRoles())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMuuu()
|
||||||
|
{
|
||||||
|
$userStandard = $this->getUser(1, User::ROLE_USER);
|
||||||
|
$this->assertVote($userStandard, null, 'view_own_timesheet', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeprecatedVoter extends AbstractVoter
|
||||||
|
{
|
||||||
|
protected function supports($attribute, $subject)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||||
|
{
|
||||||
|
if (!$this->isRegisteredPermission($attribute)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasPermission('ROLE_USER', $attribute)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
if (!$this->hasRolePermission($user, $attribute)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isFullyAuthenticated($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ class ProjectVoterTest extends AbstractVoterTest
|
|||||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(ProjectVoter::class, $user);
|
$sut = $this->getVoter(ProjectVoter::class);
|
||||||
|
|
||||||
if ($subject instanceof Project && null === $subject->getCustomer()) {
|
if ($subject instanceof Project && null === $subject->getCustomer()) {
|
||||||
$subject->setCustomer(new Customer());
|
$subject->setCustomer(new Customer());
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class RolePermissionVoterTest extends AbstractVoterTest
|
|||||||
public function testVote(User $user, $subject, $attribute, $result)
|
public function testVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(RolePermissionVoter::class, $user);
|
$sut = $this->getVoter(RolePermissionVoter::class);
|
||||||
|
|
||||||
$actual = $sut->vote($token, $subject, [$attribute]);
|
$actual = $sut->vote($token, $subject, [$attribute]);
|
||||||
$this->assertEquals($result, $actual, sprintf('Failed voting "%s" for User with roles %s.', $attribute, implode(', ', $user->getRoles())));
|
$this->assertEquals($result, $actual, sprintf('Failed voting "%s" for User with roles %s.', $attribute, implode(', ', $user->getRoles())));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class TeamVoterTest extends AbstractVoterTest
|
|||||||
public function testVote(User $user, $subject, $attribute, $result)
|
public function testVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(TeamVoter::class, $user);
|
$sut = $this->getVoter(TeamVoter::class);
|
||||||
|
|
||||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,17 @@
|
|||||||
|
|
||||||
namespace App\Tests\Voter;
|
namespace App\Tests\Voter;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Timesheet\LockdownService;
|
||||||
use App\Voter\TimesheetVoter;
|
use App\Voter\TimesheetVoter;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,10 +27,15 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
|||||||
*/
|
*/
|
||||||
class TimesheetVoterTest extends AbstractVoterTest
|
class TimesheetVoterTest extends AbstractVoterTest
|
||||||
{
|
{
|
||||||
|
protected function getVoter(string $voterClass): Voter
|
||||||
|
{
|
||||||
|
return $this->getLockdownVoter();
|
||||||
|
}
|
||||||
|
|
||||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(TimesheetVoter::class, $user);
|
$sut = $this->getVoter(TimesheetVoter::class);
|
||||||
|
|
||||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
@@ -87,6 +96,36 @@ class TimesheetVoterTest extends AbstractVoterTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getLockDownTestData
|
||||||
|
*/
|
||||||
|
public function testWithLockdown(string $permission, int $expected, string $beginModifier, string $lockdownBegin, string $lockdownEnd, ?string $lockdownGrace)
|
||||||
|
{
|
||||||
|
$user = $this->getUser(1, User::ROLE_USER);
|
||||||
|
|
||||||
|
$begin = new \DateTime('now');
|
||||||
|
$begin->modify($beginModifier);
|
||||||
|
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin($begin);
|
||||||
|
$timesheet->setUser($user);
|
||||||
|
|
||||||
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
|
$sut = $this->getLockdownVoter($lockdownBegin, $lockdownEnd, $lockdownGrace);
|
||||||
|
|
||||||
|
self::assertEquals($expected, $sut->vote($token, $timesheet, [$permission]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLockDownTestData()
|
||||||
|
{
|
||||||
|
yield ['view', VoterInterface::ACCESS_GRANTED, '+1 days', 'first day of this month', 'last day of this month', '+10 days'];
|
||||||
|
yield ['duplicate', VoterInterface::ACCESS_GRANTED, '+1 days', 'first day of this month', 'last day of this month', '+10 days'];
|
||||||
|
yield ['delete', VoterInterface::ACCESS_GRANTED, '+1 days', 'first day of this month', 'last day of this month', '+10 days'];
|
||||||
|
yield ['edit', VoterInterface::ACCESS_DENIED, '-50 days', 'first day of last month', 'last day of last month', '+1 days'];
|
||||||
|
yield ['duplicate', VoterInterface::ACCESS_DENIED, '-50 days', 'first day of last month', 'last day of last month', '+1 days'];
|
||||||
|
yield ['delete', VoterInterface::ACCESS_DENIED, '-50 days', 'first day of last month', 'last day of last month', '+1 days'];
|
||||||
|
}
|
||||||
|
|
||||||
public function testSpecialCases()
|
public function testSpecialCases()
|
||||||
{
|
{
|
||||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||||
@@ -154,10 +193,30 @@ class TimesheetVoterTest extends AbstractVoterTest
|
|||||||
*/
|
*/
|
||||||
protected function getUser($id, $role)
|
protected function getUser($id, $role)
|
||||||
{
|
{
|
||||||
$user = $this->getMockBuilder(User::class)->getMock();
|
$user = $this->createMock(User::class);
|
||||||
$user->method('getId')->willReturn($id);
|
$user->method('getId')->willReturn($id);
|
||||||
$user->method('getRoles')->willReturn([$role]);
|
$user->method('getRoles')->willReturn([$role]);
|
||||||
|
$user->method('getTimezone')->willReturn(date_default_timezone_get());
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getLockdownVoter(?string $lockdownBegin = null, ?string $lockdownEnd = null, ?string $lockdownGrace = null): Voter
|
||||||
|
{
|
||||||
|
$loader = $this->createMock(ConfigLoaderInterface::class);
|
||||||
|
$config = new SystemConfiguration($loader, [
|
||||||
|
'timesheet' => [
|
||||||
|
'rules' => [
|
||||||
|
'lockdown_period_start' => $lockdownBegin,
|
||||||
|
'lockdown_period_end' => $lockdownEnd,
|
||||||
|
'lockdown_grace_period' => $lockdownGrace,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$voter = new TimesheetVoter($this->getRolePermissionManager(), new LockdownService($config));
|
||||||
|
self::assertInstanceOf(Voter::class, $voter);
|
||||||
|
|
||||||
|
return $voter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class UserVoterTest extends AbstractVoterTest
|
|||||||
public function testVote(User $user, $subject, $attribute, $result)
|
public function testVote(User $user, $subject, $attribute, $result)
|
||||||
{
|
{
|
||||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||||
$sut = $this->getVoter(UserVoter::class, $user);
|
$sut = $this->getVoter(UserVoter::class);
|
||||||
|
|
||||||
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
$this->assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user