improve permission checks for timesheets with activated lockdown (#2271)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -89,11 +90,17 @@ class RolePermissionManagerTest extends TestCase
|
||||
'USER_ROLE' => ['foo', 'bar']
|
||||
]);
|
||||
|
||||
$user = new User();
|
||||
$user->addRole('TEST_ROLE');
|
||||
$user->addRole('FFOOOOOO');
|
||||
|
||||
self::assertTrue($sut->isRegisteredPermission('foo'));
|
||||
self::assertTrue($sut->isRegisteredPermission('bar'));
|
||||
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->hasRolePermission($user, 'foo2'));
|
||||
self::assertFalse($sut->hasRolePermission($user, 'foo'));
|
||||
self::assertFalse($sut->hasPermission('TEST_ROLE', 'foo'));
|
||||
self::assertFalse($sut->hasPermission('USER_ROLE', 'foo'));
|
||||
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\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\LockdownService;
|
||||
use App\Validator\Constraints\TimesheetLockdown;
|
||||
use App\Validator\Constraints\TimesheetLockdownValidator;
|
||||
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()
|
||||
|
||||
@@ -11,29 +11,18 @@ namespace App\Tests\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Voter\AbstractVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
abstract class AbstractVoterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param string $voterClass
|
||||
* @param User $user
|
||||
* @return AbstractVoter
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function getVoter(string $voterClass, User $user)
|
||||
protected function getVoter(string $voterClass): Voter
|
||||
{
|
||||
$isAuthenticated = empty($user->getRoles());
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($isAuthenticated);
|
||||
|
||||
$class = new \ReflectionClass($voterClass);
|
||||
/** @var AbstractVoter $voter */
|
||||
$voter = $class->newInstance($accessManager, $this->getRolePermissionManager());
|
||||
self::assertInstanceOf(AbstractVoter::class, $voter);
|
||||
/** @var Voter $voter */
|
||||
$voter = $class->newInstance($this->getRolePermissionManager());
|
||||
self::assertInstanceOf(Voter::class, $voter);
|
||||
|
||||
return $voter;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class ActivityVoterTest extends AbstractVoterTest
|
||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$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]));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class CustomerVoterTest extends AbstractVoterTest
|
||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$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]);
|
||||
$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)
|
||||
{
|
||||
$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()) {
|
||||
$subject->setCustomer(new Customer());
|
||||
|
||||
@@ -26,7 +26,7 @@ class RolePermissionVoterTest extends AbstractVoterTest
|
||||
public function testVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$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]);
|
||||
$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)
|
||||
{
|
||||
$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]));
|
||||
}
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
|
||||
namespace App\Tests\Voter;
|
||||
|
||||
use App\Configuration\ConfigLoaderInterface;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\LockdownService;
|
||||
use App\Voter\TimesheetVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
@@ -23,10 +27,15 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
*/
|
||||
class TimesheetVoterTest extends AbstractVoterTest
|
||||
{
|
||||
protected function getVoter(string $voterClass): Voter
|
||||
{
|
||||
return $this->getLockdownVoter();
|
||||
}
|
||||
|
||||
protected function assertVote(User $user, $subject, $attribute, $result)
|
||||
{
|
||||
$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]));
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
@@ -154,10 +193,30 @@ class TimesheetVoterTest extends AbstractVoterTest
|
||||
*/
|
||||
protected function getUser($id, $role)
|
||||
{
|
||||
$user = $this->getMockBuilder(User::class)->getMock();
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')->willReturn($id);
|
||||
$user->method('getRoles')->willReturn([$role]);
|
||||
$user->method('getTimezone')->willReturn(date_default_timezone_get());
|
||||
|
||||
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)
|
||||
{
|
||||
$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]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user