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