added invoice voter (#316)
This commit is contained in:
@@ -38,5 +38,5 @@ filter:
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9.30)'
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.70)'
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < 0.01)'
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.73)'
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
|
||||
@@ -57,8 +57,9 @@ cd kimai2/
|
||||
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html) and create your `.env` file:
|
||||
```bash
|
||||
chown -R www-data var/
|
||||
chmod -R 777 var/
|
||||
chown -R :www-data .
|
||||
chmod -R g+r .
|
||||
chmod -R g+rw var/
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET"})
|
||||
* @Security("is_granted('view', 'invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -110,6 +111,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"GET"})
|
||||
* @Security("is_granted('create', 'invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -209,8 +211,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"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Security("is_granted('view', 'invoice_template')")
|
||||
*
|
||||
* @param $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -227,8 +228,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Security("is_granted('edit', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
@@ -242,8 +242,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Security("is_granted('create', 'invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -261,9 +260,8 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* The route to delete an existing template.
|
||||
*
|
||||
* TODO permission
|
||||
*
|
||||
* @Route(path="/template/{id}/delete", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
|
||||
61
src/Security/AclDecisionManager.php
Normal file
61
src/Security/AclDecisionManager.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
|
||||
class AclDecisionManager
|
||||
{
|
||||
/**
|
||||
* @var AccessDecisionManagerInterface
|
||||
*/
|
||||
protected $decisionManager;
|
||||
|
||||
/**
|
||||
* AbstractVoter constructor.
|
||||
* @param AccessDecisionManagerInterface $decisionManager
|
||||
*/
|
||||
public function __construct(AccessDecisionManagerInterface $decisionManager)
|
||||
{
|
||||
$this->decisionManager = $decisionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
public function isFullyAuthenticated(TokenInterface $token)
|
||||
{
|
||||
if ($this->decisionManager->decide($token, ['IS_AUTHENTICATED_FULLY'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @param string|array $roles
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRole(TokenInterface $token, $roles)
|
||||
{
|
||||
if (!is_array($roles)) {
|
||||
$roles = [$roles];
|
||||
}
|
||||
|
||||
if ($this->decisionManager->decide($token, $roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Security\AclDecisionManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
@@ -19,15 +19,15 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
abstract class AbstractVoter extends Voter
|
||||
{
|
||||
/**
|
||||
* @var AccessDecisionManagerInterface
|
||||
* @var AclDecisionManager
|
||||
*/
|
||||
protected $decisionManager;
|
||||
|
||||
/**
|
||||
* AbstractVoter constructor.
|
||||
* @param AccessDecisionManagerInterface $decisionManager
|
||||
* @param AclDecisionManager $decisionManager
|
||||
*/
|
||||
public function __construct(AccessDecisionManagerInterface $decisionManager)
|
||||
public function __construct(AclDecisionManager $decisionManager)
|
||||
{
|
||||
$this->decisionManager = $decisionManager;
|
||||
}
|
||||
@@ -38,11 +38,7 @@ abstract class AbstractVoter extends Voter
|
||||
*/
|
||||
protected function isFullyAuthenticated(TokenInterface $token)
|
||||
{
|
||||
if ($this->decisionManager->decide($token, ['IS_AUTHENTICATED_FULLY'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->decisionManager->isFullyAuthenticated($token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,10 +48,6 @@ abstract class AbstractVoter extends Voter
|
||||
*/
|
||||
protected function hasRole($role, TokenInterface $token)
|
||||
{
|
||||
if ($this->decisionManager->decide($token, [$role])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->decisionManager->hasRole($token, [$role]);
|
||||
}
|
||||
}
|
||||
|
||||
136
src/Voter/InvoiceVoter.php
Normal file
136
src/Voter/InvoiceVoter.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
80
tests/Voter/InvoiceVoterTest.php
Normal file
80
tests/Voter/InvoiceVoterTest.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\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;
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ namespace App\Tests\Voter;
|
||||
use App\Entity\Customer;
|
||||
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\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
/**
|
||||
@@ -26,12 +26,13 @@ class TimesheetVoterTest extends TestCase
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testCustomerIsDisallowed(User $user, $allow, $subject, $attributes, $result)
|
||||
public function testVote(User $user, $allow, $subject, $attributes, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
|
||||
$accessManager = $this->getMockBuilder(AccessDecisionManagerInterface::class)->getMock();
|
||||
$accessManager->method('decide')->willReturn($allow);
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($allow);
|
||||
$accessManager->method('hasRole')->willReturn($allow);
|
||||
|
||||
$sut = new TimesheetVoter($accessManager);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user