feat: gate functionality for customer expert users

This commit is contained in:
2026-09-15 15:13:47 +02:00
parent 1d14f00bb7
commit a1d1fdde14
15 changed files with 55 additions and 43 deletions
+4
View File
@@ -17,8 +17,12 @@ security:
roles: [ ROLE_MAILJET_WEBHOOK ] roles: [ ROLE_MAILJET_WEBHOOK ]
role_hierarchy: role_hierarchy:
# ROLE_CUSTOMER_EXPERT gates the booking-draft and log surfaces, which used to be
# ROLE_ADMIN only. Inheriting it here is what keeps those surfaces open to administrators
# after the #[IsGranted] attributes moved to the narrower role.
ROLE_ADMIN: ROLE_ADMIN:
- ROLE_GROUPS_ADMIN - ROLE_GROUPS_ADMIN
- ROLE_CUSTOMER_EXPERT
ROLE_GROUPS_ADMIN: ROLE_GROUPS_ADMIN:
- ROLE_GROUPS_MANAGER - ROLE_GROUPS_MANAGER
firewalls: firewalls:
@@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class DeleteController extends AbstractController class DeleteController extends AbstractController
{ {
use ReturnUrlTrait; use ReturnUrlTrait;
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class ExportController extends AbstractController class ExportController extends AbstractController
{ {
use ReturnUrlTrait; use ReturnUrlTrait;
@@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class FilterController extends AbstractController class FilterController extends AbstractController
{ {
use ListFilterTrait; use ListFilterTrait;
@@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class IndexController extends AbstractController class IndexController extends AbstractController
{ {
use ListFilterTrait; use ListFilterTrait;
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class ShowController extends AbstractController class ShowController extends AbstractController
{ {
use ReturnUrlTrait; use ReturnUrlTrait;
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class DownloadController extends AbstractController class DownloadController extends AbstractController
{ {
public function __construct(private readonly XmlDumpReader $xmlDumpReader) public function __construct(private readonly XmlDumpReader $xmlDumpReader)
@@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class FilterController extends AbstractController class FilterController extends AbstractController
{ {
use ListFilterTrait; use ListFilterTrait;
+1 -1
View File
@@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class IndexController extends AbstractController class IndexController extends AbstractController
{ {
use ListFilterTrait; use ListFilterTrait;
@@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_CUSTOMER_EXPERT')]
class XmlDumpController extends AbstractController class XmlDumpController extends AbstractController
{ {
public function __construct(private readonly XmlDumpReader $xmlDumpReader) public function __construct(private readonly XmlDumpReader $xmlDumpReader)
@@ -37,7 +37,7 @@ class RecentLogEntriesWidgetProvider implements DashboardWidgetProviderInterface
public function getRequiredRole(): string public function getRequiredRole(): string
{ {
return Role::ADMIN; return Role::CUSTOMER_EXPERT;
} }
public function getPriority(): int public function getPriority(): int
+2 -1
View File
@@ -2,6 +2,7 @@
namespace App\Menu; namespace App\Menu;
use App\Security\Voter\AdministrativeAccessVoter;
use Knp\Menu\FactoryInterface; use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface; use Knp\Menu\ItemInterface;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
@@ -39,7 +40,7 @@ abstract class AbstractMenuBuilder
protected function addAdminItem(ItemInterface $menu): void protected function addAdminItem(ItemInterface $menu): void
{ {
if ($this->security->isGranted('ROLE_ADMIN')) { if ($this->security->isGranted(AdministrativeAccessVoter::ADMINISTRATIVE_ACCESS)) {
$this->addDivider($menu); $this->addDivider($menu);
$menu->addChild('zum Adminbereich', [ $menu->addChild('zum Adminbereich', [
'route' => 'app_admin_index', 'route' => 'app_admin_index',
+6
View File
@@ -22,6 +22,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'chart', 'icon' => 'chart',
], ],
]); ]);
if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) {
$menu->addChild('Buchungsentwürfe', [ $menu->addChild('Buchungsentwürfe', [
'route' => 'app_admin_bookingeditdraft', 'route' => 'app_admin_bookingeditdraft',
'linkAttributes' => [ 'linkAttributes' => [
@@ -32,6 +33,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']], 'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']],
], ],
]); ]);
}
if ($this->security->isGranted('ROLE_GROUPS_ADMIN')) { if ($this->security->isGranted('ROLE_GROUPS_ADMIN')) {
$menu->addChild('Gruppenbuchungen', [ $menu->addChild('Gruppenbuchungen', [
@@ -56,6 +58,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
]); ]);
} }
if ($this->security->isGranted('ROLE_ADMIN')) {
$menu->addChild('Benutzer', [ $menu->addChild('Benutzer', [
'route' => 'app_admin_user', 'route' => 'app_admin_user',
'linkAttributes' => [ 'linkAttributes' => [
@@ -65,6 +68,8 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'users', 'icon' => 'users',
], ],
]); ]);
}
if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) {
$menu->addChild('Logs', [ $menu->addChild('Logs', [
'route' => 'app_admin_log', 'route' => 'app_admin_log',
'linkAttributes' => [ 'linkAttributes' => [
@@ -75,6 +80,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'routes' => [['pattern' => '/^app_admin_log/']], 'routes' => [['pattern' => '/^app_admin_log/']],
], ],
]); ]);
}
$this->addLogoutItem($menu); $this->addLogoutItem($menu);
@@ -29,6 +29,7 @@ class AdministrativeAccessVoter extends Voter
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{ {
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']) return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN'])
|| $this->accessDecisionManager->decide($token, ['ROLE_GROUPS_MANAGER']); || $this->accessDecisionManager->decide($token, ['ROLE_GROUPS_MANAGER'])
|| $this->accessDecisionManager->decide($token, ['ROLE_CUSTOMER_EXPERT']);
} }
} }
@@ -46,7 +46,7 @@ class RecentLogEntriesWidgetProviderTest extends TestCase
$this->assertStringEndsWith(str_repeat('a', 60).'…', $widget->entries[0]->label); $this->assertStringEndsWith(str_repeat('a', 60).'…', $widget->entries[0]->label);
} }
public function testAsksForFiveEntriesAndRequiresAdmin(): void public function testAsksForFiveEntriesAndRequiresCustomerExpert(): void
{ {
$repository = $this->createMock(LogEntryRepository::class); $repository = $this->createMock(LogEntryRepository::class);
$repository->expects($this->once()) $repository->expects($this->once())
@@ -57,7 +57,7 @@ class RecentLogEntriesWidgetProviderTest extends TestCase
$provider = new RecentLogEntriesWidgetProvider($repository, $this->urlGenerator()); $provider = new RecentLogEntriesWidgetProvider($repository, $this->urlGenerator());
$this->assertSame(Role::ADMIN, $provider->getRequiredRole()); $this->assertSame(Role::CUSTOMER_EXPERT, $provider->getRequiredRole());
$this->assertNotNull($provider->build()); $this->assertNotNull($provider->build());
} }