added configurable permission system (#424)

This commit is contained in:
Kevin Papst
2018-11-26 13:20:32 +01:00
committed by GitHub
parent 0334f6ce86
commit 8fddf627bf
62 changed files with 1831 additions and 794 deletions

View File

@@ -9,12 +9,14 @@
namespace App\Voter;
use App\Entity\User;
use App\Security\AclDecisionManager;
use App\Security\RolePermissionManager;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/**
* Abstract voter to help with checking user roles.
* Abstract voter to help with checking user permissions.
*/
abstract class AbstractVoter extends Voter
{
@@ -22,14 +24,19 @@ abstract class AbstractVoter extends Voter
* @var AclDecisionManager
*/
protected $decisionManager;
/**
* @var RolePermissionManager
*/
protected $roleManager;
/**
* AbstractVoter constructor.
* @param AclDecisionManager $decisionManager
* @param RolePermissionManager $roleManager
*/
public function __construct(AclDecisionManager $decisionManager)
public function __construct(AclDecisionManager $decisionManager, RolePermissionManager $roleManager)
{
$this->decisionManager = $decisionManager;
$this->roleManager = $roleManager;
}
/**
@@ -50,4 +57,39 @@ abstract class AbstractVoter extends Voter
{
return $this->decisionManager->hasRole($token, [$role]);
}
/**
* @param string $role
* @param string $permission
* @return bool
*/
protected function hasPermission($role, $permission)
{
return $this->roleManager->hasPermission($role, $permission);
}
/**
* @param User $user
* @param string $permission
* @return bool
*/
protected function hasRolePermission(User $user, $permission)
{
foreach ($user->getRoles() as $role) {
if ($this->hasPermission($role, $permission)) {
return true;
}
}
return false;
}
/**
* @param string $permission
* @return bool
*/
public function isRegisteredPermission($permission)
{
return $this->roleManager->isRegisteredPermission($permission);
}
}

View File

@@ -22,6 +22,9 @@ class ActivityVoter extends AbstractVoter
public const EDIT = 'edit';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: Activity)
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
@@ -30,16 +33,16 @@ class ActivityVoter extends AbstractVoter
/**
* @param string $attribute
* @param mixed $subject
* @param Activity $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
if (!$subject instanceof Activity) {
return false;
}
if (!$subject instanceof Activity) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -60,54 +63,10 @@ class ActivityVoter extends AbstractVoter
return false;
}
switch ($attribute) {
case self::VIEW:
return $this->canView($subject, $user, $token);
case self::EDIT:
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($token);
if ($subject instanceof Activity) {
return $this->hasRolePermission($user, $attribute . '_activity');
}
return false;
}
/**
* @param Activity $activity
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canView(Activity $activity, User $user, TokenInterface $token)
{
if ($this->canEdit($activity, $user, $token)) {
return true;
}
return false;
}
/**
* @param Activity $activity
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEdit(Activity $activity, User $user, TokenInterface $token)
{
if ($this->canDelete($token)) {
return true;
}
return false;
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canDelete(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -22,6 +22,9 @@ class CustomerVoter extends AbstractVoter
public const EDIT = 'edit';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: Customer)
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
@@ -30,16 +33,16 @@ class CustomerVoter extends AbstractVoter
/**
* @param string $attribute
* @param mixed $subject
* @param Customer $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
if (!$subject instanceof Customer) {
return false;
}
if (!$subject instanceof Customer) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -60,54 +63,10 @@ class CustomerVoter extends AbstractVoter
return false;
}
switch ($attribute) {
case self::VIEW:
return $this->canView($subject, $user, $token);
case self::EDIT:
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($token);
if ($subject instanceof Customer) {
return $this->hasRolePermission($user, $attribute . '_customer');
}
return false;
}
/**
* @param Customer $customer
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canView(Customer $customer, User $user, TokenInterface $token)
{
if ($this->canEdit($customer, $user, $token)) {
return true;
}
return false;
}
/**
* @param Customer $customer
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEdit(Customer $customer, User $user, TokenInterface $token)
{
if ($this->canDelete($token)) {
return true;
}
return false;
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canDelete(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Voter;
use App\Entity\InvoiceTemplate;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* A voter to check permissions on InvoiceTemplateVote.
*/
class InvoiceTemplateVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: InvoiceTemplate)
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE
];
/**
* @param string $attribute
* @param InvoiceTemplate $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!$subject instanceof InvoiceTemplate) {
return false;
}
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
return true;
}
/**
* @param string $attribute
* @param InvoiceTemplate $subject
* @param TokenInterface $token
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($subject instanceof InvoiceTemplate) {
return $this->hasRolePermission($user, $attribute . '_invoice_template');
}
return false;
}
}

View File

@@ -1,136 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Voter;
use App\Entity\InvoiceTemplate;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* A voter to check permissions on Invoices.
*/
class InvoiceVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const CREATE = 'create';
public const DELETE = 'delete';
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::CREATE,
self::EDIT,
self::DELETE
];
public const ALLOWED_SUBJECTS = [
'invoice',
'invoice_template'
];
/**
* @param string $attribute
* @param mixed $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
if (!$subject instanceof InvoiceTemplate) {
if (!is_string($subject) || !in_array($subject, self::ALLOWED_SUBJECTS)) {
return false;
}
}
return true;
}
/**
* @param string $attribute
* @param string|InvoiceTemplate $subject
* @param TokenInterface $token
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
switch ($attribute) {
case self::VIEW:
return $this->canView($user, $token);
case self::CREATE:
return $this->canCreate($user, $token);
case self::EDIT:
return $this->canEdit($user, $token);
case self::DELETE:
return $this->canDelete($token);
}
return false;
}
/**
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canView(User $user, TokenInterface $token)
{
if ($this->canEdit($user, $token)) {
return true;
}
return false;
}
/**
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canCreate(User $user, TokenInterface $token)
{
if ($this->canDelete($token)) {
return true;
}
return false;
}
/**
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEdit(User $user, TokenInterface $token)
{
if ($this->canDelete($token)) {
return true;
}
return false;
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canDelete(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_TEAMLEAD', $token);
}
}

View File

@@ -22,6 +22,9 @@ class ProjectVoter extends AbstractVoter
public const EDIT = 'edit';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: Project)
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
@@ -30,16 +33,16 @@ class ProjectVoter extends AbstractVoter
/**
* @param string $attribute
* @param mixed $subject
* @param Project $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
if (!$subject instanceof Project) {
return false;
}
if (!$subject instanceof Project) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -60,54 +63,10 @@ class ProjectVoter extends AbstractVoter
return false;
}
switch ($attribute) {
case self::VIEW:
return $this->canView($subject, $user, $token);
case self::EDIT:
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($token);
if ($subject instanceof Project) {
return $this->hasRolePermission($user, $attribute . '_project');
}
return false;
}
/**
* @param Project $project
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canView(Project $project, User $user, TokenInterface $token)
{
if ($this->canEdit($project, $user, $token)) {
return true;
}
return false;
}
/**
* @param Project $project
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEdit(Project $project, User $user, TokenInterface $token)
{
if ($this->canDelete($token)) {
return true;
}
return false;
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canDelete(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Voter;
use App\Entity\Activity;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* A voter to check the free-configurable permission from "kimai.permissions".
*/
class RolePermissionVoter extends AbstractVoter
{
/**
* @param string $attribute
* @param mixed $subject
* @return bool
*/
protected function supports($attribute, $subject)
{
// we only work on single strings that have no subject
if (null !== $subject) {
return false;
}
// and which is not neither a user role like USER_ADMIN
// nor an implicit role like IS_REMEMBERED / IS_FULLY_AUTHENTICATED
if (strpos($attribute, 'ROLE_') === false && strpos($attribute, 'IS_') === false) {
return $this->isRegisteredPermission($attribute);
}
return false;
}
/**
* @param string $attribute
* @param Activity $subject
* @param TokenInterface $token
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!($user instanceof User)) {
return false;
}
foreach ($user->getRoles() as $role) {
if ($this->hasPermission($role, $attribute)) {
return true;
}
}
return false;
}
}

View File

@@ -21,16 +21,21 @@ class TimesheetVoter extends AbstractVoter
{
public const START = 'start';
public const STOP = 'stop';
public const VIEW = 'view';
public const EDIT = 'edit';
public const DELETE = 'delete';
public const VIEW_RATE = 'view_rate';
public const EDIT_RATE = 'edit_rate';
/**
* support rules based on the given $subject (here: Timesheet)
*/
public const ALLOWED_ATTRIBUTES = [
self::START,
self::STOP,
self::VIEW,
self::EDIT,
self::DELETE
self::DELETE,
self::VIEW_RATE,
self::EDIT_RATE,
];
/**
@@ -40,11 +45,11 @@ class TimesheetVoter extends AbstractVoter
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
if (!$subject instanceof Timesheet) {
return false;
}
if (!$subject instanceof Timesheet) {
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
return false;
}
@@ -61,40 +66,48 @@ class TimesheetVoter extends AbstractVoter
{
$user = $token->getUser();
if (!$user instanceof User) {
if (!($user instanceof User)) {
return false;
}
switch ($attribute) {
case self::STOP:
return $this->canStop($subject, $user, $token);
case self::START:
return $this->canStart($subject, $user, $token);
case self::VIEW:
return $this->canView($subject, $user, $token);
case self::EDIT:
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($subject, $user, $token);
if (!($subject instanceof Timesheet)) {
return false;
}
return false;
}
$permission = '';
/**
* @param Timesheet $timesheet
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canStop(Timesheet $timesheet, User $user, TokenInterface $token)
{
// if a teamlead stops an entry for another user, check that this user is part of his team
return $this->isOwnOrTeamlead($timesheet, $user, $token);
switch ($attribute) {
case self::START:
if (!$this->canStart($subject, $user, $token)) {
return false;
}
$permission .= $attribute;
break;
case self::VIEW_RATE:
case self::EDIT_RATE:
case self::STOP:
case self::EDIT:
case self::DELETE:
$permission .= $attribute;
break;
default:
return false;
}
$permission .= '_';
// extend me for "team" support later on
if ($subject->getUser()->getId() == $user->getId()) {
$permission .= 'own';
} else {
$permission .= 'other';
}
$permission .= '_timesheet';
return $this->hasRolePermission($user, $permission);
}
/**
@@ -118,65 +131,4 @@ class TimesheetVoter extends AbstractVoter
return true;
}
/**
* @param Timesheet $timesheet
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canView(Timesheet $timesheet, User $user, TokenInterface $token)
{
return $this->isOwnOrTeamlead($timesheet, $user, $token);
}
/**
* @param Timesheet $timesheet
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEdit(Timesheet $timesheet, User $user, TokenInterface $token)
{
return $this->isOwnOrTeamlead($timesheet, $user, $token);
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token)
{
if (!$this->isFullyAuthenticated($token)) {
return false;
}
return $this->isOwnOrAdmin($timesheet, $user, $token);
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function isOwnOrTeamlead(Timesheet $timesheet, User $user, TokenInterface $token)
{
if ($timesheet->getUser()->getId() == $user->getId()) {
return true;
}
return $this->hasRole('ROLE_TEAMLEAD', $token);
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function isOwnOrAdmin(Timesheet $timesheet, User $user, TokenInterface $token)
{
if ($timesheet->getUser()->getId() == $user->getId()) {
return true;
}
return $this->hasRole('ROLE_ADMIN', $token);
}
}

View File

@@ -19,22 +19,22 @@ class UserVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const CREATE = 'create';
public const DELETE = 'delete';
public const PASSWORD = 'password';
public const ROLES = 'roles';
public const PREFERENCES = 'preferences';
public const API_TOKEN = 'api-token';
public const HOURLY_RATE = 'hourly-rate';
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::CREATE,
self::ROLES,
self::PASSWORD,
self::DELETE,
self::PREFERENCES,
self::API_TOKEN,
self::HOURLY_RATE,
];
/**
@@ -48,7 +48,7 @@ class UserVoter extends AbstractVoter
return false;
}
if (!$subject instanceof User) {
if (!($subject instanceof User)) {
return false;
}
@@ -65,66 +65,48 @@ class UserVoter extends AbstractVoter
{
$user = $token->getUser();
if (!$user instanceof User) {
if (!($user instanceof User)) {
return false;
}
$permission = '';
switch ($attribute) {
// special case for the UserController
case self::DELETE:
if (!$this->canDelete($subject, $user, $token)) {
return false;
}
return $this->hasRolePermission($user, 'delete_user');
// used in templates and ProfileController
case self::VIEW:
return $this->canView($subject, $user, $token);
case self::EDIT:
case self::API_TOKEN:
case self::PASSWORD:
return $this->canEdit($subject, $user, $token);
case self::DELETE:
return $this->canDelete($subject, $user, $token);
case self::CREATE: // create actually passes in the current user as $subject, not the new one
case self::ROLES:
return $this->canAdminUsers($token);
case self::PREFERENCES:
return $this->canEditPreferences($subject, $user, $token);
case self::HOURLY_RATE:
$permission .= $attribute;
break;
default:
return false;
}
return false;
}
$permission .= '_';
/**
* @param User $profile
* @param User $user
* @param TokenInterface $token
* @return bool
*/
protected function canEditPreferences(User $profile, User $user, TokenInterface $token)
{
return $profile->getId() === $user->getId();
}
/**
* @param User $profile
* @param User $user
* @return bool
*/
protected function canView(User $profile, User $user, TokenInterface $token)
{
if ($this->canEdit($profile, $user, $token)) {
return true;
// extend me for "team" support later on
if ($subject->getId() == $user->getId()) {
$permission .= 'own';
} else {
$permission .= 'other';
}
return $profile->getId() === $user->getId();
}
$permission .= '_profile';
/**
* @param User $profile
* @param User $user
* @return bool
*/
protected function canEdit(User $profile, User $user, TokenInterface $token)
{
if ($this->canAdminUsers($token)) {
return true;
}
return $profile->getId() === $user->getId();
return $this->hasRolePermission($user, $permission);
}
/**
@@ -134,19 +116,6 @@ class UserVoter extends AbstractVoter
*/
protected function canDelete(User $profile, User $user, TokenInterface $token)
{
if (!$this->canAdminUsers($token)) {
return false;
}
return $profile->getId() !== $user->getId();
}
/**
* @param TokenInterface $token
* @return bool
*/
protected function canAdminUsers(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole(User::ROLE_SUPER_ADMIN, $token);
}
}