Cómo filtrar el acceso a los elementos de la API Platform para Symfony

API Platform filteringVengo a traer un pequeño truco que es algo complicado de encontrar. Es decir, traigo un HOWTO para una configuración en concreto de una joya del software. Se trata de la API Platform para Symfony, con la que implementamos en muy poco tiempo una API web en un proyecto completo.

Con esta API Platform tendremos acceso remoto, mediante las clásicas llamadas GET, POST, PUT, DELETE a las entidades del proyecto. Podemos limitar el acceso a cada usuario a cada elemento de una entidad de la que sea propietario.

Por ejemplo, imaginemos que tenemos usuarios y pedidos, dos entidades que vamos a llamar User y SalesOrder. El límite clásico de muchos CMSs es el acceso mediante sí/no a cada entidad. Es decir, si un usuario tiene permiso de acceso a los pedidos, podrá ver todos los pedidos de todos los usuarios. Pues con la API Platform no, vamos un paso más allá, con pocas configuraciones podemos limitar a cada usuario el acceso a sólo sus pedidos.

Limitando acceso a cada registro a su propietario

Tenemos que hacer dos cosas, una es crear un filtro y otra es configurar los accesos a la entidad SalesOrder a sólo su propietario. Comenzamos por la entidad de pedidos:

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\SalesOrderRepository")
 * @ApiResource(
 *   attributes={"filters"={"object.current_user"}},
 *   collectionOperations={
 *      "post"={"access_control"="object.user == user"},
 *      "get"={"access_control"="object.user == user"}
 *   },
 *   itemOperations={"get"={"access_control"="object.user == user"}},
 *   normalizationContext={"groups"={"read"}, "enable_max_depth"=true},
 *   denormalizationContext={"groups"={"read","write"}, "enable_max_depth"=true}
 * )
 * @ApiFilter(SearchFilter::class, properties={"id": "exact", "cmsReference": "partial", "cmsReferenceB2b": "partial"})
 * @UniqueEntity(
 *      fields={"cmsReferenceB2b", "user"},
 *      message="Hay otro pedido con este mismo valor de cmsReferenceB2b."
 * )
 */
class SalesOrder
{

Marco en negrita lo que hace la magia del filtrado por el usuario actual.

Hay que crear el filtro de Doctrine para poder aplicarlo en la API Platorm

Lo siguiente es crear el filtro como comento, que se hace creando el fichero siguiente src/Doctrine/CurrentUserExtension.php:

<?php

namespace App\Doctrine;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\SalesOrder;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    private $tokenStorage;
    private $authorizationChecker;

    public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $checker)
    {
        $this->tokenStorage = $tokenStorage;
        $this->authorizationChecker = $checker;
    }

    /**
     * {@inheritdoc}
     */
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     * {@inheritdoc}
     */
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {
        $this->addWhere($queryBuilder, $resourceClass);
    }

    /**
     * @param QueryBuilder $queryBuilder
     * @param string $resourceClass
     */
    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass)
    {
        $user = $this->tokenStorage->getToken()->getUser();
        if ($user instanceof User && SalesOrder::class === $resourceClass && !$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            $rootAlias = $queryBuilder->getRootAliases()[0];
            $queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
            $queryBuilder->setParameter('current_user', $user->getId());
        }
    }
}

Terminando, unas aclaraciones

Fíjate que igual que aplicamos el filtrado a ciertas operaciones, por usuario, lo que simplemente hacemos es aplicar un filtrado de Doctrine por el usuario actual. Es decir, tenemos aplicadas restricciones de la colección y también para las operaciones sobre items.

Es una maravilla el proyecto de la API Platform. Da mucho trabajo leerse toda la documentación, pero la estabilidad y productividad que te da el poder disponer de este bundle para Symfony es inigualable.

Más información en la página oficial el proyecto: https://api-platform.com/

Compartir..

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

16 + diecinueve =