Buy Access to Course
23.

Votante de seguridad

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Nuestra seguridad se está convirtiendo en una casa de locos, lo que no me gusta. Quiero que mi lógica de seguridad sea simple y centralizada. La forma de hacerlo en Symfony es con un votador. Vamos a crear uno.

En la línea de comandos, ejecuta:

php ./bin/console make:voter

Llámalo DragonTreasureVoter. Es bastante común tener un votante por entidad para la que necesites lógica de seguridad. Así que este votante tomará todas las decisiones relacionadas con DragonTreasure: puede el usuario actual editar una, borrar una, ver una: lo que eventualmente necesitemos.

Ve a abrirlo: src/Security/Voter/DragonTreasureVoter.php:

45 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 2
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\User\UserInterface;
class DragonTreasureVoter extends Voter
{
public const EDIT = 'POST_EDIT';
public const VIEW = 'POST_VIEW';
protected function supports(string $attribute, mixed $subject): bool
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof \App\Entity\DragonTreasure;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case self::EDIT:
// logic to determine if the user can EDIT
// return true or false
break;
case self::VIEW:
// logic to determine if the user can VIEW
// return true or false
break;
}
return false;
}
}

Antes de hablar de esta clase, déjame mostrarte cómo la utilizaremos. EnDragonTreasure, vamos a seguir utilizando la función is_granted(). Pero para el primer argumento, pasa EDIT... que es sólo una cadena que me estoy inventando: ya verás cómo se utiliza en el votante. Luego pasa object:

249 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Patch(
security: 'is_granted("EDIT", object)',
// ... line 43
),
// ... lines 45 - 47
],
// ... lines 49 - 65
)]
// ... lines 67 - 87
class DragonTreasure
{
// ... lines 90 - 247
}

Normalmente pasamos a is_granted() un único argumento: ¡un papel! Pero también puedes pasarle cualquier cadena aleatoria como EDIT... siempre que tengas un votante configurado para manejar eso. Si tu votante necesita información adicional para tomar su decisión, puedes pasársela como segundo argumento.

A grandes rasgos, estamos preguntando al sistema de seguridad si el usuario actual tiene permiso o no para EDIT este objeto DragonTreasure. DragonTreasureVoter tomará esa decisión.

Copia esto y pégalo abajo para securityPostDenormalize:

249 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Patch(
security: 'is_granted("EDIT", object)',
securityPostDenormalize: 'is_granted("EDIT", object)',
),
// ... lines 45 - 47
],
// ... lines 49 - 65
)]
// ... lines 67 - 87
class DragonTreasure
{
// ... lines 90 - 247
}

Cómo funcionan los votantes

Así que el asunto es el siguiente: cada vez que se llama a is_granted() -desde cualquier lugar, no sólo desde API Platform- Symfony recorre una lista de clases "votantes" e intenta averiguar cuál de ellas sabe cómo tomar esa decisión. Cuando comprobamos un rol, hay un votante existente que sabe cómo manejarlo. En el caso de EDIT, no hay ningún votante principal que sepa cómo manejarlo. Así que haremos que DragonTreasureVoter pueda manejarlo.

Para determinar quién puede manejar una llamada a isGranted, Symfony llama a supports() en cada votante pasándole los mismos dos argumentos. En nuestro caso, $attribute seráEDIT y $subject será el objeto DragonTreasure:

45 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 8
class DragonTreasureVoter extends Voter
{
// ... lines 11 - 13
protected function supports(string $attribute, mixed $subject): bool
{
// ... lines 16 - 19
}
// ... lines 21 - 43
}

MakeBundle generó un votante que se encarga de comprobar si podemos "editar" o "ver" un DragonTreasure. Ahora mismo no necesitamos esa "vista", así que la borraré. A continuación, cambiaré esto por una instancia de DragonTreasure y volveré a escribir el final y le daré al tabulador para añadir la declaración use... sólo para limpiar las cosas:

40 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 9
class DragonTreasureVoter extends Voter
{
public const EDIT = 'EDIT';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT])
&& $subject instanceof DragonTreasure;
}
// ... lines 19 - 38
}

Así, si alguien llama a isGranted() y le pasa la cadena EDIT y un objeto DragonTreasure, sabremos cómo tomar esa decisión.

Ah, y tengo que cambiar el valor de la constante a EDIT para que coincida con la cadena EDIT que pasamos a is_granted().

Si devolvemos true desde supports(), Symfony llamará entonces a voteOnAttribute(). Muy sencillo: devolvemos true si el usuario debe tener acceso, false en caso contrario.

Para empezar, basta con return false:

