6cf5ef4cfc
Auto Tag Develop / tag (push) Successful in 6s
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: MALIO-DEV/Coltura#8 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
109 lines
4.2 KiB
PHP
109 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Sites\Infrastructure\ApiPlatform\Extension;
|
|
|
|
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
|
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
|
|
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
|
use ApiPlatform\Metadata\Operation;
|
|
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
|
|
use App\Shared\Domain\Contract\SiteAwareInterface;
|
|
use Doctrine\ORM\QueryBuilder;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
|
|
use function is_subclass_of;
|
|
use function sprintf;
|
|
|
|
/**
|
|
* Extension API Platform qui filtre automatiquement les collections et les
|
|
* items des resources implementant SiteAwareInterface selon le site
|
|
* courant de l'utilisateur authentifie (ticket 4 module Sites).
|
|
*
|
|
* Appliquee automatiquement par API Platform sur toutes les requetes GET
|
|
* (collection + item), mais devient no-op si :
|
|
* - la resource cible n'implemente pas SiteAwareInterface ;
|
|
* - l'user a la permission `sites.bypass_scope` ;
|
|
* - CurrentSiteProvider::get() retourne null (module desactive, pas
|
|
* d'user authentifie, ou user sans currentSite).
|
|
*
|
|
* Le filtrage est identique pour les deux interfaces Collection et Item,
|
|
* factorise dans `applyScope()`. Consequence sur GET /api/resource/{id} :
|
|
* si l'item existe en base mais appartient a un autre site, Doctrine
|
|
* retourne null apres filtrage et API Platform converti en 404
|
|
* (anti-enumeration : le user ne peut pas distinguer "n'existe pas" de
|
|
* "appartient a un autre site").
|
|
*/
|
|
final class SiteScopedQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
|
|
{
|
|
public function __construct(
|
|
private readonly CurrentSiteProviderInterface $currentSiteProvider,
|
|
private readonly Security $security,
|
|
) {}
|
|
|
|
public function applyToCollection(
|
|
QueryBuilder $queryBuilder,
|
|
QueryNameGeneratorInterface $queryNameGenerator,
|
|
string $resourceClass,
|
|
?Operation $operation = null,
|
|
array $context = [],
|
|
): void {
|
|
$this->applyScope($queryBuilder, $queryNameGenerator, $resourceClass);
|
|
}
|
|
|
|
public function applyToItem(
|
|
QueryBuilder $queryBuilder,
|
|
QueryNameGeneratorInterface $queryNameGenerator,
|
|
string $resourceClass,
|
|
array $identifiers,
|
|
?Operation $operation = null,
|
|
array $context = [],
|
|
): void {
|
|
$this->applyScope($queryBuilder, $queryNameGenerator, $resourceClass);
|
|
}
|
|
|
|
/**
|
|
* Ajoute la clause `WHERE <alias>.site = :currentSite` au query builder
|
|
* si les 3 conditions d'application sont remplies. No-op sinon.
|
|
*/
|
|
private function applyScope(
|
|
QueryBuilder $queryBuilder,
|
|
QueryNameGeneratorInterface $queryNameGenerator,
|
|
string $resourceClass,
|
|
): void {
|
|
// 1) Filtrer uniquement les resources qui ont opt-in via l'interface.
|
|
// `is_subclass_of` gere a la fois `implements` direct et herite.
|
|
if (!is_subclass_of($resourceClass, SiteAwareInterface::class)) {
|
|
return;
|
|
}
|
|
|
|
// 2) Admin ou user avec bypass explicite : visibilite globale.
|
|
// is_granted('sites.bypass_scope') retourne true pour les admins
|
|
// (bypass total via isAdmin) meme sans permission explicite.
|
|
if ($this->security->isGranted('sites.bypass_scope')) {
|
|
return;
|
|
}
|
|
|
|
// 3) Pas de site courant -> no-op plutot que collection vide.
|
|
// Decision assumee (cf. ticket 4 spec Risque 1) : un user sans
|
|
// currentSite voit tout. L'alternative "collection vide" est
|
|
// rejetee car elle rendrait l'app inutilisable pour un user
|
|
// mal configure.
|
|
$currentSite = $this->currentSiteProvider->get();
|
|
if (null === $currentSite) {
|
|
return;
|
|
}
|
|
|
|
// Application du filtre : alias racine du QueryBuilder, parametre
|
|
// genere pour eviter les collisions avec d'autres extensions.
|
|
$rootAlias = $queryBuilder->getRootAliases()[0];
|
|
$parameterName = $queryNameGenerator->generateParameterName('currentSite');
|
|
|
|
$queryBuilder
|
|
->andWhere(sprintf('%s.site = :%s', $rootAlias, $parameterName))
|
|
->setParameter($parameterName, $currentSite)
|
|
;
|
|
}
|
|
}
|