@@ -187,6 +187,178 @@ also adjust the query parameter name via the ``parameter`` setting:
187
187
),
188
188
));
189
189
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
+
F438
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
+
190
362
Events
191
363
------
192
364
0 commit comments