User roles and permission management via Admin UI (#1231)
This commit is contained in:
@@ -38,7 +38,7 @@ trait StringAccessibleConfigTrait
|
||||
*/
|
||||
protected function getConfigurations(ConfigLoaderInterface $repository): array
|
||||
{
|
||||
return $repository->getConfiguration($this->getPrefix() . '.');
|
||||
return $repository->getConfiguration($this->getPrefix());
|
||||
}
|
||||
|
||||
protected function prepare()
|
||||
|
||||
@@ -27,7 +27,10 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
public const DOMAIN_FLASH = 'flashmessages';
|
||||
public const DOMAIN_ERROR = 'exceptions';
|
||||
|
||||
public const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||
/**
|
||||
* @deprecated since 1.6, will be removed with 2.0
|
||||
*/
|
||||
public const ROLE_ADMIN = User::ROLE_ADMIN;
|
||||
|
||||
/**
|
||||
* @return DataCollectorTranslator
|
||||
|
||||
@@ -21,7 +21,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to display calendars.
|
||||
*
|
||||
* @Route(path="/calendar")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Dashboard controller for the admin area.
|
||||
*
|
||||
* @Route(path="/dashboard")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class DashboardController extends AbstractController
|
||||
{
|
||||
@@ -121,9 +121,16 @@ class DashboardController extends AbstractController
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$sections = $event->getSections();
|
||||
$clearedSections = [];
|
||||
/** @var WidgetContainerInterface $section */
|
||||
foreach ($sections as $key => $section) {
|
||||
if (!empty($section->getWidgets())) {
|
||||
$clearedSections[] = $section;
|
||||
}
|
||||
}
|
||||
|
||||
uasort(
|
||||
$sections,
|
||||
$clearedSections,
|
||||
function (WidgetContainerInterface $a, WidgetContainerInterface $b) {
|
||||
if ($a->getOrder() == $b->getOrder()) {
|
||||
return 0;
|
||||
@@ -134,7 +141,7 @@ class DashboardController extends AbstractController
|
||||
);
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'widgets' => $sections
|
||||
'widgets' => $clearedSections
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Homepage controller is a redirect controller with user specific logic.
|
||||
*
|
||||
* @Route(path="/homepage")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||
*/
|
||||
class HomepageController extends AbstractController
|
||||
{
|
||||
|
||||
34
src/Controller/LayoutController.php
Normal file
34
src/Controller/LayoutController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Used for the (initial) page rendering.
|
||||
*/
|
||||
class LayoutController extends AbstractController
|
||||
{
|
||||
public function activeEntries(TimesheetRepository $repository, TimesheetConfiguration $configuration): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$activeEntries = $repository->getActiveEntries($user);
|
||||
|
||||
return $this->render(
|
||||
'navbar/active-entries.html.twig',
|
||||
[
|
||||
'entries' => $activeEntries,
|
||||
'soft_limit' => $configuration->getActiveEntriesSoftLimit(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
161
src/Controller/PermissionController.php
Normal file
161
src/Controller/PermissionController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\Role;
|
||||
use App\Entity\RolePermission;
|
||||
use App\Form\RoleType;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
use App\Repository\RoleRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Security\RoleService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage user roles and role permissions.
|
||||
*
|
||||
* @Route(path="/admin/permissions")
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
final class PermissionController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var RoleService
|
||||
*/
|
||||
private $roleService;
|
||||
/**
|
||||
* @var RolePermissionManager
|
||||
*/
|
||||
private $manager;
|
||||
/**
|
||||
* @var RoleRepository
|
||||
*/
|
||||
private $roleRepository;
|
||||
|
||||
public function __construct(RoleService $roleService, RolePermissionManager $manager, RoleRepository $roleRepository)
|
||||
{
|
||||
$this->roleService = $roleService;
|
||||
$this->manager = $manager;
|
||||
$this->roleRepository = $roleRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="admin_user_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
$existing = [];
|
||||
|
||||
foreach ($all as $role) {
|
||||
$existing[] = $role->getName();
|
||||
}
|
||||
|
||||
$existing = array_map('strtoupper', $existing);
|
||||
|
||||
// automatically import all hard coded (default) roles into the database table
|
||||
foreach ($this->roleService->getAvailableNames() as $roleName) {
|
||||
$roleName = strtoupper($roleName);
|
||||
if (!in_array($roleName, $existing)) {
|
||||
$role = new Role();
|
||||
$role->setName($roleName);
|
||||
$this->roleRepository->saveRole($role);
|
||||
$existing[] = $roleName;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('user/permissions.html.twig', [
|
||||
'roles' => $this->roleRepository->findAll(),
|
||||
'permissions' => $this->manager->getPermissions(),
|
||||
'manager' => $this->manager,
|
||||
'system_roles' => $this->roleService->getSystemRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/create", name="admin_user_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
public function createRole(Request $request): Response
|
||||
{
|
||||
$role = new Role();
|
||||
|
||||
$form = $this->createForm(RoleType::class, $role, [
|
||||
'action' => $this->generateUrl('admin_user_roles', []),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->roleRepository->saveRole($role);
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashSuccess('action.update.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
|
||||
return $this->render('user/edit_role.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'role' => $role,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/{id}/delete", name="admin_user_role_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
public function deleteRole(Role $role): Response
|
||||
{
|
||||
try {
|
||||
$this->roleRepository->deleteRole($role);
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/roles/{id}/{name}/{value}", name="admin_user_permission_save", methods={"GET"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*/
|
||||
public function savePermission(Role $role, string $name, string $value, RolePermissionRepository $rolePermissionRepository): Response
|
||||
{
|
||||
if (!$this->manager->isRegisteredPermission($name)) {
|
||||
throw $this->createNotFoundException('Unknown permission: ' . $name);
|
||||
}
|
||||
|
||||
try {
|
||||
$permission = $rolePermissionRepository->findRolePermission($role, $name);
|
||||
if (null === $permission) {
|
||||
$permission = new RolePermission();
|
||||
$permission->setRole($role);
|
||||
$permission->setPermission($name);
|
||||
}
|
||||
$permission->setAllowed((bool) $value);
|
||||
|
||||
$rolePermissionRepository->saveRolePermission($permission);
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
}
|
||||
@@ -78,23 +78,4 @@ class TimesheetController extends TimesheetAbstractController
|
||||
{
|
||||
return $this->create($request, 'timesheet/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the initial page rendering.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function activeEntriesAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$activeEntries = $this->getRepository()->getActiveEntries($user);
|
||||
|
||||
return $this->render(
|
||||
'navbar/active-entries.html.twig',
|
||||
[
|
||||
'entries' => $activeEntries,
|
||||
'soft_limit' => $this->getSoftLimit(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ use App\Form\UserCreateType;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
@@ -64,12 +65,8 @@ class UserController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
public function indexAction($page, Request $request): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setPage($page);
|
||||
@@ -99,11 +96,8 @@ class UserController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_user')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
public function createAction(Request $request): Response
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
@@ -144,14 +138,8 @@ class UserController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', userToDelete)")
|
||||
*
|
||||
* @param User $userToDelete
|
||||
* @param Request $request
|
||||
* @param TimesheetRepository $repository
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository)
|
||||
public function deleteAction(User $userToDelete, Request $request, TimesheetRepository $repository): Response
|
||||
{
|
||||
// $userToDelete MUST not be called $user, as $user is always the current user!
|
||||
$stats = $repository->getUserStatistics($userToDelete);
|
||||
@@ -189,27 +177,7 @@ class UserController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/permissions", name="admin_user_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('role_permissions')")
|
||||
*
|
||||
* @param RolePermissionManager $manager
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function permissions(RolePermissionManager $manager)
|
||||
{
|
||||
return $this->render('user/permissions.html.twig', [
|
||||
'roles' => $manager->getRoles(),
|
||||
'permissions' => $manager->getPermissions(),
|
||||
'manager' => $manager,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(UserQuery $query)
|
||||
protected function getToolbarForm(UserQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(UserToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_user', [
|
||||
@@ -219,11 +187,7 @@ class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(User $user)
|
||||
private function createEditForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(UserCreateType::class, $user, [
|
||||
'action' => $this->generateUrl('admin_user_create'),
|
||||
|
||||
@@ -525,7 +525,6 @@ class Configuration implements ConfigurationInterface
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([])
|
||||
|
||||
59
src/Entity/Role.php
Normal file
59
src/Entity/Role.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_roles",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="roles_name", columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\RoleRepository")
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
class Role
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\Length(min=5, max=50)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): Role
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
101
src/Entity/RolePermission.php
Normal file
101
src/Entity/RolePermission.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_roles_permissions",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="role_permission", columns={"role_id","permission"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\RolePermissionRepository")
|
||||
* @UniqueEntity({"role", "permission"})
|
||||
*/
|
||||
class RolePermission
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var Role
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Role")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $role;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="permission", type="string", length=50, nullable=false)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $permission;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="allowed", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $allowed = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRole(): ?Role
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(Role $role): RolePermission
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPermission(): ?string
|
||||
{
|
||||
return $this->permission;
|
||||
}
|
||||
|
||||
public function setPermission(string $permission): RolePermission
|
||||
{
|
||||
$this->permission = $permission;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for isValue()
|
||||
*/
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
return $this->allowed;
|
||||
}
|
||||
|
||||
public function setAllowed(bool $allowed): RolePermission
|
||||
{
|
||||
$this->allowed = $allowed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -82,10 +82,6 @@ class DashboardSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
public function onDashboardEvent(DashboardEvent $event)
|
||||
{
|
||||
if (!$this->security->isGranted(User::ROLE_ADMIN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$section = new CompoundRow();
|
||||
$section->setTitle('ROLE_ADMIN');
|
||||
$section->setOrder(100);
|
||||
|
||||
79
src/Form/RoleType.php
Normal file
79
src/Form/RoleType.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\Role;
|
||||
use FOS\RestBundle\Validator\Constraints\Regex;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* The form used to edit roles.
|
||||
*/
|
||||
class RoleType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
'help' => 'Allowed character: A-Z and _',
|
||||
'constraints' => [
|
||||
new Regex(['pattern' => '/^[a-zA-Z_]{5,}$/'])
|
||||
],
|
||||
'attr' => [
|
||||
'maxlength' => 50
|
||||
]
|
||||
])
|
||||
;
|
||||
|
||||
// help the user to figure out the allowed name
|
||||
$builder->get('name')->addViewTransformer(
|
||||
new CallbackTransformer(
|
||||
function ($roleName) {
|
||||
if (is_string($roleName)) {
|
||||
$roleName = str_replace(' ', '_', $roleName);
|
||||
$roleName = str_replace('-', '_', $roleName);
|
||||
$roleName = strtoupper($roleName);
|
||||
}
|
||||
|
||||
return $roleName;
|
||||
},
|
||||
function ($roleName) {
|
||||
return $roleName;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Role::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_role',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.userRoleUpdate',
|
||||
'data-msg-success' => 'action.update.success',
|
||||
'data-msg-error' => 'action.update.error',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class UserRoleType extends AbstractType
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
$roles = [];
|
||||
foreach ($this->roles->getAvailableNames() as $name) {
|
||||
$roles[$name] = $name;
|
||||
$roles[$name] = strtoupper($name);
|
||||
}
|
||||
|
||||
if ($options['include_default'] !== true && isset($roles[User::DEFAULT_ROLE])) {
|
||||
|
||||
67
src/Migrations/Version20191108151534.php
Normal file
67
src/Migrations/Version20191108151534.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds the user roles and role permissions table
|
||||
*
|
||||
* @version 1.6
|
||||
*/
|
||||
final class Version20191108151534 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Adds the user roles and role permissions table';
|
||||
}
|
||||
|
||||
protected function isSupportingForeignKeys(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
if ($this->isPlatformSqlite()) {
|
||||
// does fail if we use transactions, as tables are re-created and foreign keys would fail
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$roles = $schema->createTable('kimai2_roles');
|
||||
$roles->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$roles->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$roles->setPrimaryKey(['id']);
|
||||
$roles->addUniqueIndex(['name'], 'roles_name');
|
||||
|
||||
$rolePermissions = $schema->createTable('kimai2_roles_permissions');
|
||||
$rolePermissions->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$rolePermissions->addColumn('role_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$rolePermissions->addColumn('permission', 'string', ['notnull' => true, 'length' => 50]);
|
||||
$rolePermissions->addColumn('allowed', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$rolePermissions->setPrimaryKey(['id']);
|
||||
$rolePermissions->addUniqueIndex(['role_id', 'permission'], 'role_permission');
|
||||
$rolePermissions->addForeignKeyConstraint('kimai2_roles', ['role_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_D263A3B8D60322AC');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_roles_permissions');
|
||||
$schema->dropTable('kimai2_roles');
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,53 @@ use App\Entity\Configuration;
|
||||
use App\Form\Model\SystemConfiguration;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
class ConfigurationRepository extends EntityRepository implements ConfigLoaderInterface
|
||||
{
|
||||
private static $cacheByPrefix = null;
|
||||
private static $cacheAll = [];
|
||||
|
||||
private function clearCache()
|
||||
{
|
||||
static::$cacheByPrefix = null;
|
||||
}
|
||||
|
||||
private function prefillCache()
|
||||
{
|
||||
if (null !== static::$cacheByPrefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Configuration[] $configs */
|
||||
$configs = $this->findAll();
|
||||
static::$cacheByPrefix = [];
|
||||
foreach ($configs as $config) {
|
||||
$key = substr($config->getName(), 0, strpos($config->getName(), '.'));
|
||||
if (!array_key_exists($key, static::$cacheByPrefix)) {
|
||||
static::$cacheByPrefix[$key] = [];
|
||||
}
|
||||
static::$cacheByPrefix[$key][] = $config;
|
||||
static::$cacheAll[] = $config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @return Configuration[]
|
||||
*/
|
||||
public function getConfiguration(?string $prefix = null): array
|
||||
{
|
||||
$this->prefillCache();
|
||||
|
||||
if (null === $prefix) {
|
||||
return $this->findAll();
|
||||
return static::$cacheAll;
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c')
|
||||
->where($qb->expr()->like('c.name', ':prefix'))
|
||||
->setParameter(':prefix', $prefix . '%');
|
||||
if (!array_key_exists($prefix, static::$cacheByPrefix)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_OBJECT);
|
||||
return static::$cacheByPrefix[$prefix];
|
||||
}
|
||||
|
||||
public function saveSystemConfiguration(SystemConfiguration $model)
|
||||
@@ -68,5 +94,7 @@ class ConfigurationRepository extends EntityRepository implements ConfigLoaderIn
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Repository/RolePermissionRepository.php
Normal file
40
src/Repository/RolePermissionRepository.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\Role;
|
||||
use App\Entity\RolePermission;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class RolePermissionRepository extends EntityRepository
|
||||
{
|
||||
public function saveRolePermission(RolePermission $permission)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($permission);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function findRolePermission(Role $role, string $permission)
|
||||
{
|
||||
return $this->findOneBy(['role' => $role, 'permission' => $permission]);
|
||||
}
|
||||
|
||||
public function getAllAsArray()
|
||||
{
|
||||
$qb = $this->createQueryBuilder('rp');
|
||||
|
||||
$qb->select('r.name as role,rp.permission,rp.allowed')
|
||||
->leftJoin('rp.role', 'r');
|
||||
|
||||
return $qb->getQuery()->execute([], AbstractQuery::HYDRATE_ARRAY);
|
||||
}
|
||||
}
|
||||
47
src/Repository/RoleRepository.php
Normal file
47
src/Repository/RoleRepository.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\Role;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
|
||||
class RoleRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @return Role[]
|
||||
*/
|
||||
public function findAll()
|
||||
{
|
||||
return parent::findAll();
|
||||
}
|
||||
|
||||
public function saveRole(Role $role)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($role);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteRole(Role $role)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
$em->remove($role);
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (ORMException $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,32 +9,61 @@
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
class RolePermissionManager
|
||||
use App\Entity\User;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
|
||||
final class RolePermissionManager
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [];
|
||||
private $permissions = [];
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $knownPermissions = [];
|
||||
/**
|
||||
* @var RoleService
|
||||
*/
|
||||
private $roles;
|
||||
private $knownPermissions = [];
|
||||
|
||||
public function __construct(RoleService $roles, array $permissions)
|
||||
public function __construct(RolePermissionRepository $repository, array $permissions)
|
||||
{
|
||||
$this->roles = $roles;
|
||||
$this->permissions = $permissions;
|
||||
|
||||
foreach ($permissions as $role => $perms) {
|
||||
$this->knownPermissions = array_merge($this->knownPermissions, $perms);
|
||||
}
|
||||
$this->knownPermissions = array_unique($this->knownPermissions);
|
||||
|
||||
$all = $repository->getAllAsArray();
|
||||
foreach ($all as $item) {
|
||||
$perm = $item['permission'];
|
||||
$role = strtoupper($item['role']);
|
||||
$isAllowed = $item['allowed'];
|
||||
|
||||
// see permissions.html.twig for this special case
|
||||
if ($role === User::ROLE_SUPER_ADMIN && in_array($perm, ['role_permissions', 'view_user'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$isAllowed) {
|
||||
if (array_key_exists($role, $this->permissions)) {
|
||||
if (($key = array_search($perm, $this->permissions[$role])) !== false) {
|
||||
unset($this->permissions[$role][$key]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!array_key_exists($role, $this->permissions)) {
|
||||
$this->permissions[$role] = [];
|
||||
}
|
||||
$this->permissions[$role][] = $perm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only permissions which were registered through the Symfony configuration stack will be acknowledged here.
|
||||
*
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission(string $permission): bool
|
||||
{
|
||||
return in_array($permission, $this->knownPermissions);
|
||||
@@ -42,6 +71,8 @@ class RolePermissionManager
|
||||
|
||||
public function hasPermission(string $role, string $permission): bool
|
||||
{
|
||||
$role = strtoupper($role);
|
||||
|
||||
if (!isset($this->permissions[$role])) {
|
||||
return false;
|
||||
}
|
||||
@@ -49,11 +80,11 @@ class RolePermissionManager
|
||||
return in_array($permission, $this->permissions[$role]);
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return $this->roles->getAvailableNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only permissions which were registered through the Symfony configuration stack will be returned here.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->knownPermissions;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\Role;
|
||||
use App\Repository\RoleRepository;
|
||||
|
||||
final class RoleService
|
||||
{
|
||||
/**
|
||||
@@ -19,13 +22,18 @@ final class RoleService
|
||||
* @var string[]
|
||||
*/
|
||||
private $roleNames = [];
|
||||
/**
|
||||
* @var RoleRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(array $roles)
|
||||
public function __construct(RoleRepository $repository, array $roles)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
public function getAvailableNames(): array
|
||||
private function cacheNames()
|
||||
{
|
||||
if (empty($this->roleNames)) {
|
||||
$roles = [];
|
||||
@@ -38,9 +46,24 @@ final class RoleService
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Role $item */
|
||||
foreach ($this->repository->findAll() as $item) {
|
||||
$roles[] = $item->getName();
|
||||
}
|
||||
|
||||
$this->roleNames = array_values(array_unique($roles));
|
||||
}
|
||||
}
|
||||
|
||||
public function getAvailableNames(): array
|
||||
{
|
||||
$this->cacheNames();
|
||||
|
||||
return $this->roleNames;
|
||||
}
|
||||
|
||||
public function getSystemRoles(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ final class IconExtension extends AbstractExtension
|
||||
'profile-stats' => 'far fa-chart-bar',
|
||||
'project' => 'fas fa-briefcase',
|
||||
'repeat' => 'fas fa-redo-alt',
|
||||
'roles' => 'fas fa-user-shield',
|
||||
'search' => 'fas fa-search',
|
||||
'settings' => 'fas fa-cog',
|
||||
'shop' => 'fas fa-shopping-cart',
|
||||
|
||||
@@ -41,7 +41,8 @@ class RoleValidator extends ConstraintValidator
|
||||
$roles = [$roles];
|
||||
}
|
||||
|
||||
$allowedRoles = $this->service->getAvailableNames();
|
||||
// the fos user entity uppercases the roles by default
|
||||
$allowedRoles = array_map('strtoupper', $this->service->getAvailableNames());
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if (!is_string($role) || !in_array($role, $allowedRoles)) {
|
||||
|
||||
@@ -29,10 +29,6 @@ abstract class AbstractVoter extends Voter
|
||||
*/
|
||||
protected $roleManager;
|
||||
|
||||
/**
|
||||
* @param AclDecisionManager $decisionManager
|
||||
* @param RolePermissionManager $roleManager
|
||||
*/
|
||||
public function __construct(AclDecisionManager $decisionManager, RolePermissionManager $roleManager)
|
||||
{
|
||||
$this->decisionManager = $decisionManager;
|
||||
|
||||
@@ -30,13 +30,7 @@ class RolePermissionVoter extends AbstractVoter
|
||||
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;
|
||||
return $this->isRegisteredPermission($attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,12 +47,6 @@ class RolePermissionVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($user->getRoles() as $role) {
|
||||
if ($this->hasPermission($role, $attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->hasRolePermission($user, $attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +82,8 @@ class UserVoter extends AbstractVoter
|
||||
|
||||
return $this->hasRolePermission($user, 'delete_user');
|
||||
|
||||
// used in templates and ProfileController
|
||||
case self::VIEW:
|
||||
case self::EDIT:
|
||||
// always allow the user to edit these own settings
|
||||
if ($subject->getId() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
// no break on purpose
|
||||
|
||||
case self::PREFERENCES:
|
||||
case self::PASSWORD:
|
||||
case self::API_TOKEN:
|
||||
|
||||
Reference in New Issue
Block a user