From cf3bd005c4166d94c715cc3ca091d3a46b084f0b Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Mon, 26 Feb 2018 13:59:08 -0500 Subject: [PATCH 1/4] [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 4061d5ea625..2c16cd53e72 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -187,6 +187,178 @@ also adjust the query parameter name via the ``parameter`` 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 e6c569b8dbedf36f83f1f934803d450cff9409c4 Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Tue, 3 Apr 2018 22:44:11 -0400 Subject: [PATCH 2/4] 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 2c16cd53e72..224b41885bc 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 876257be889711671f4ac98466fbaee9497cc632 Mon Sep 17 00:00:00 2001 From: Jody Mickey Date: Fri, 18 May 2018 09:42:03 -0400 Subject: [PATCH 3/4] 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 224b41885bc..8dee8cb75e5 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -187,30 +187,28 @@ also adjust the query parameter name via the ``parameter`` 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. From 7853ec02008b2046ab704a300feaf8ef8f86855a Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 4 Jan 2019 09:23:22 +0100 Subject: [PATCH 4/4] Minor tweaks --- security/impersonating_user.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index 8dee8cb75e5..c69dcc4b2ab 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -190,13 +190,14 @@ also adjust the query parameter name via the ``parameter`` 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 -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 +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. .. versionadded:: 4.1 + The target user was added as the voter subject parameter in Symfony 4.1. Create the voter class:: @@ -223,7 +224,7 @@ Create the voter class:: return false; } - if (in_array('ROLE_CUSTOMER', $subject->getRoles()) + if (in_array('ROLE_CUSTOMER', $subject->getRoles()) && $this->hasSwitchToCustomerRole($token)) { return true; } @@ -238,16 +239,19 @@ Create the voter class:: return true; } } - + return false; } } -Thanks to service autoconfiguration and autowiring, this new voter is automatically -registered as a service and tagged as a security voter. +To enable the new voter in the app, register it as a service and +:doc:`tag it ` with the ``security.voter`` +tag. If you're using the +:ref:`default services.yaml configuration `, +this is already done for you, thanks to :ref:`autoconfiguration `. -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. +Now a user who has the ``ROLE_SWITCH_TO_CUSTOMER`` role can switch to a user who +has the ``ROLE_CUSTOMER`` role, but not other users. Events ------