40 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 9
class DragonTreasureVoter extends Voter
{
// ... lines 12 - 19
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
return false;
// ... lines 23 - 37
}
}

Si hemos jugado bien nuestras cartas, nuestro votante se abalanzará como un superhéroe hiperactivo cada vez que hagamos una petición PATCH y cerrará de golpe la puerta de acceso. Antes de probar esa teoría, elimina el caso "vista" de aquí abajo:

40 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 9
class DragonTreasureVoter extends Voter
{
// ... lines 12 - 19
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
return false;
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case self::EDIT:
// logic to determine if the user can EDIT
// return true or false
break;
}
return false;
}
}

Bien, ¡asegurémonos de que nuestras pruebas fallan! Ejecuta:

symfony php bin/phpunit

Y... ¡sí! Fallan dos pruebas: ambas porque se deniega el acceso. Nuestro votante está siendo llamado.

Añadir la lógica del votante

De vuelta a la clase, a voteOnAttribute() se le pasa el atributo - EDIT - el$subject - un objeto DragonTreasure y un $token, que es una envoltura alrededor del objeto User actual. Así que primero comprobamos que el usuario está autenticado.

Después, assert() que $subject es una instancia de DragonTreasure porque este método sólo debería llamarse cuando supports() devuelve true:

43 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 9
class DragonTreasureVoter extends Voter
{
// ... lines 12 - 19
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
assert($subject instanceof DragonTreasure);
// ... (check conditions and return true to grant permission) ...
// ... lines 31 - 40
}
}

Principalmente escribo esto para que mi editor sepa que $subject es una DragonTreasure:assert() es una forma práctica de hacerlo.

La declaración switch sólo tiene un case en este momento. Y aquí es donde vivirá nuestra lógica. Muy sencillo: si $subject - que es el DragonTreasure - ->getOwner()es igual a $user, entonces devuelve true. En caso contrario, será igual a break y devolveráfalse:

43 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 9
class DragonTreasureVoter extends Voter
{
// ... lines 12 - 19
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
// ... lines 22 - 29
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case self::EDIT:
if ($subject->getOwner() === $user) {
return true;
}
break;
}
return false;
}
}

Ésta no es toda la lógica que necesitamos, ¡pero es un buen comienzo!

Prueba ahora las pruebas:

symfony php bin/phpunit

¡Un fallo menos!

Comprobación de roles en el votante

¿Qué es lo siguiente? Bueno, no tenemos una prueba para ello, pero si nos autenticamos con un token de la API, para editar un tesoro, necesitas ROLE_TREASURE_EDIT, que puedes obtener a través del ámbito del token.

Así que, en el votante, tenemos que comprobar si el usuario tiene ese rol. Añade un método __construct()y autoconecta Security - el del SecurityBundle - $security:

52 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 5
use Symfony\Bundle\SecurityBundle\Security;
// ... lines 7 - 10
class DragonTreasureVoter extends Voter
{
// ... lines 13 - 14
public function __construct(private Security $security)
{
}
// ... lines 18 - 50
}

Entonces, a continuación, antes de comprobar el propietario, si no$this->security->isGranted('ROLE_TREASURE_EDIT'), entonces devuelve definitivamentefalse:

52 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 10
class DragonTreasureVoter extends Voter
{
// ... lines 13 - 24
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
// ... lines 27 - 35
switch ($attribute) {
case self::EDIT:
if (!$this->security->isGranted('ROLE_TREASURE_EDIT')) {
return false;
}
if ($subject->getOwner() === $user) {
return true;
}
break;
}
// ... lines 48 - 49
}
}

La última prueba que falla es comprobar que un administrador puede parchear para editar cualquier tesoro. Como ya hemos inyectado el servicio Security, esto es fácil.

Hagamos como si los usuarios administradores pudieran hacer cualquier cosa. Así que por encima de switch, si $this->security->isGranted('ROLE_ADMIN'), entonces devuelve true:

56 lines | src/Security/Voter/DragonTreasureVoter.php
// ... lines 1 - 10
class DragonTreasureVoter extends Voter
{
// ... lines 13 - 24
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
// ... lines 27 - 32
if ($this->security->isGranted('ROLE_ADMIN')) {
return true;
}
assert($subject instanceof DragonTreasure);
// ... lines 38 - 53
}
}

Momento de la verdad:

symfony php bin/phpunit

¡Voilà! Nuestra lógica ha encontrado un hogar acogedor dentro del votante, la expresión securityes ahora tan sencilla que casi da miedo, y hemos conseguido escribir nuestra lógica en PHP.

A continuación: vamos a explorar la posibilidad de ocultar determinados campos en la respuesta en función del usuario.