Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Permitir que los usuarios administradores editen cualquier tesoro

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

Hemos configurado las cosas para que sólo el propietario de un tesoro pueda editarlo. Ahora ha llegado un nuevo requisito desde las alturas: los usuarios administradores deben poder editar cualquier tesoro. Eso significa que un usuario que tenga ROLE_ADMIN.

¡A la prueba-móvil! Añade un public function testAdminCanPatchToEditTreasure(). A continuación, crea un usuario administrador con UserFactory::createOne() pasando los roles aROLE_ADMIN:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 138
public function testAdminCanPatchToEditTreasure(): void
{
$admin = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]);
}
}

Métodos de Estado de Foundry

Eso funcionará bien. Pero si necesitamos crear muchos usuarios admin en nuestras pruebas, podemos añadir un acceso directo a Foundry. Abre UserFactory. Vamos a crear algo llamado método "estado". En cualquier lugar de su interior, añade una función pública llamada, qué talwithRoles() que tenga un argumento array $roles y devuelva self, lo que hará que esto sea más cómodo cuando lo utilicemos. Entoncesreturn $this->addState(['roles' => $roles]):

... lines 1 - 30
final class UserFactory extends ModelFactory
{
... lines 33 - 54
public function withRoles(array $roles): self
{
return $this->addState(['roles' => $roles]);
}
... lines 59 - 92
}

Lo que pasemos a addState() se convierte en parte de los datos que se utilizarán para hacer este usuario.

Para utilizar el método de estado, el código cambia a UserFactory::new(). En lugar de crear un objeto User, esto instanciará un nuevo UserFactory... y entonces podremos llamar awithRoles() y pasarle ROLE_ADMIN:

Así, estamos "elaborando" el aspecto que queremos que tenga el usuario. Cuando hayamos terminado, llamamos acreate():

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 138
public function testAdminCanPatchToEditTreasure(): void
{
$admin = UserFactory::new()->withRoles(['ROLE_ADMIN'])->create();
}
}

createOne() es un método abreviado estático. Pero como tenemos una instancia de la fábrica, utiliza create().

Pero podemos ir aún más lejos. De vuelta en UserFactory, añade otro método de estado llamadoasAdmin() que devuelva self. Dentro devuelve $this->withRoles(['ROLE_ADMIN']):

... lines 1 - 30
final class UserFactory extends ModelFactory
{
... lines 33 - 59
public function asAdmin(): self
{
return $this->withRoles(['ROLE_ADMIN']);
}
... lines 64 - 97
}

Gracias a eso, podemos simplificar a UserFactory::new()->asAdmin()->create():

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 138
public function testAdminCanPatchToEditTreasure(): void
{
$admin = UserFactory::new()->asAdmin()->create();
}
}

¡Bien!

Escribir la prueba

Ahora vamos a poner en marcha esta prueba. Crea un nuevo $treasure establecido enDragonTreasureFactory::createOne():

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 138
public function testAdminCanPatchToEditTreasure(): void
{
$admin = UserFactory::new()->asAdmin()->create();
$treasure = DragonTreasureFactory::createOne();
... lines 143 - 153
}
}

Como no estamos pasando un owner, esto creará un nuevo User en segundo plano y lo utilizará como owner. Esto significa que nuestro usuario administrador no será el propietario.

Ahora, $this->browser()->actingAs($adminUser) luego ->patch() a/api/treasures/, $treasure->getId(), enviando json para actualizar value al mismo 12345. ->assertStatus(200) y assertJsonMatches(), value, 12345:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 138
public function testAdminCanPatchToEditTreasure(): void
{
$admin = UserFactory::new()->asAdmin()->create();
$treasure = DragonTreasureFactory::createOne();
$this->browser()
->actingAs($admin)
->patch('/api/treasures/'.$treasure->getId(), [
'json' => [
'value' => 12345,
],
])
->assertStatus(200)
->assertJsonMatches('value', 12345)
;
}
}

¡Genial! Copia el nombre del método. Vamos a probarlo:

symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure

Y... ¡vale! Aún no lo hemos implementado, así que falla.

Permitir a los administradores editar cualquier cosa

Entonces, ¿cómo permitimos que los administradores editen cualquier tesoro? Bueno, al principio es relativamente fácil porque tenemos el control total a través de la expresión security. Así que podemos añadir algo como if is_granted("ROLE_ADMIN") OR y luego poner paréntesis alrededor del otro caso de uso:

... lines 1 - 27
#[ApiResource(
... lines 29 - 30
operations: [
... lines 32 - 40
new Patch(
security: 'is_granted("ROLE_ADMIN") or (is_granted("ROLE_TREASURE_EDIT") and object.getOwner() == user)',
... line 43
),
... lines 45 - 47
],
... lines 49 - 65
)]
... lines 67 - 87
class DragonTreasure
{
... lines 90 - 247
}

¡Asegurémonos de que funciona!

symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure

¡Un error 500! Veamos qué está pasando. Haz clic para abrir esto.

Token "nombre" inesperado alrededor de la posición 26.

Así que... eso ha sido un accidente. Cambia OR por or. Y... mueve también esta nueva lógica a securityPostDenormalize:

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

Luego vuelve a intentar la prueba:

symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure

¡Lo tengo! Pero mi metedura de pata saca a relucir un gran punto: la expresión security se está volviendo demasiado compleja. Es tan legible como un script PERL de una sola línea... y no queremos cometer errores cuando se trata de seguridad.

Así que, a continuación, centralicemos esta lógica con un votante.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice