From d20153c56966c4696d58a78350c7f5174c4cb101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 4 Jul 2023 00:31:13 +0200 Subject: [PATCH] Added basic logic for impersonation --- config/packages/security.yaml | 3 ++ config/permissions.yaml | 3 ++ src/Security/Voter/ImpersonateUserVoter.php | 60 +++++++++++++++++++++ src/Twig/UserExtension.php | 26 ++++++++- templates/_navbar.html.twig | 44 +++++++++++---- templates/admin/user_admin.html.twig | 6 +++ translations/messages.en.xlf | 48 +++++++++++------ 7 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 src/Security/Voter/ImpersonateUserVoter.php diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 342e38bf..92b9f188 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -21,6 +21,9 @@ security: user_checker: App\Security\UserChecker entry_point: form_login + # Enable user impersonation + switch_user: { role: CAN_SWITCH_USER } + two_factor: auth_form_path: 2fa_login check_path: 2fa_login_check diff --git a/config/permissions.yaml b/config/permissions.yaml index bcd3d79c..6cb798f5 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -190,6 +190,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co set_password: label: "perm.users.set_password" alsoSet: 'read' + impersonate: + label: "perm.users.impersonate" + alsoSet: ['set_password'] change_user_settings: label: "perm.users.change_user_settings" show_history: diff --git a/src/Security/Voter/ImpersonateUserVoter.php b/src/Security/Voter/ImpersonateUserVoter.php new file mode 100644 index 00000000..f1392568 --- /dev/null +++ b/src/Security/Voter/ImpersonateUserVoter.php @@ -0,0 +1,60 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\Voter; + +use App\Entity\UserSystem\User; +use App\Services\UserSystem\PermissionManager; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\User\UserInterface; + +class ImpersonateUserVoter extends Voter +{ + + public function __construct(private PermissionManager $permissionManager) + { + } + + protected function supports(string $attribute, mixed $subject): bool + { + return $attribute == 'CAN_SWITCH_USER' + && $subject instanceof UserInterface; + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + + if (!$user instanceof User || !$subject instanceof UserInterface) { + return false; + } + + //An disabled user is not allowed to do anything... + if ($user->isDisabled()) { + return false; + } + + return $this->permissionManager->inherit($user, 'users', 'impersonate') ?? false; + } +} \ No newline at end of file diff --git a/src/Twig/UserExtension.php b/src/Twig/UserExtension.php index be5d3c41..1d5c6588 100644 --- a/src/Twig/UserExtension.php +++ b/src/Twig/UserExtension.php @@ -46,6 +46,8 @@ use App\Entity\UserSystem\User; use App\Entity\LogSystem\AbstractLogEntry; use App\Repository\LogEntryRepository; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -57,7 +59,7 @@ final class UserExtension extends AbstractExtension { private readonly LogEntryRepository $repo; - public function __construct(EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em, private readonly Security $security) { $this->repo = $em->getRepository(AbstractLogEntry::class); } @@ -76,9 +78,31 @@ final class UserExtension extends AbstractExtension new TwigFunction('last_editing_user', fn(AbstractDBElement $element): ?User => $this->repo->getLastEditingUser($element)), /* Returns the user which has created the given entity. */ new TwigFunction('creating_user', fn(AbstractDBElement $element): ?User => $this->repo->getCreatingUser($element)), + new TwigFunction('impersonator_user', $this->getImpersonatorUser(...)), + new TwigFunction('impersonation_active', $this->isImpersonationActive(...)), ]; } + /** + * This function returns the user which has impersonated the current user. + * If the current user is not impersonated, null is returned. + * @return User|null + */ + public function getImpersonatorUser(): ?User + { + $token = $this->security->getToken(); + if ($token instanceof SwitchUserToken) { + return $token->getOriginalToken()->getUser(); + } + + return null; + } + + public function isImpersonationActive(): bool + { + return $this->security->isGranted('IS_IMPERSONATOR'); + } + /** * This function/filter generates a path. */ diff --git a/templates/_navbar.html.twig b/templates/_navbar.html.twig index e8ffdf3e..164848f1 100644 --- a/templates/_navbar.html.twig +++ b/templates/_navbar.html.twig @@ -37,23 +37,45 @@