Buy Access to Course
21.

Permitir editar sólo a los propietarios

|

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

Nueva búsqueda de seguridad: Quiero permitir que sólo el propietario de un tesoro pueda editarlo. Ahora mismo, puedes editar un tesoro siempre que tengas este rol. Pero eso significa que puedes editar el tesoro de cualquiera. Alguien sigue cambiando elcoolFactor de mi cuadro Velvis a 0. Eso no mola nada.

TDD: Probar que sólo los Propietarios pueden Editar

Escribamos una prueba para esto. En la parte inferior dipublic function testPatchToUpdateTreasure():

// ... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 15 - 97
public function testPatchToUpdateTreasure()
{
// ... lines 100 - 112
}
}

Y empezaremos como siempre: $user = UserFactory::createOne() luego$this->browser->actingAs($user).

Como vamos a editar un tesoro, vamos a ->patch() a /api/treasures/... ¡y luego necesitamos un tesoro para editar! Crea uno encima:$treasure = DragonTreasureFactory::createOne(). Y para esta prueba, queremos asegurarnos de que el owner es definitivamente este $user. Termina la URL con$treasure->getId().

Para los datos, envía algo de json para actualizar sólo el campo value a 12345, luego assertStatus(200) y assertJsonMatches('value', 12345):

// ... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 15 - 97
public function testPatchToUpdateTreasure()
{
$user = UserFactory::createOne();
$treasure = DragonTreasureFactory::createOne(['owner' => $user]);
$this->browser()
->actingAs($user)
->patch('/api/treasures/'.$treasure->getId(), [
'json' => [
'value' => 12345,
],
])
->assertStatus(200)
->assertJsonMatches('value', 12345)
;
}
}

¡Excelente! Esto debería estar permitido porque somos el owner. Copia el nombre del método, luego busca tu terminal y ejecútalo:

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

No te sorprendas, pasa.

Ahora probemos el otro caso: iniciemos sesión como otra persona e intentemos actualizar este tesoro.

Copia toda la sección $browser. Podríamos crear otro método de prueba, pero esto funcionará bien todo en uno. Antes de esto, añade$user2 = UserFactory::createOne() - y luego inicia sesión como ese usuario. Esta vez, cambia el value por 6789 y, como esto no debería estar permitido, afirma que el código de estado es 403:

// ... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 15 - 97
public function testPatchToUpdateTreasure()
{
// ... lines 100 - 113
$user2 = UserFactory::createOne();
$this->browser()
->actingAs($user2)
->patch('/api/treasures/'.$treasure->getId(), [
'json' => [
'value' => 6789,
],
])
->assertStatus(403)
;
}
}

Cuando intentemos la prueba ahora

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

¡Falla! Esto está permitido: ¡la API devuelve un 200!

Expresiones de seguridad más complejas

Entonces, ¿cómo podemos hacer que sólo el propietario de un tesoro pueda editarlo? Bueno, en DragonTreasure, la respuesta está en la opción security:

251 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Put(
security: 'is_granted("ROLE_TREASURE_EDIT")',
),
new Patch(
security: 'is_granted("ROLE_TREASURE_EDIT")',
),
// ... lines 47 - 49
],
// ... lines 51 - 67
)]
// ... lines 69 - 89
class DragonTreasure
{
// ... lines 92 - 249
}

Una cosa que resulta complicada con Put() y Patch() es que ambos se utilizan para editar usuarios. Así que si vas a tener ambos, necesitas mantener sus opciones securitysincronizadas. De hecho, voy a eliminar Put() para que podamos centrarnos en Patch().

La cadena dentro de security es una expresión... y podemos ponernos un poco elegantes. Podemos conceder acceso si tienes ROLE_TREASURE_EDIT y si object.owner == user:

248 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Patch(
security: 'is_granted("ROLE_TREASURE_EDIT") and object.owner == user',
),
// ... lines 44 - 46
],
// ... lines 48 - 64
)]
// ... lines 66 - 86
class DragonTreasure
{
// ... lines 89 - 246
}

Dentro de la expresión de seguridad, Symfony nos da unas cuantas variables. Una es user, que es el objeto actual User. Otra es object, que será el objeto actual para esta operación. Así que el objeto DragonTreasure. Así que estamos diciendo que se debe permitir el acceso si el DragonTreasures owner es igual aluser autenticado actualmente. Eso es... ¡exactamente lo que queremos!

Así que, ¡vuelve a intentar la prueba!

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

