Restreindre l'accès aux participants ou auteurs

À ce stade, si nous rentrons n'importe quel id de discussion, nous pouvons les lire et ajouter des messages alors que nous ne faisons pas partie des participants. Nous avons même accès aux discussions si nous ne sommes pas identifié.

Nous allons ajouter un peu plus d'informations quand on demande la collection des Discussion sur GET /api/discussions. On va rajouter sur participants et author le groupe discussion:read comme suit :

/**
 * @ORM\ManyToMany(targetEntity=User::class)
 * @Groups("discussion:read")
 */
private $participants;

// ...

/**
 * @ORM\ManyToOne(targetEntity=User::class)
 * @ORM\JoinColumn(nullable=false)
 * @Groups("discussion:read")
 */
private $author;

Maintenant notre réponse contient participants et author :

{
    "@context": "/api/contexts/Discussion",
    "@id": "/api/discussions",
    "@type": "hydra:Collection",
    "hydra:member": [
        {
            "@id": "/api/discussions/1",
            "@type": "Discussion",
            "participants": [
                "/api/users/35",
                "/api/users/36",
                "/api/users/37",
                "/api/users/38"
            ],
            "author": "/api/users/10"
        },
        {
            "@id": "/api/discussions/2",
            "@type": "Discussion",
            "participants": [
                "/api/users/11"
            ],
            "author": "/api/users/10"
        },

Nous allons également restreindre l'accès à Discussion uniquement aux utilisateurs identifiés. Pour ce faire dans l'entité Discussion on rajoute la propriété accessControl Voici l'état actuel des annotations sur l'entité Discussion

/**
 * @ApiResource(
 *     collectionOperations={
 *          "post"={
 *                "input"=CreateDiscussionInput::class,
 *                "denormalization_context"={"groups"={"discussion:write"}},
 *                "output"=CreateDiscussionOutput::class
 *          },
 *          "createNewMessage"={
 *                "method"="POST",
 *                "path"="discussions/{id}/messages",
 *                "input"=CreateMessageInput::class,
 *                "denormalization_context"={"groups"={"message:write"}},
 *           },
 *          "get"
 *    },
 *    normalizationContext={"groups"={"discussion:read"}},
 *    subresourceOperations={},
 *    accessControl="is_granted('ROLE_USER')"
 * )
 * @ORM\Entity(repositoryClass=DiscussionRepository::class)
 */
class Discussion
{

Doctrine Extension

Nous allons ajouter deux contraintes dans la requête, une qui vérifie si on est dans les participants, et une autre qui vérifie si on est l'auteur de la Discussion. Si l'une ou l'autre est vérifié alors on à accès à cette Discussion.

On va pour créer une Extension pour Doctrine qui va nous permettre de récupérer le queryBuilder et de lui ajouter des restrictions.

Voici notre CurrentUserDiscussionExtension qui implemente les interfaces QueryCollectionExtensionInterface et QueryItemExtensionInterface. L'une surcharge la requête pour chaque collection retourné avec la méthode applyToCollection et l'autre surcharge la requête pour chaque item retourné avec applyToItem.

<?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\Discussion;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;

class CurrentUserDiscussionExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    private $security;
    private $authorizationChecker;

    public function __construct(
        Security $security,
        AuthorizationCheckerInterface $authorizationChecker
    ) {
        $this->authorizationChecker = $authorizationChecker;
        $this->security = $security;
    }

    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {

    }

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {

    }
}

On va créer une méthode qui sera appelé pour chaque item ou collection. Premièrement on va récupérer l'utilisateur identifié. Ensuite on rajoute un andWhere avec nos deux conditions. On veut que l'une des deux soit vrai donc on va les séparer d'un OR. On regarde si on est l'auteur avec $rootAlias.author = :user ou alors on regarde si on fait partie des participants avec :user MEMBER OF $rootAlias.participants

$user = $this->security->getUser();

if (null === $user) {
    return;
}

if ($resourceClass !== Discussion::class) {
    return;
}

$rootAlias = $queryBuilder->getRootAliases()[0];

$queryBuilder
    ->andWhere("($rootAlias.author = :user) OR (:user MEMBER OF $rootAlias.participants)")
    ->setParameter("user", $user)
;

On va mettre ce queryBuilder appelé pour item et collection avec de cette façon

<?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\Courier;
use App\Entity\CourierFile;
use App\Entity\Discussion;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;

class CurrentUserDiscussionExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    private $security;
    private $authorizationChecker;

    public function __construct(
        Security $security,
        AuthorizationCheckerInterface $authorizationChecker
    ) {
        $this->authorizationChecker = $authorizationChecker;
        $this->security = $security;
    }

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

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->apply($queryBuilder, $resourceClass);
    }

    private function apply(QueryBuilder $queryBuilder, string $resourceClass)
    {
        $user = $this->security->getUser();

        if (null === $user) {
            return;
        }

        if (!$resourceClass === Discussion::class) {
            return;
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];

        $queryBuilder
            ->andWhere("($rootAlias.author = :user) OR (:user MEMBER OF $rootAlias.participants)")
            ->setParameter("user", $user)
        ;
    }
}

