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',
+35 -29
View File
@@ -22,16 +22,18 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'chart', 'icon' => 'chart',
], ],
]); ]);
$menu->addChild('Buchungsentwürfe', [ if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) {
'route' => 'app_admin_bookingeditdraft', $menu->addChild('Buchungsentwürfe', [
'linkAttributes' => [ 'route' => 'app_admin_bookingeditdraft',
'title' => 'Buchungsentwürfe', 'linkAttributes' => [
], 'title' => 'Buchungsentwürfe',
'extras' => [ ],
'icon' => 'edit', 'extras' => [
'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']], 'icon' => 'edit',
], '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,25 +58,29 @@ class AdminMenuBuilder extends AbstractMenuBuilder
]); ]);
} }
$menu->addChild('Benutzer', [ if ($this->security->isGranted('ROLE_ADMIN')) {
'route' => 'app_admin_user', $menu->addChild('Benutzer', [
'linkAttributes' => [ 'route' => 'app_admin_user',
'title' => 'Benutzer', 'linkAttributes' => [
], 'title' => 'Benutzer',
'extras' => [ ],
'icon' => 'users', 'extras' => [
], 'icon' => 'users',
]); ],
$menu->addChild('Logs', [ ]);
'route' => 'app_admin_log', }
'linkAttributes' => [ if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) {
'title' => 'Logs', $menu->addChild('Logs', [
], 'route' => 'app_admin_log',
'extras' => [ 'linkAttributes' => [
'icon' => 'list', 'title' => 'Logs',
'routes' => [['pattern' => '/^app_admin_log/']], ],
], 'extras' => [
]); 'icon' => 'list',
'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());
} }