Y... ¡oh! ¡Bajamos a un error 500! Aquí es donde resulta útil ese archivo de registro guardado. Haré clic para abrirlo. Si esto es difícil de leer, mira la fuente de la página. Mucho mejor. Dice

No se puede acceder a la propiedad privada DragonTreasure::$owner.

Y viene de ExpressionLanguage de Symfony . Ah, ya sé lo que pasa. El lenguaje de expresión es como Twig... pero no exactamente igual. No podemos hacer cosas extravagantes como .owner cuando owner es una propiedad privada. Tenemos que llamar al método público:

248 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Patch(
security: 'is_granted("ROLE_TREASURE_EDIT") and object.getOwner() == user',
),
// ... lines 44 - 46
],
// ... lines 48 - 64
)]
// ... lines 66 - 86
class DragonTreasure
{
// ... lines 89 - 246
}

Redoble de tambores, por favor:

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

¡Pasa con éxito!

Impedir el cambio de propietario: securityPostDenormalize

Pero ya me conoces, tengo que hacerlo más difícil. Copia parte de la prueba. Esta vez, iniciar sesión como propietario y editar nuestro propio tesoro. Hasta aquí, todo bien. Pero ahora intenta cambiar el owner por otro: $user2->getId():

// ... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 15 - 97
public function testPatchToUpdateTreasure()
{
// ... lines 100 - 126
$this->browser()
// ... line 128
->patch('/api/treasures/'.$treasure->getId(), [
'json' => [
// change the owner to someone else
'owner' => '/api/users/'.$user2->getId(),
],
])
// ... line 135
;
}
}

Ahora puede que esto sea algo que quieras permitir. Tal vez digas

Si puedes editar un DragonTreasure, entonces eres libre de asignarle un > propietario diferente propietario.

Pero supongamos que queremos impedirlo. Entonces assertStatus(403). ¿Crees que la prueba pasará? Inténtalo:

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

¡Falla! ¡Nos ha permitido cambiar el owner! Vuelve a DragonTreasure. La expresión security se ejecuta antes de que los nuevos datos se deserialicen en el objeto. En otras palabras, object será el DragonTreasure de la base de datos, antes de que se le aplique nada del nuevo JSON. Esto significa que se está comprobando que el owner actual es igual al usuario conectado en ese momento, que es el caso principal que queremos proteger.

Pero a veces quieres ejecutar la seguridad después de que los nuevos datos se hayan introducido en el objeto. En ese caso, utiliza una opción llamada securityPostDenormalize. Recuerda que desnormalizar es el proceso de tomar los datos y ponerlos en el objeto. Así quesecurity seguirá ejecutándose primero... y se asegurará de que somos el propietario original. Ahora también podemos decir object.getOwner() == user:

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

Esto parece idéntico... pero esta vez object será el DragonTreasure con los nuevos datos. Así que estamos comprobando que el nuevo propietario también es igual al usuario actualmente conectado.

Por cierto, en securityPostDenormalize, también tienes una variable previous_object, que es igual al objeto antes de la desnormalización. Por tanto, es idéntica a objecten la opción security. Pero, no necesitamos eso.

Haz la prueba ahora:

symfony php bin/phpunit --filter=testPatchToUpdateTreasure

¡Lo hemos conseguido!

Seguridad frente a validación

Este último ejemplo pone de manifiesto dos tipos diferentes de comprobaciones de seguridad. La primera comprobación determina si el usuario puede o no realizar esta operación. Por ejemplo: ¿puede el usuario actual hacer una petición a este tesoro a PATCH? Eso depende del usuario actual y del DragonTreasure actual en la base de datos.

Pero la segunda comprobación dice

Vale, ahora que sé que se me permite hacer una petición a PATCH, ¿se me permite cambiar los datos de esta forma exacta?

Esto depende del usuario conectado en ese momento y de los datos que se estén enviando.

Traigo a colación esta diferencia porque, para mí, el primer caso -en el que intentas averiguar si una operación está permitida en absoluto, independientemente de los datos que se envíen- es tarea de la seguridad. Y así es exactamente como yo lo implementaría.

Sin embargo, en el segundo caso, en el que intentas averiguar si el usuario está autorizado a enviar esos datos exactos -por ejemplo, si puede cambiar la direcciónowner o no-, creo que es mejor que se encargue de ello la capa de validación.

Por ahora voy a mantener esto en la capa de seguridad. Pero más adelante, cuando hablemos de la validación personalizada, lo trasladaremos a ella.

Próximamente: ¿podemos flexibilizar la opción security lo suficiente como para permitir también a los usuarios administradores editar el tesoro de cualquiera? ¡Permanece atento!