Maintenant si on refait un GET /api/discussions nous aurons uniquement les Discussions dans laquelle on est auteur ou participant :

{
    "@context": "/api/contexts/Discussion",
    "@id": "/api/discussions",
    "@type": "hydra:Collection",
    "hydra:member": [
        {
            "@id": "/api/discussions/1",
            "@type": "Discussion",
            "participants": [
                "/api/users/35",
                "/api/users/36",
                "/api/users/37",
                "/api/users/38"
            ],
            "author": "/api/users/10"
        },
        {
            "@id": "/api/discussions/105",
            "@type": "Discussion",
            "participants": [
                "/api/users/10",
                "/api/users/11",
                "/api/users/12"
            ],
            "author": "/api/users/38"
        }
    ],
    "hydra:totalItems": 2
}

Je suis l'utilisateur 38 et je suis présent dans les participants de la Discussion 1 et je suis l'auteur de la Discussion 105 en tant que participant.

Maintenant si je veux accéder aux autres discussion par exemple GET /api/discussions/99 j'ai un 404 de la part de Doctrine car je rentre dans applyToItem de notre CurrentUserDiscussionExtension

Restreindre l'accès aux messages

On va créer une autre Extension pour la lecture des messages sur GET /api/discussions/{id}/messages. Seulement comme nous sommes sur Message on va devoir remonter à la Discussion pour connaitre les participants dans notre queryBuidler. Voici l'autre Extension pour Message :

<?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\Message;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;

class CurrentUserMessageExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    private $security;
    private $authorizationChecker;

    public function __construct(
        Security $security,
        AuthorizationCheckerInterface $authorizationChecker
    ) {
        $this->authorizationChecker = $authorizationChecker;
        $this->security = $security;
    }

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

    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        $this->apply($queryBuilder, $resourceClass);
    }

    private function apply(QueryBuilder $queryBuilder, string $resourceClass)
    {
        $user = $this->security->getUser();

        if (null === $user) {
            return;
        }

        if ($resourceClass !== Message::class) {
            return;
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];

        $queryBuilder
            ->innerJoin($rootAlias . '.discussion', 'd')
            ->andWhere("(d.author = :user) OR (:user MEMBER OF d.participants)")
            ->setParameter("user", $user)
        ;
    }
}

Rajoutons également la contrainte d'être identifié avec accessControl pour avoir accès aux messages :

 * @ApiResource(
 *     accessControl="is_granted('ROLE_USER')"
 * )
 * @ORM\Entity(repositoryClass=MessageRepository::class)
 * @ApiFilter(OrderFilter::class)
 */
class Message
prev next

Commentaires

Connectez-vous pour laisser un commentaire