fix(sites) : processors transactionnels + garde sites.manage + anti-TOCTOU
- UserRbacProcessor : persist + ensureCurrentSiteConsistency wrappes dans
wrapInTransaction (plus de double flush non atomique qui pouvait laisser
currentSite orphelin sur un crash entre les deux flush).
- UserRbacProcessor : detecte la mutation de `sites` via
PersistentCollection::isDirty() et verifie is_granted('sites.manage')
avant de deleguer (empeche core.users.manage de contourner sites.manage).
- UserRbacProcessor : skip ensureCurrentSiteConsistency si ni sites ni
currentSite n'ont ete modifies (plus de bascule silencieuse de site sur
un simple toggle isAdmin apres suppression de site).
- CurrentSiteProcessor : refresh($user) avant hasSite() pour fermer la
fenetre TOCTOU entre /rbac revoke et /me/current-site. Catch
OptimisticLockException pour etre pret a un futur @ORM\Version.
- SiteAwareInjectionProcessor : valide un site explicite contre
$user->getSites() (bypass via sites.bypass_scope) — bloque le cross-site
write quand l'entite expose `site` en ecriture.
This commit is contained in:
@@ -50,6 +50,14 @@ final class UserRbacProcessorTest extends TestCase
|
||||
|
||||
$this->entityManager->method('getUnitOfWork')->willReturn($this->unitOfWork);
|
||||
|
||||
// wrapInTransaction doit executer reellement la closure pour que le
|
||||
// resultat de persistProcessor->process() soit capture dans $result.
|
||||
// Sans ce stub, la closure n'est jamais invoquee et $result reste null.
|
||||
$this->entityManager
|
||||
->method('wrapInTransaction')
|
||||
->willReturnCallback(static fn (callable $fn) => $fn())
|
||||
;
|
||||
|
||||
$this->processor = new UserRbacProcessor(
|
||||
$this->persistProcessor,
|
||||
$this->entityManager,
|
||||
|
||||
@@ -11,8 +11,10 @@ use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use App\Module\Sites\Infrastructure\ApiPlatform\State\Processor\SiteAwareInjectionProcessor;
|
||||
use App\Shared\Domain\Contract\SiteAwareInterface;
|
||||
use App\Shared\Domain\Contract\SiteInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
/**
|
||||
@@ -125,20 +127,27 @@ final class SiteAwareInjectionProcessorTest extends TestCase
|
||||
$provider = $this->createStub(CurrentSiteProviderInterface::class);
|
||||
$provider->method('get')->willReturn($currentSite);
|
||||
|
||||
return new SiteAwareInjectionProcessor($inner, $provider);
|
||||
// Stub Security : bypass_scope = true par defaut pour preserver le
|
||||
// comportement des tests historiques (pas de validation cross-site).
|
||||
// Les tests dedies a la validation cross-site instancient leur propre
|
||||
// Security via un helper dedie.
|
||||
$security = $this->createStub(Security::class);
|
||||
$security->method('isGranted')->willReturn(true);
|
||||
|
||||
return new SiteAwareInjectionProcessor($inner, $provider, $security);
|
||||
}
|
||||
|
||||
private function makeSiteAwareStub(?Site $initialSite): SiteAwareInterface
|
||||
{
|
||||
return new class($initialSite) implements SiteAwareInterface {
|
||||
public function __construct(private ?Site $site) {}
|
||||
public function __construct(private ?SiteInterface $site) {}
|
||||
|
||||
public function getSite(): ?Site
|
||||
public function getSite(): ?SiteInterface
|
||||
{
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
public function setSite(Site $site): void
|
||||
public function setSite(SiteInterface $site): void
|
||||
{
|
||||
$this->site = $site;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user