From 426d4d3e233eebaf9ebf46e078e9328e71102efd Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Mon, 26 Feb 2018 13:59:08 -0500 Subject: [PATCH 1/3] [WIP] add example for using a voter to restrict switch_user --- security/impersonating_user.rst | 172 ++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 41a1e460f51..d67fe9b6a48 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -187,6 +187,178 @@ setting: ), )); +If you need more control over user switching, but don't require the complexity +of a full ACL implementation, you can use a security voter. For example, you +may want to allow employees to be able to impersonate a user with the +``ROLE_CUSTOMER`` role without giving them the ability to impersonate a more +elevated user such as an administrator. + +First, create the voter class:: + + namespace AppBundle\Security\Voter; + + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + use Symfony\Component\Security\Core\Role\RoleHierarchy; + use Symfony\Component\Security\Core\User\UserInterface; + + class SwitchToCustomerVoter extends Voter + { + private $roleHierarchy; + + public function __construct(RoleHierarchy $roleHierarchy) + { + $this->roleHierarchy = $roleHierarchy; + } + + protected function supports($attribute, $subject) + { + return in_array($attribute, ['ROLE_ALLOWED_TO_SWITCH']) + && $subject instanceof UserInterface; + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + // if the user is anonymous or if the subject is not a user, do not grant access + if (!$user instanceof UserInterface || !$subject instanceof UserInterface) { + return false; + } + + if (in_array('ROLE_CUSTOMER', $subject->getRoles()) + && $this->hasSwitchToCustomerRole($token)) { + return self::ACCESS_GRANTED; + } + + return false; + } + + private function hasSwitchToCustomerRole(TokenInterface $token) + { + $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); + foreach ($roles as $role) { + if ($role->getRole() === 'ROLE_SWITCH_TO_CUSTOMER') { + return true; + } + } + + return false; + } + } + +.. caution:: + + Notice that when checking for the ``ROLE_CUSTOMER`` role on the target user, only the roles + explicitly assigned to the user are checked rather than checking all reachable roles from + the role hierarchy. The reason for this is to avoid accidentally granting access to an + elevated user that may have inherited the role via the hierarchy. This logic is specific + to the example, but keep this in mind when writing your own voter. + +Next, add the roles to the security configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + role_hierarchy: + ROLE_CUSTOMER: [ROLE_USER] + ROLE_EMPLOYEE: [ROLE_USER, ROLE_SWITCH_TO_CUSTOMER] + ROLE_SUPER_ADMIN: [ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH] + + .. code-block:: xml + + + + + + + + ROLE_USER + ROLE_USER, ROLE_SWITCH_TO_CUSTOMER + ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', array( + // ... + + 'role_hierarchy' => array( + 'ROLE_CUSTOMER' => 'ROLE_USER', + 'ROLE_EMPLOYEE' => 'ROLE_USER, ROLE_SWITCH_TO_CUSTOMER', + 'ROLE_SUPER_ADMIN' => array( + 'ROLE_EMPLOYEE', + 'ROLE_ALLOWED_TO_SWITCH', + ), + ), + )); + +Thanks to autowiring, we only need to configure the role hierarchy argument when registering +the voter as a service: + +.. configuration-block:: + + .. code-block:: yaml + + // config/services.yaml + services: + # ... + + App\Security\Voter\SwitchToCustomerVoter: + arguments: + $roleHierarchy: "@security.role_hierarchy" + + .. code-block:: xml + + + + + + + + + "@security.role_hierarchy" + + + + + .. code-block:: php + + // config/services.php + use App\Security\Voter\SwitchToCustomerVoter; + use Symfony\Component\DependencyInjection\Definition; + + // Same as before + $definition = new Definition(); + + $definition + ->setAutowired(true) + ->setAutoconfigured(true) + ->setPublic(false) + ; + + $this->registerClasses($definition, 'App\\', '../src/*', '../src/{Entity,Migrations,Tests}'); + + // Explicitly configure the service + $container->getDefinition(SwitchToCustomerVoter::class) + ->setArgument('$roleHierarchy', '@security.role_hierarchy'); + +Now a user who has the ``ROLE_SWITCH_TO_CUSTOMER`` role can switch to a user who explicitly has the +``ROLE_CUSTOMER`` role, but not other users. + Events ------ From f9137fa5d107406ad7c1c5722ba517619a82f841 Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Tue, 3 Apr 2018 22:44:11 -0400 Subject: [PATCH 2/3] Fix recommendations from community review --- security/impersonating_user.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 7b83e7c187e..e78209061c6 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -195,7 +195,7 @@ elevated user such as an administrator. First, create the voter class:: - namespace AppBundle\Security\Voter; + namespace App\Security\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -227,7 +227,7 @@ First, create the voter class:: if (in_array('ROLE_CUSTOMER', $subject->getRoles()) && $this->hasSwitchToCustomerRole($token)) { - return self::ACCESS_GRANTED; + return true; } return false; @@ -340,6 +340,7 @@ the voter as a service: // config/services.php use App\Security\Voter\SwitchToCustomerVoter; use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; // Same as before $definition = new Definition(); @@ -354,7 +355,7 @@ the voter as a service: // Explicitly configure the service $container->getDefinition(SwitchToCustomerVoter::class) - ->setArgument('$roleHierarchy', '@security.role_hierarchy'); + ->setArgument('$roleHierarchy', new Reference('security.role_hierarchy')); Now a user who has the ``ROLE_SWITCH_TO_CUSTOMER`` role can switch to a user who explicitly has the ``ROLE_CUSTOMER`` role, but not other users. From 458ca2ee0669912a203db83e5916beb53c6eaf98 Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Fri, 18 May 2018 09:42:03 -0400 Subject: [PATCH 3/3] implemented community suggestions - removed role_heirarchy from switch user voter to simplify - removed double-spaces between sentences. - added versionadded - added header --- security/impersonating_user.rst | 133 +++----------------------------- 1 file changed, 11 insertions(+), 122 deletions(-) diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index e78209061c6..085b623328d 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -187,30 +187,28 @@ setting: ), )); +Limiting User Switching +----------------------- + If you need more control over user switching, but don't require the complexity -of a full ACL implementation, you can use a security voter. For example, you +of a full ACL implementation, you can use a security voter. For example, you may want to allow employees to be able to impersonate a user with the ``ROLE_CUSTOMER`` role without giving them the ability to impersonate a more elevated user such as an administrator. -First, create the voter class:: +.. versionadded:: 4.1 + The target user was added as the voter subject parameter in Symfony 4.1. + +Create the voter class:: namespace App\Security\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; - use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\User\UserInterface; class SwitchToCustomerVoter extends Voter { - private $roleHierarchy; - - public function __construct(RoleHierarchy $roleHierarchy) - { - $this->roleHierarchy = $roleHierarchy; - } - protected function supports($attribute, $subject) { return in_array($attribute, ['ROLE_ALLOWED_TO_SWITCH']) @@ -235,8 +233,7 @@ First, create the voter class:: private function hasSwitchToCustomerRole(TokenInterface $token) { - $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); - foreach ($roles as $role) { + foreach ($token->getRoles() as $role) { if ($role->getRole() === 'ROLE_SWITCH_TO_CUSTOMER') { return true; } @@ -246,116 +243,8 @@ First, create the voter class:: } } -.. caution:: - - Notice that when checking for the ``ROLE_CUSTOMER`` role on the target user, only the roles - explicitly assigned to the user are checked rather than checking all reachable roles from - the role hierarchy. The reason for this is to avoid accidentally granting access to an - elevated user that may have inherited the role via the hierarchy. This logic is specific - to the example, but keep this in mind when writing your own voter. - -Next, add the roles to the security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - role_hierarchy: - ROLE_CUSTOMER: [ROLE_USER] - ROLE_EMPLOYEE: [ROLE_USER, ROLE_SWITCH_TO_CUSTOMER] - ROLE_SUPER_ADMIN: [ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH] - - .. code-block:: xml - - - - - - - - ROLE_USER - ROLE_USER, ROLE_SWITCH_TO_CUSTOMER - ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... - - 'role_hierarchy' => array( - 'ROLE_CUSTOMER' => 'ROLE_USER', - 'ROLE_EMPLOYEE' => 'ROLE_USER, ROLE_SWITCH_TO_CUSTOMER', - 'ROLE_SUPER_ADMIN' => array( - 'ROLE_EMPLOYEE', - 'ROLE_ALLOWED_TO_SWITCH', - ), - ), - )); - -Thanks to autowiring, we only need to configure the role hierarchy argument when registering -the voter as a service: - -.. configuration-block:: - - .. code-block:: yaml - - // config/services.yaml - services: - # ... - - App\Security\Voter\SwitchToCustomerVoter: - arguments: - $roleHierarchy: "@security.role_hierarchy" - - .. code-block:: xml - - - - - - - - - "@security.role_hierarchy" - - - - - .. code-block:: php - - // config/services.php - use App\Security\Voter\SwitchToCustomerVoter; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - // Same as before - $definition = new Definition(); - - $definition - ->setAutowired(true) - ->setAutoconfigured(true) - ->setPublic(false) - ; - - $this->registerClasses($definition, 'App\\', '../src/*', '../src/{Entity,Migrations,Tests}'); - - // Explicitly configure the service - $container->getDefinition(SwitchToCustomerVoter::class) - ->setArgument('$roleHierarchy', new Reference('security.role_hierarchy')); +Thanks to service autoconfiguration and autowiring, this new voter is automatically +registered as a service and tagged as a security voter. Now a user who has the ``ROLE_SWITCH_TO_CUSTOMER`` role can switch to a user who explicitly has the ``ROLE_CUSTOMER`` role, but not other users.