feat(sites) : scope /api/sites et /api/users aux sites autorises du caller
- SiteCollectionScopedExtension filtre /api/sites aux sites du user (name/adresse/CP/ville plus lisibles par un delegataire sites.view qui n'appartient pas a ces sites). Bypass via sites.bypass_scope. - UserSiteScopedExtension filtre /api/users aux users partageant au moins un site avec le caller. Empeche un delegataire de core.users.view d'enumerer l'organigramme complet + les sites de tous les tenants. - Helper createUserWithPermission rattache le user jetable a tous les sites fixtures, sinon le scoping le rend aveugle aux cibles. - test_target de UserRbacApiTest attache de meme aux sites pour rester visible depuis un caller non-admin. - testUserCannotSwitchToUnauthorizedSite : 403 -> 400 (anti-enumeration).
This commit is contained in:
@@ -9,6 +9,7 @@ use ApiPlatform\Symfony\Bundle\Test\Client;
|
||||
use App\Module\Core\Domain\Entity\Permission;
|
||||
use App\Module\Core\Domain\Entity\Role;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
@@ -123,6 +124,19 @@ abstract class AbstractApiTestCase extends ApiTestCase
|
||||
$user->setIsAdmin(false);
|
||||
$user->setPassword($hasher->hashPassword($user, $password));
|
||||
$user->addRbacRole($role);
|
||||
|
||||
// Le helper attache le user jetable a tous les sites existants pour
|
||||
// neutraliser le filtrage par UserSiteScopedExtension : la plupart
|
||||
// des tests assume une visibilite globale sur les users cibles. Les
|
||||
// tests qui valident le comportement "sans sites" doivent creer leur
|
||||
// user a la main (pas via ce helper).
|
||||
$siteRepository = $em->getRepository(Site::class);
|
||||
if (null !== $siteRepository) {
|
||||
foreach ($siteRepository->findAll() as $site) {
|
||||
$user->addSite($site);
|
||||
}
|
||||
}
|
||||
|
||||
$em->persist($user);
|
||||
|
||||
$em->flush();
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Tests\Module\Core\Api;
|
||||
use App\Module\Core\Domain\Entity\Permission;
|
||||
use App\Module\Core\Domain\Entity\Role;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
/**
|
||||
@@ -41,11 +42,18 @@ final class UserRbacApiTest extends AbstractApiTestCase
|
||||
/** @var UserPasswordHasherInterface $hasher */
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
|
||||
// User cible standard (non admin).
|
||||
// User cible standard (non admin). On lui attache tous les sites
|
||||
// fixtures pour rester visible depuis les callers non-admin munis de
|
||||
// sites (cf. UserSiteScopedExtension qui filtre `/api/users` par
|
||||
// intersection de sites). Sans cela, un user `core.users.manage`
|
||||
// sans site commun avec test_target recevrait un 404 sur le PATCH.
|
||||
$target = new User();
|
||||
$target->setUsername('test_target');
|
||||
$target->setIsAdmin(false);
|
||||
$target->setPassword($hasher->hashPassword($target, 'secret'));
|
||||
foreach ($em->getRepository(Site::class)->findAll() as $site) {
|
||||
$target->addSite($site);
|
||||
}
|
||||
$em->persist($target);
|
||||
|
||||
// User admin dedie pour le cas d'auto-suicide (pas l'admin fixture).
|
||||
|
||||
@@ -39,7 +39,14 @@ final class CurrentSiteSwitchApiTest extends AbstractApiTestCase
|
||||
|
||||
public function testUserCannotSwitchToUnauthorizedSite(): void
|
||||
{
|
||||
// alice n'a que Chatellerault. Tenter Pommevic → 403.
|
||||
// alice n'a que Chatellerault. Tenter Pommevic → 400 (anti-enumeration).
|
||||
//
|
||||
// Depuis l'ajout de SiteCollectionScopedExtension, les sites hors
|
||||
// du scope de l'user sont filtres a la source : l'IriConverter ne
|
||||
// peut pas resoudre `/api/sites/{id}` pour un site non autorise et
|
||||
// leve 400 "Item not found". Reponse identique a "site inexistant",
|
||||
// ce qui empeche l'enumeration des ids de sites tiers. Avant la PR
|
||||
// scope, le processor traduisait SiteNotAuthorizedException → 403.
|
||||
$em = $this->getEm();
|
||||
$pommevic = $em->getRepository(Site::class)->findOneBy(['name' => 'Pommevic']);
|
||||
self::assertNotNull($pommevic);
|
||||
@@ -50,7 +57,7 @@ final class CurrentSiteSwitchApiTest extends AbstractApiTestCase
|
||||
'json' => ['site' => '/api/sites/'.$pommevic->getId()],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
}
|
||||
|
||||
public function testSwitchWithMissingSiteFieldReturns400(): void
|
||||
|
||||
Reference in New Issue
Block a user