@@ -187,6 +187,178 @@ also adjust the query parameter name via the ``parameter`` setting:
187187 ),
188188 ));
189189
190+ If you need more control over user switching, but don't require the complexity
191+ of a full ACL implementation, you can use a security voter. For example, you
192+ may want to allow employees to be able to impersonate a user with the
193+ ``ROLE_CUSTOMER `` role without giving them the ability to impersonate a more
194+ elevated user such as an administrator.
195+
196+ First, create the voter class::
197+
198+ namespace AppBundle\Security\Voter;
199+
200+ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
201+ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
202+ use Symfony\Component\Security\Core\Role\RoleHierarchy;
203+ use Symfony\Component\Security\Core\User\UserInterface;
204+
205+ class SwitchToCustomerVoter extends Voter
206+ {
207+ private $roleHierarchy;
208+
209+ public function __construct(RoleHierarchy $roleHierarchy)
210+ {
211+ $this->roleHierarchy = $roleHierarchy;
212+ }
213+
214+ protected function supports($attribute, $subject)
215+ {
216+ return in_array($attribute, ['ROLE_ALLOWED_TO_SWITCH'])
217+ && $subject instanceof UserInterface;
218+ }
219+
220+ protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
221+ {
222+ $user = $token->getUser();
223+ // if the user is anonymous or if the subject is not a user, do not grant access
224+ if (!$user instanceof UserInterface || !$subject instanceof UserInterface) {
225+ return false;
226+ }
227+
228+ if (in_array('ROLE_CUSTOMER', $subject->getRoles())
229+ && $this->hasSwitchToCustomerRole($token)) {
230+ return self::ACCESS_GRANTED;
231+ }
232+
233+ return false;
234+ }
235+
236+ private function hasSwitchToCustomerRole(TokenInterface $token)
237+ {
238+ $roles = $this->roleHierarchy->getReachableRoles($token->getRoles());
239+ foreach ($roles as $role) {
240+ if ($role->getRole() === 'ROLE_SWITCH_TO_CUSTOMER') {
241+ return true;
242+ }
243+ }
244+
245+ return false;
246+ }
247+ }
248+
249+ .. caution ::
250+
251+ Notice that when checking for the ``ROLE_CUSTOMER `` role on the target user, only the roles
252+ explicitly assigned to the user are checked rather than checking all reachable roles from
253+ the role hierarchy. The reason for this is to avoid accidentally granting access to an
254+ elevated user that may have inherited the role via the hierarchy. This logic is specific
255+ to the example, but keep this in mind when writing your own voter.
256+
257+ Next, add the roles to the security configuration:
258+
259+ .. configuration-block ::
260+
261+ .. code-block :: yaml
262+
263+ # config/packages/security.yaml
264+ security :
265+ # ...
266+
267+ role_hierarchy :
268+ ROLE_CUSTOMER : [ROLE_USER]
269+ ROLE_EMPLOYEE : [ROLE_USER, ROLE_SWITCH_TO_CUSTOMER]
270+ ROLE_SUPER_ADMIN : [ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH]
271+
272+ .. code-block :: xml
273+
274+ <!-- config/packages/security.xml -->
275+ <?xml version =" 1.0" encoding =" UTF-8" ?>
276+ <srv : container xmlns =" http://symfony.com/schema/dic/security"
277+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
278+ xmlns : srv =" http://symfony.com/schema/dic/services"
279+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
280+ http://symfony.com/schema/dic/services/services-1.0.xsd" >
281+ <config >
282+ <!-- ... -->
283+
284+ <role id =" ROLE_CUSTOMER" >ROLE_USER</role >
285+ <role id =" ROLE_EMPLOYEE" >ROLE_USER, ROLE_SWITCH_TO_CUSTOMER</role >
286+ <role id =" ROLE_SUPER_ADMIN" >ROLE_EMPLOYEE, ROLE_ALLOWED_TO_SWITCH</role >
287+ </config >
288+ </srv : container >
289+
290+ .. code-block :: php
291+
292+ // config/packages/security.php
293+ $container->loadFromExtension('security', array(
294+ // ...
295+
296+ 'role_hierarchy' => array(
297+ 'ROLE_CUSTOMER' => 'ROLE_USER',
298+ 'ROLE_EMPLOYEE' => 'ROLE_USER, ROLE_SWITCH_TO_CUSTOMER',
299+ 'ROLE_SUPER_ADMIN' => array(
300+ 'ROLE_EMPLOYEE',
301+ 'ROLE_ALLOWED_TO_SWITCH',
302+ ),
303+ ),
304+ ));
305+
306+ Thanks to autowiring, we only need to configure the role hierarchy argument when registering
307+ the voter as a service:
308+
309+ .. configuration-block ::
310+
311+ .. code-block :: yaml
312+
313+ // config/services.yaml
314+ services :
315+ # ...
316+
317+ App\Security\Voter\SwitchToCustomerVoter :
318+ arguments :
319+ $roleHierarchy : " @security.role_hierarchy"
320+
321+ .. code-block :: xml
322+
323+ <!-- config/services.xml -->
324+ <?xml version =" 1.0" encoding =" UTF-8" ?>
325+ <container xmlns =" http://symfony.com/schema/dic/services"
326+ xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
327+ xsi : schemaLocation =" http://symfony.com/schema/dic/services
328+ http://symfony.com/schema/dic/services/services-1.0.xsd" >
329+
330+ <services >
331+ <!-- ... -->
332+ <service id =" App\Security\Voter\SwitchToCustomerVoter" >
333+ <argument key =" $roleHierarchy" >"@security.role_hierarchy"</argument >
334+ </service >
335+ </services >
336+ </container >
337+
338+ .. code-block :: php
339+
340+ // config/services.php
341+ use App\Security\Voter\SwitchToCustomerVoter;
342+ use Symfony\Component\DependencyInjection\Definition;
343+
344+ // Same as before
345+ $definition = new Definition();
346+
347+ $definition
348+ ->setAutowired(true)
349+ ->setAutoconfigured(true)
350+ ->setPublic(false)
351+ ;
352+
353+ $this->registerClasses($definition, 'App\\', '../src/*', '../src/{Entity,Migrations,Tests}');
354+
355+ // Explicitly configure the service
356+ $container->getDefinition(SwitchToCustomerVoter::class)
357+ ->setArgument('$roleHierarchy', '@security.role_hierarchy');
358+
359+ Now a user who has the ``ROLE_SWITCH_TO_CUSTOMER `` role can switch to a user who explicitly has the
360+ ``ROLE_CUSTOMER `` role, but not other users.
361+
190362Events
191363------
192364
0 commit comments