Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Suplantación: switch_user

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

¿Alguna vez te has encontrado en una situación en la que estás ayudando a alguien en línea... y sería mucho más fácil si pudieras ver lo que está viendo en su pantalla... o, mejor, si pudieras hacerte cargo temporalmente y solucionar el problema tú mismo?

Sí, sólo tienes que hacer clic en el pequeño icono del clip para adjuntar el archivo. Debería estar como cerca de la parte inferior... un clip. ¿Qué es "adjuntar un archivo"? Oh... es... como enviar un "paquete"... pero en Internet.

Ah, los recuerdos. Symfony no puede ayudar a enseñar a tu familia cómo adjuntar archivos a un correo electrónico, pero sí puede ayudar a tu personal de atención al cliente a través de una función llamada suplantación de identidad. Muy sencillo: esto da a algunos usuarios el superpoder de iniciar sesión temporalmente como otra persona.

Activar el autentificador switch_user

Así es como se hace. En primer lugar, tenemos que habilitar la función. En security.yaml, bajo nuestro cortafuegos en algún lugar, añade switch_user: true:

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 46
switch_user: true
... lines 48 - 61

Esto activa un nuevo autentificador. Así que ahora tenemos nuestro CustomAuthenticator,form_login, remember_me y también switch_user.

¿Cómo funciona? Bien, ahora podemos "iniciar sesión" como cualquiera añadiendo ?_switch_user=a la URL y luego una dirección de correo electrónico. Vuelve al archivo de accesorios -src/Fixtures/AppFixtures.php - y desplázate hacia abajo. Tenemos otro usuario cuyo correo electrónico conocemos: es abraca_user@example.com:

... lines 1 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 20 - 51
UserFactory::createOne([
'email' => 'abraca_user@example.com',
]);
... lines 55 - 57
}
}

Cópialo, pégalo al final de la URL y...

Acceso denegado.

Por supuesto No podemos permitir que cualquiera haga esto. El autentificador sólo lo permitirá si tenemos un rol llamado ROLE_ALLOWED_TO_SWITCH. Vamos a dárselo a nuestros usuarios administradores. Podemos hacerlo a través de role_hierarchy. Aquí arriba, ROLE_ADMIN tieneROLE_COMMENT_ADMIN y ROLE_USER_ADMIN. Vamos a darles tambiénROLE_ALLOWED_TO_SWITCH:

security:
... lines 2 - 6
role_hierarchy:
ROLE_ADMIN: ['ROLE_COMMENT_ADMIN', 'ROLE_USER_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']
... lines 9 - 61

Y ahora... ¡vaya! ¡Hemos cambiado de usuario! ¡Es un icono de usuario diferente! Y lo más importante, abajo en la barra de herramientas de depuración de la web, vemos abraca_user@example.com... e incluso muestra quién es el usuario original.

Entre bastidores, cuando introdujimos la dirección de correo electrónico en la URL, el autentificador switch_userla cogió y luego aprovechó nuestro proveedor de usuarios para cargar ese objetoUser. Recuerda: tenemos un proveedor de usuarios que sabe cómo cargar usuarios de la base de datos consultando su propiedad email:

security:
... lines 2 - 13
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 21 - 61

Por eso usamos email en la URL.

Para "salir" y volver a nuestro usuario original, añade de nuevo ?_switch_user= con el especial _exit.

Cambios de estilo durante la suplantación

Pero antes de hacer eso, una vez que una persona del servicio de atención al cliente se ha cambiado a otra cuenta, queremos asegurarnos de que no olvida que se ha cambiado. Así que vamos a añadir un indicador muy obvio a nuestra página de que actualmente estamos "cambiados": hagamos este fondo de cabecera rojo.

Abre el diseño base: templates/base.html.twig. En la parte superior... busca el body y elnav... y lo dividiré en varias líneas. ¿Cómo podemos comprobar si estamos suplantando a alguien? Di is_granted() y pasa estoROLE_PREVIOUS_ADMIN. Si estás suplantando a alguien, tendrás este rol.

En ese caso, añade style="background-color: red"... con !important para anular el estilo nav:

... line 1
<html>
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
... lines 20 - 66
</nav>
... lines 68 - 72
</body>
</html>

¡Vamos a verlo! Actualiza y... ¡ja! Es una pista muy obvia de que estamos suplantando.

Ayudar al usuario a acabar con la suplantación

Para ayudar al usuario a acabar con la suplantación, vamos a añadir un enlace. Baja al menú desplegable. Una vez más, comprueba si is_granted('ROLE_PREVIOUS_ADMIN'). Copia el enlace de abajo... pégalo... y envía al usuario a - app_homepage pero pasa un parámetro extra_switch_user establecido en _exit.

Si pasas algo al segundo argumento de path() que no sea un comodín en la ruta, Symfony lo establecerá como parámetro de consulta. Así que esto debería darnos exactamente lo que queremos. Para el texto, di "Salir de la suplantación":

... line 1
<html>
... lines 3 - 14
<body>
<nav
class="navbar navbar-expand-lg navbar-light bg-light px-1"
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }}
>
<div class="container-fluid">
... lines 21 - 29
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 31 - 41
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
... lines 44 - 54
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<li>
<a class="dropdown-item" href="{{ path('app_homepage', {
'_switch_user': '_exit'
}) }}">Exit Impersonation</a>
</li>
{% endif %}
... lines 63 - 65
</ul>
</div>
{% else %}
... lines 69 - 70
{% endif %}
</div>
</div>
</nav>
... lines 75 - 79
</body>
</html>

¡Inténtalo! Refresca. Es obvio que estamos suplantando... pulsa "Salir de la suplantación" y... volvemos a ser abraca_admin@example.com. ¡Qué bien!

Por cierto, si necesitas más control sobre los usuarios a los que se puede cambiar, puedes escuchar el evento SwitchUserEvent. Para evitar el cambio, lanza un eventoAuthenticationException. Más adelante hablaremos de los escuchadores de eventos.

A continuación: hagamos un breve descanso para hacer algo totalmente divertido, pero... no relacionado con la seguridad: construir una ruta de usuario de la API.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}