Seguridad en el campo con Parche
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEn un giro heroico de valentía, hemos decidido ejecutar todas las pruebas del tesoro del dragón:
symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php
Y... tenemos tres fallos, incluido uno detestAdminCanPatchToEditTreasure
en la línea 200... que dice->assertJsonMatched('isPublished', true)
. Eso falla porque... ¡no tenemos en absoluto un campo isPublished
en nuestro DragonTreasureApi
!
Añadir el campo isPublished
Esto se debe a que se trata de un campo interesante. Antes, este campo sólo lo podían leer los usuarios administradores o el propietario. Volvamos a añadir este campo y mantengamos ese comportamiento. Digamos public bool $isPublished = false
.
// ... lines 1 - 40 | |
class DragonTreasureApi | |
{ | |
// ... lines 43 - 58 | |
public bool $isPublished = false; | |
// ... lines 60 - 68 | |
} |
Entonces... entra en el mapeador para rellenar esto. Aquí abajo, deshazte de TODO
y di $entity->setIsPublished($dto->isPublished)
.
// ... lines 1 - 12 | |
class DragonTreasureApiToEntityMapper implements MapperInterface | |
{ | |
// ... lines 15 - 35 | |
public function populate(object $from, object $to, array $context): object | |
{ | |
// ... lines 38 - 51 | |
$entity->setIsPublished($dto->isPublished); | |
// ... lines 53 - 54 | |
} | |
} |
Así, si cambiamos isPublished
en la llamada a la API, el nuevo valor se sincronizará con la entidad.
En el otro lado... no importa dónde... di$dto->isPublished = $entity->getIsPublished()
.
// ... lines 1 - 13 | |
class DragonTreasureEntityToApiMapper implements MapperInterface | |
{ | |
// ... lines 16 - 33 | |
public function populate(object $from, object $to, array $context): object | |
{ | |
// ... lines 36 - 41 | |
$dto->isPublished = $entity->getIsPublished(); | |
// ... lines 43 - 53 | |
} | |
} |
¡Genial! Aún no tenemos seguridad... así que cuando ejecutamos las pruebas:
symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php
Algunas pasan, pero la original sigue fallando - testGetCollectionOfTreasures
- porque no espera que isPublished
esté ahí.
Mostrar condicionalmente isPublished mediante seguridad
Fíjate: ésta es la primera prueba, y en la parte inferior hemos afirmado que éstas son las propiedades exactas que deberíamos tener si obtenemos tesoros como usuario anónimo. Por tanto, como no somos el propietario ni un administrador, no deberíamos ver isPublished
¿Cómo podemos hacerlo? Antes hemos trabajado con DragonTreasureApiVoter
. Cuando lo llamamos con el atributo EDIT
, comprueba si somos administradores y, si lo somos, nos da acceso. También comprueba si somos el propietario. Ésta es exactamente la lógica que queremos utilizar para determinar si el campo isPublished
debe serializarse.
Así que... ¡vamos a utilizarla! Sobre esta propiedad, digamos#[ApiProperty(security: 'is_granted("EDIT", object)')]
.
// ... lines 1 - 40 | |
class DragonTreasureApi | |
{ | |
// ... lines 43 - 58 | |
security: 'is_granted("EDIT", object)') | (|
public bool $isPublished = false; | |
// ... lines 61 - 69 | |
} |
Si quieres, puedes cambiar este atributo por otra cosa -como OWNER
-, si te resulta más claro. EDIT
suena un poco raro aquí... ya que sólo estamos decidiendo si debemos incluir este campo en la respuesta... no "editarlo"... pero tú decides.
Y lo que es más importante, veamos si esto funciona. Ejecuta las pruebas:
symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php
¡Se ha solucionado nuestra primera prueba! Ya no se muestra el campo isPublished
. Pero, curiosamente, hemos hecho fallar otra prueba. ¡A la porra! Ahora estestPublishTreasure
- falla en la línea 244.
Vamos a buscarlo. Vale, como su nombre indica, estamos probando si podemos publicar este tesoro. Creamos un tesoro que es'isPublished' => false
, iniciamos sesión como su propietario y, a continuación, enviamos una petición a patch()
para establecer isPublished
en true
. Por último, afirmamos que el JSON de la respuesta tiene isPublished
verdadero. Y eso es lo que falla.
La opción de seguridad ApiProperty en las operaciones de parcheo
¿Por qué? Me llevó un poco de depuración desentrañar este misterio. El problema es que, cuando se deserializa el JSON, isPublished
no es escribible.
La expresión security
se llama tanto al serializar como al deserializar: al tomar el JSON de la petición y al actualizar el objeto. Por alguna razón, durante la deserialización, ¡nuestra expresión security
devuelve false!
La razón es... posiblemente un error: Tengo una incidencia abierta en API Platform. Cuando realizas una petición a patch()
, nuestro proveedor de datos carga primero el objeto desde la base de datos. A pesar de ello, cuando se llama a la expresión durante la deserialización,object
siempre es nulo. Y como nuestro votante sólo admite si object
es un DragonTreasureApi
, éste devuelve false
. En última instancia, ningún votante admite esto, y cuando ocurre, se deniega el acceso. El resultado final es que isPublished
no es escribible.
La solución es un poco extraña, pero quédate conmigo. Básicamente, vamos a permitir el acceso a este campo si object === null
ois_granted("EDIT", object)
.
// ... lines 1 - 40 | |
class DragonTreasureApi | |
{ | |
// ... lines 43 - 58 | |
// Object is null ONLY during deserialization: so this allows isPublished | |
// to be writable in ALL cases (which is ok because the operations are secured). | |
// During serialization, object will always be a DragonTreasureApi, so our | |
// voter is called. | |
security: 'object === null or is_granted("EDIT", object)') | (|
public bool $isPublished = false; | |
// ... lines 65 - 73 | |
} |
Piensa en esto. Si estamos leyendo un DragonTreasure
, entonces object
nunca es null
. Siempre tendremos un objeto, por lo que siempre se llamará al votante. Este object === null
sólo ocurrirá durante la deserialización: cuando estemos comprobando si podemos escribir este campo. Esto hace que el campo sea siempre escribible. Esto parece un problema, pero no lo es, porque ya tenemos security
aquí arriba en Post()
y Patch()
. En Patch
, sólo el propietario puede editar este objeto. Así que una vez superada la seguridad de Patch
, ya sabemos que puede editar este objeto. Así que, aquí abajo, está bien que siempre podamos editar este campo.
Si esto te parece demasiado raro, otra estrategia es dejar la seguridad API fuera del campo por completo. Entonces, utilizaríamos el mapeador para manejar la configuración condicional del campo isPublished
. Podríamos poner aquí una lógica de seguridad que dijera básicamente
Sólo establece el campo
isPublished
en el DTO si eres el propietario. En caso contrario dejaisPublished
nulo por defecto.
Es bueno recordar que tenemos el control total de los datos a través de nuestros mapeadores.
Bien, volvamos atrás y añadamos de nuevo nuestra expresión de seguridad. Y vuelve también al mapeador: Acabo de darme cuenta de que también queremos mantener ese código isPublished
... sólo que no en la declaración if
.
Muy bien, ahora vuelve a ejecutar todas las pruebas.
symfony php bin/phpunit tests/Functional/DragonTreasureResourceTest.php
Y... ¡ooh! ¡Tan cerca! Sólo nos queda un fallo en testPublishTreasure
. Esto prueba que, cuando se publica un tesoro, enviamos una notificación. Veamos cómo podemos resolverlo en nuestro nuevo sistema
Dear Symfonycast,
I got a question about updating data, On the frontend I am useing react-admin.
When I Post new entity, I got no issues.
But when I update a entity, It uses apiplatform "PUT" method.
Unfortunatley It does not update the entity,
And I dont really have an error, except one from the "manyToOne" property "relatedUser"
The error complains about not having finding a JWT token - 401 Unauthorized
I can see in the browser network tab that this error occures because the "ManyToOne" assocation property,
makes a separate request to APi-platform, in my case it makes a separare call to "api/profiles" which contains all profiles of possible users.
But I dont have any issue with Posting a new entity, I tries watching all Api-platform courses and tries finding for solutions on stackoverflow, But could not find a solution for this. And I have spend two whole days trying to figure out why update/ "PUT" method is not working as expected.
I hope you guys can push me to the right direction so I can update an entity
payload:
response update:
Api resource code:
Only One Error: api/profiles