feat: restrict admin role assignment to the privileged roles
This commit is contained in:
@@ -45,6 +45,7 @@ class EditController extends AbstractController
|
||||
return $this->render('admin/user/modal_edit.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $form,
|
||||
'syncedRoles' => Role::syncedOnly($user->getRoles()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ class EditController extends AbstractController
|
||||
return $this->render('admin/user/modal_edit.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $form,
|
||||
'syncedRoles' => Role::syncedOnly($user->getRoles()),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Lets an administrator assign roles and hotel codes to an existing account.
|
||||
* Lets an administrator assign the privileged roles and the hotel codes of an existing account.
|
||||
*
|
||||
* This is the only way those two get changed after the account was created — BpnAuthenticator
|
||||
* imports them once and never touches them again.
|
||||
* Only Role::PRIVILEGED is offered: the remaining roles are synced from the BusPro CRM on every
|
||||
* login (see BpnAuthenticator), so editing them here would be undone at the user's next login.
|
||||
* Hotel codes are seeded once at account creation, which makes this form the only way to change
|
||||
* them afterwards.
|
||||
*
|
||||
* @extends AbstractType<User>
|
||||
*/
|
||||
@@ -35,14 +37,16 @@ class UserType extends AbstractType
|
||||
$builder
|
||||
->add('roles', ChoiceType::class, [
|
||||
'label' => 'Rollen',
|
||||
'choices' => array_flip(Role::labels()),
|
||||
'choices' => $this->privilegedChoices(),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
// User::getRoles() prepends the implicit ROLE_USER, which must not be written back.
|
||||
'getter' => static fn (User $user): array => array_values(array_diff($user->getRoles(), ['ROLE_USER'])),
|
||||
'help' => 'Alle übrigen Rollen kommen bei jeder Anmeldung aus BusPro und lassen sich hier nicht ändern.',
|
||||
// Only the administrator-granted half is editable; the synced half is preserved,
|
||||
// as is the implicit ROLE_USER, which must never be written back.
|
||||
'getter' => static fn (User $user): array => Role::privilegedOnly($user->getRoles()),
|
||||
'setter' => static function (User $user, array $roles): void {
|
||||
$user->setRoles(array_values(array_unique($roles)));
|
||||
$user->setRoles(Role::combine($user->getRoles(), $roles));
|
||||
},
|
||||
])
|
||||
->add('hotelCodes', ChoiceType::class, [
|
||||
@@ -58,6 +62,14 @@ class UserType extends AbstractType
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> label => role
|
||||
*/
|
||||
private function privilegedChoices(): array
|
||||
{
|
||||
return array_flip(array_intersect_key(Role::labels(), array_flip(Role::PRIVILEGED)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Codes already stored on the account are always offered, even when they are missing from
|
||||
* the configured catalog — otherwise saving the form would silently drop them.
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
<div class="pb-4 text-sm text-gray-500">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
<div class="pb-4 text-sm text-gray-500">
|
||||
aus BusPro: {{ syncedRoles | map_roles | join(', ') | default('–') }}
|
||||
</div>
|
||||
{{ form_start(form) }}
|
||||
<div class="grid lg:grid-cols-2 gap-y-4 lg:gap-x-8">
|
||||
{{ form_row(form.roles) }}
|
||||
|
||||
@@ -13,29 +13,37 @@ use Symfony\Component\Form\Forms;
|
||||
|
||||
class UserTypeTest extends TestCase
|
||||
{
|
||||
public function testStoredRolesArePreselectedWithoutTheImplicitRoleUser(): void
|
||||
public function testOnlyThePrivilegedRolesArePreselected(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setRoles([Role::TEAMER]);
|
||||
$user = (new User('[email protected]'))->setRoles([Role::TEAMER, Role::GROUPS_MANAGER]);
|
||||
|
||||
self::assertSame([Role::TEAMER], $this->createForm($user)->get('roles')->getData());
|
||||
self::assertSame([Role::GROUPS_MANAGER], $this->createForm($user)->get('roles')->getData());
|
||||
}
|
||||
|
||||
public function testSubmittingRolesDoesNotStoreTheImplicitRoleUser(): void
|
||||
public function testOnlyPrivilegedRolesAreOffered(): void
|
||||
{
|
||||
$choices = $this->createForm(new User('[email protected]'))->get('roles')->getConfig()->getOption('choices');
|
||||
|
||||
// The rest is synced from BusPro on every login and would be overwritten right away.
|
||||
self::assertSame(Role::PRIVILEGED, array_values($choices));
|
||||
}
|
||||
|
||||
public function testSubmittingRolesKeepsTheSyncedOnesAndNotTheImplicitRoleUser(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))->setRoles([Role::TEAMER]);
|
||||
|
||||
$form = $this->createForm($user);
|
||||
$form->submit(['roles' => [Role::TEAMER, Role::GROUPS_MANAGER], 'hotelCodes' => []]);
|
||||
$form->submit(['roles' => [Role::GROUPS_MANAGER], 'hotelCodes' => []]);
|
||||
|
||||
self::assertTrue($form->isSynchronized());
|
||||
// getRoles() prepends ROLE_USER; it must not have been persisted a second time.
|
||||
self::assertSame(['ROLE_USER', Role::TEAMER, Role::GROUPS_MANAGER], $user->getRoles());
|
||||
}
|
||||
|
||||
public function testClearingEveryCheckboxEmptiesTheAssignment(): void
|
||||
public function testClearingEveryCheckboxKeepsTheSyncedRoles(): void
|
||||
{
|
||||
$user = (new User('[email protected]'))
|
||||
->setRoles([Role::TEAMER])
|
||||
->setRoles([Role::TEAMER, Role::GROUPS_MANAGER])
|
||||
->setHotelCodes(['SSL'])
|
||||
;
|
||||
|
||||
@@ -43,7 +51,7 @@ class UserTypeTest extends TestCase
|
||||
$form->submit([]);
|
||||
|
||||
self::assertTrue($form->isSynchronized());
|
||||
self::assertSame(['ROLE_USER'], $user->getRoles());
|
||||
self::assertSame(['ROLE_USER', Role::TEAMER], $user->getRoles());
|
||||
self::assertSame([], $user->getHotelCodes());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user