fix: properly gate menus by role
This commit is contained in:
@@ -13,6 +13,30 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractMenuBuilder
|
||||
{
|
||||
/**
|
||||
* Routes of the areas that have pages of their own, mapped to the role owning the area.
|
||||
*
|
||||
* Note that "app_admin_" does not match the shared "app_administrative_" routes.
|
||||
*/
|
||||
protected const AREA_ROUTE_PREFIXES = [
|
||||
'app_admin_' => 'ROLE_ADMIN',
|
||||
'app_manager_' => 'ROLE_MANAGER',
|
||||
'app_house_manager_' => 'ROLE_HOUSE_MANAGER',
|
||||
'app_teamer_' => 'ROLE_TEAMER',
|
||||
];
|
||||
|
||||
/**
|
||||
* Priority order of the area roles, must stay in sync with User::getDefaultRoute().
|
||||
*/
|
||||
protected const ROLE_PRIORITY = [
|
||||
'ROLE_ADMIN',
|
||||
'ROLE_MANAGER',
|
||||
'ROLE_HOUSE_MANAGER',
|
||||
'ROLE_TEAMER',
|
||||
];
|
||||
|
||||
protected const AREA_SESSION_KEY = 'menu_area';
|
||||
|
||||
public function __construct(
|
||||
protected FactoryInterface $factory,
|
||||
protected Security $security,
|
||||
@@ -26,6 +50,49 @@ abstract class AbstractMenuBuilder
|
||||
return $this->factory->createItem('root');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the role owning the area the current request belongs to.
|
||||
*
|
||||
* Entering an area that has pages of its own selects that area and keeps it
|
||||
* for the rest of the session. The routes shared by all administrative roles
|
||||
* (app_administrative_*) carry no area of their own and therefore stay in the
|
||||
* area the user entered last, so that switching areas sticks. Without a
|
||||
* remembered area the highest role wins.
|
||||
*/
|
||||
protected function resolveArea(): ?string
|
||||
{
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
$session = null !== $this->security->getUser() && true === $request?->hasSession()
|
||||
? $request->getSession()
|
||||
: null;
|
||||
$route = $request?->attributes->get('_route');
|
||||
|
||||
if (is_string($route)) {
|
||||
foreach (self::AREA_ROUTE_PREFIXES as $prefix => $role) {
|
||||
if (str_starts_with($route, $prefix) && $this->security->isGranted($role)) {
|
||||
$session?->set(self::AREA_SESSION_KEY, $role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rememberedArea = $session?->get(self::AREA_SESSION_KEY);
|
||||
|
||||
// The remembered area is re-checked, roles can be revoked mid-session.
|
||||
if (is_string($rememberedArea) && $this->security->isGranted($rememberedArea)) {
|
||||
return $rememberedArea;
|
||||
}
|
||||
|
||||
foreach (self::ROLE_PRIORITY as $role) {
|
||||
if ($this->security->isGranted($role)) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getDefaultRouteParameters(string $parameter = 'id', string $default = '0'): array
|
||||
{
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
@@ -36,6 +103,41 @@ abstract class AbstractMenuBuilder
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the link leading back out of a detail view.
|
||||
*
|
||||
* The return url is taken from the menu options first, then from the request,
|
||||
* and falls back to the overview itself. Without that fallback the item ends up
|
||||
* without an uri and is rendered as a dead label instead of a link.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
protected function addBackItem(
|
||||
ItemInterface $menu,
|
||||
array $options,
|
||||
string $defaultRoute = 'app_administrative_teamer_index',
|
||||
): void {
|
||||
$this->addDivider($menu);
|
||||
|
||||
// Already decoded by ReturnUrlTrait::getReturnUrl(), must not be decoded again.
|
||||
$returnUrl = $options['return_url'] ?? null;
|
||||
|
||||
if (false === is_string($returnUrl) || '' === $returnUrl) {
|
||||
$requestReturnUrl = $this->requestStack->getMainRequest()?->get('r');
|
||||
|
||||
$returnUrl = is_string($requestReturnUrl) && '' !== $requestReturnUrl
|
||||
? rawurldecode($requestReturnUrl)
|
||||
: null;
|
||||
}
|
||||
|
||||
$menu->addChild('zurück zur Übersicht', [
|
||||
...(null !== $returnUrl ? ['uri' => $returnUrl] : ['route' => $defaultRoute]),
|
||||
'extras' => [
|
||||
'icon' => 'arrow-left',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addAdminItem(ItemInterface $menu): void
|
||||
{
|
||||
if ($this->security->isGranted('ROLE_ADMIN')) {
|
||||
|
||||
@@ -337,16 +337,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
],
|
||||
]);
|
||||
|
||||
$this->addDivider($menu);
|
||||
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
|
||||
$menu->addChild('zurück zur Übersicht', [
|
||||
'uri' => rawurldecode($request->get('r')),
|
||||
'extras' => [
|
||||
'icon' => 'arrow-left',
|
||||
],
|
||||
]);
|
||||
$this->addBackItem($menu, $options);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
@@ -204,16 +204,7 @@ class ManagerMenuBuilder extends AbstractMenuBuilder
|
||||
],
|
||||
]);
|
||||
|
||||
$this->addDivider($menu);
|
||||
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
|
||||
$menu->addChild('zurück zur Übersicht', [
|
||||
'uri' => rawurldecode($request->get('r')),
|
||||
'extras' => [
|
||||
'icon' => 'arrow-left',
|
||||
],
|
||||
]);
|
||||
$this->addBackItem($menu, $options);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Menu;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Serves the menu of the area the current user has the highest privileges for.
|
||||
*
|
||||
* Used by templates that are reachable by more than one role, so that the area
|
||||
* selection follows a single priority order instead of being decided in Twig.
|
||||
*/
|
||||
class MenuBuilder extends AbstractMenuBuilder
|
||||
{
|
||||
public function __construct(
|
||||
FactoryInterface $factory,
|
||||
Security $security,
|
||||
RequestStack $requestStack,
|
||||
TranslatorInterface $translator,
|
||||
private readonly AdminMenuBuilder $adminMenuBuilder,
|
||||
private readonly ManagerMenuBuilder $managerMenuBuilder,
|
||||
private readonly HouseManagerMenuBuilder $houseManagerMenuBuilder,
|
||||
private readonly TeamerMenuBuilder $teamerMenuBuilder,
|
||||
) {
|
||||
parent::__construct($factory, $security, $requestStack, $translator);
|
||||
}
|
||||
|
||||
public function createMainMenu(array $options): ItemInterface
|
||||
{
|
||||
return match ($this->resolveArea()) {
|
||||
'ROLE_ADMIN' => $this->adminMenuBuilder->createMainMenu($options),
|
||||
'ROLE_MANAGER' => $this->managerMenuBuilder->createMainMenu($options),
|
||||
'ROLE_HOUSE_MANAGER' => $this->houseManagerMenuBuilder->createMainMenu($options),
|
||||
'ROLE_TEAMER' => $this->teamerMenuBuilder->createMainMenu($options),
|
||||
default => $this->createRootElement(),
|
||||
};
|
||||
}
|
||||
|
||||
public function createTeamerMenu(array $options): ItemInterface
|
||||
{
|
||||
return match ($this->resolveArea()) {
|
||||
'ROLE_ADMIN' => $this->adminMenuBuilder->createTeamerMenu($options),
|
||||
'ROLE_MANAGER' => $this->managerMenuBuilder->createTeamerMenu($options),
|
||||
default => $this->createRootElement(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user