Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfd6e5251d |
@@ -3,6 +3,14 @@ lexik_jwt_authentication:
|
|||||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
token_ttl: '%env(int:JWT_TOKEN_TTL)%'
|
token_ttl: '%env(int:JWT_TOKEN_TTL)%'
|
||||||
|
# Tolerance d'horloge (en secondes) appliquee a la validation des claims
|
||||||
|
# temporels iat / nbf / exp (LooseValidAt cote lcobucci). Sans cette marge
|
||||||
|
# (defaut 0), un recul d'horloge entre la signature (/login_check) et la
|
||||||
|
# requete suivante rend iat/nbf « dans le futur » -> « Invalid JWT Token »
|
||||||
|
# (401). Observe en dev sous WSL2/Docker (horloge CLOCK_REALTIME non
|
||||||
|
# monotone) : flakes intermittents de la suite PHPUnit (ERP-98). Benefice
|
||||||
|
# aussi en prod si les noeuds derivent legerement entre eux.
|
||||||
|
clock_skew: 15
|
||||||
remove_token_from_body_when_cookies_used: true
|
remove_token_from_body_when_cookies_used: true
|
||||||
token_extractors:
|
token_extractors:
|
||||||
authorization_header:
|
authorization_header:
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ namespace App\Shared\Infrastructure\Doctrine;
|
|||||||
|
|
||||||
use App\Shared\Domain\Contract\BlamableInterface;
|
use App\Shared\Domain\Contract\BlamableInterface;
|
||||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
use Doctrine\ORM\Event\PrePersistEventArgs;
|
use Doctrine\ORM\Event\PrePersistEventArgs;
|
||||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||||
use Doctrine\ORM\Events;
|
use Doctrine\ORM\Events;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,12 +30,19 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
#[AsDoctrineListener(event: Events::preUpdate)]
|
#[AsDoctrineListener(event: Events::preUpdate)]
|
||||||
final class TimestampableBlamableSubscriber
|
final class TimestampableBlamableSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly Security $security) {}
|
// L'horloge est injectee (et non un `new DateTimeImmutable()` direct) pour
|
||||||
|
// que les tests puissent figer/avancer le temps de facon deterministe via
|
||||||
|
// ClockSensitiveTrait (cf. ERP-98). En prod, le service `clock` delegue a
|
||||||
|
// l'horloge systeme reelle.
|
||||||
|
public function __construct(
|
||||||
|
private readonly Security $security,
|
||||||
|
private readonly ClockInterface $clock,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function prePersist(PrePersistEventArgs $args): void
|
public function prePersist(PrePersistEventArgs $args): void
|
||||||
{
|
{
|
||||||
$entity = $args->getObject();
|
$entity = $args->getObject();
|
||||||
$now = new DateTimeImmutable();
|
$now = $this->clock->now();
|
||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
if ($entity instanceof TimestampableInterface) {
|
if ($entity instanceof TimestampableInterface) {
|
||||||
@@ -55,7 +62,7 @@ final class TimestampableBlamableSubscriber
|
|||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
if ($entity instanceof TimestampableInterface) {
|
if ($entity instanceof TimestampableInterface) {
|
||||||
$entity->setUpdatedAt(new DateTimeImmutable());
|
$entity->setUpdatedAt($this->clock->now());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entity instanceof BlamableInterface && $user instanceof UserInterface) {
|
if ($entity instanceof BlamableInterface && $user instanceof UserInterface) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace App\Tests\Module\Catalog\Api;
|
|||||||
use App\Module\Catalog\Domain\Entity\Category;
|
use App\Module\Catalog\Domain\Entity\Category;
|
||||||
use App\Module\Core\Domain\Entity\User;
|
use App\Module\Core\Domain\Entity\User;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
|
use Symfony\Component\Clock\Test\ClockSensitiveTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests RG-1.15 / RG-1.16 : le TimestampableBlamableSubscriber doit remplir
|
* Tests RG-1.15 / RG-1.16 : le TimestampableBlamableSubscriber doit remplir
|
||||||
@@ -20,12 +22,39 @@ use DateTimeImmutable;
|
|||||||
* - DELETE : deletedAt rempli ET updatedAt + updatedBy mis a jour (UPDATE
|
* - DELETE : deletedAt rempli ET updatedAt + updatedBy mis a jour (UPDATE
|
||||||
* Doctrine declenche le subscriber)
|
* Doctrine declenche le subscriber)
|
||||||
*
|
*
|
||||||
|
* ERP-98 : ces tests pilotent une horloge mockee (ClockSensitiveTrait) plutot
|
||||||
|
* que de dependre d'un `sleep(1)` reel. Le subscriber lit le service `clock`,
|
||||||
|
* que `self::mockTime()` remplace par un MockClock fige au niveau du process —
|
||||||
|
* ce qui survit aux reboots de kernel entre requetes (POST admin / PATCH bob)
|
||||||
|
* et reste insensible a la derive d'horloge WSL2 a l'origine des flakes.
|
||||||
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
||||||
{
|
{
|
||||||
|
use ClockSensitiveTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fige l'horloge globale sur l'instant courant DANS LE FUSEAU PHP par
|
||||||
|
* defaut, et la retourne pour la piloter (`sleep()`).
|
||||||
|
*
|
||||||
|
* Subtilite : `self::mockTime()` cree par defaut un MockClock en UTC, or
|
||||||
|
* les colonnes `TIMESTAMP WITHOUT TIME ZONE` round-trippent via le fuseau
|
||||||
|
* PHP (Europe/Paris). Un MockClock UTC decalerait createdAt de l'offset
|
||||||
|
* (2h) au rechargement. On seede donc avec `new DateTimeImmutable()`
|
||||||
|
* (fuseau par defaut), exactement comme le NativeClock en prod.
|
||||||
|
*/
|
||||||
|
private function freezeClock(): ClockInterface
|
||||||
|
{
|
||||||
|
return self::mockTime(new DateTimeImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreatedByAdminOnPost(): void
|
public function testCreatedByAdminOnPost(): void
|
||||||
{
|
{
|
||||||
|
// Horloge figee : le subscriber posera createdAt/updatedAt sur cet
|
||||||
|
// instant exact, insensible a tout decalage d'horloge reel.
|
||||||
|
$clock = $this->freezeClock();
|
||||||
|
|
||||||
$type = $this->createCategoryType();
|
$type = $this->createCategoryType();
|
||||||
|
|
||||||
/** @var User $admin */
|
/** @var User $admin */
|
||||||
@@ -33,9 +62,7 @@ final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
|||||||
self::assertNotNull($admin);
|
self::assertNotNull($admin);
|
||||||
$adminId = $admin->getId();
|
$adminId = $admin->getId();
|
||||||
|
|
||||||
$before = new DateTimeImmutable();
|
$before = $clock->now();
|
||||||
// Petit decalage pour absorber les arrondis a la seconde de Postgres.
|
|
||||||
sleep(1);
|
|
||||||
|
|
||||||
$client = $this->createAdminClient();
|
$client = $this->createAdminClient();
|
||||||
$response = $client->request('POST', '/api/categories', [
|
$response = $client->request('POST', '/api/categories', [
|
||||||
@@ -103,6 +130,8 @@ final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
|||||||
|
|
||||||
public function testPatchUpdatesUpdatedFieldsOnly(): void
|
public function testPatchUpdatesUpdatedFieldsOnly(): void
|
||||||
{
|
{
|
||||||
|
$clock = $this->freezeClock();
|
||||||
|
|
||||||
// Etape 1 : creation par admin pour figer createdBy=admin.
|
// Etape 1 : creation par admin pour figer createdBy=admin.
|
||||||
$type = $this->createCategoryType();
|
$type = $this->createCategoryType();
|
||||||
$adminClient = $this->createAdminClient();
|
$adminClient = $this->createAdminClient();
|
||||||
@@ -127,9 +156,9 @@ final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
|||||||
$initialUpdatedAt = $initial->getUpdatedAt();
|
$initialUpdatedAt = $initial->getUpdatedAt();
|
||||||
$initialCreatedById = $initial->getCreatedBy()->getId();
|
$initialCreatedById = $initial->getCreatedBy()->getId();
|
||||||
|
|
||||||
// Decalage temporel suffisant pour que la precision PG (seconde)
|
// Avance deterministe de l'horloge mockee : garantit un updatedAt
|
||||||
// capte un updatedAt different.
|
// strictement superieur cote PG (precision seconde) sans sleep reel.
|
||||||
sleep(1);
|
$clock->sleep(1);
|
||||||
|
|
||||||
// Etape 2 : PATCH par un autre user (manager non-admin) — simule "bob".
|
// Etape 2 : PATCH par un autre user (manager non-admin) — simule "bob".
|
||||||
$manage = $this->createManageClient();
|
$manage = $this->createManageClient();
|
||||||
@@ -180,6 +209,8 @@ final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
|||||||
|
|
||||||
public function testSoftDeleteAlsoUpdatesUpdatedFields(): void
|
public function testSoftDeleteAlsoUpdatesUpdatedFields(): void
|
||||||
{
|
{
|
||||||
|
$clock = $this->freezeClock();
|
||||||
|
|
||||||
// RG-1.16 : le soft delete est un UPDATE Doctrine, donc le subscriber
|
// RG-1.16 : le soft delete est un UPDATE Doctrine, donc le subscriber
|
||||||
// doit aussi avancer updatedAt et updatedBy en plus de poser deletedAt.
|
// doit aussi avancer updatedAt et updatedBy en plus de poser deletedAt.
|
||||||
$type = $this->createCategoryType();
|
$type = $this->createCategoryType();
|
||||||
@@ -202,7 +233,8 @@ final class CategoryTimestampableBlamableTest extends AbstractCatalogApiTestCase
|
|||||||
$initial = $em->getRepository(Category::class)->find($createdId);
|
$initial = $em->getRepository(Category::class)->find($createdId);
|
||||||
$initialUpdatedAt = $initial->getUpdatedAt();
|
$initialUpdatedAt = $initial->getUpdatedAt();
|
||||||
|
|
||||||
sleep(1);
|
// Avance deterministe de l'horloge mockee (cf. testPatch).
|
||||||
|
$clock->sleep(1);
|
||||||
|
|
||||||
// Soft delete par un manager non-admin.
|
// Soft delete par un manager non-admin.
|
||||||
$manage = $this->createManageClient();
|
$manage = $this->createManageClient();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use Doctrine\ORM\Event\PrePersistEventArgs;
|
|||||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,7 @@ final class TimestampableBlamableSubscriberTest extends TestCase
|
|||||||
public function testPrePersistWithUser(): void
|
public function testPrePersistWithUser(): void
|
||||||
{
|
{
|
||||||
$user = $this->createStub(UserInterface::class);
|
$user = $this->createStub(UserInterface::class);
|
||||||
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user));
|
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user), new MockClock());
|
||||||
$entity = new FullAuditableFixture();
|
$entity = new FullAuditableFixture();
|
||||||
|
|
||||||
$subscriber->prePersist($this->prePersistArgs($entity));
|
$subscriber->prePersist($this->prePersistArgs($entity));
|
||||||
@@ -45,7 +46,7 @@ final class TimestampableBlamableSubscriberTest extends TestCase
|
|||||||
|
|
||||||
public function testPrePersistWithoutUser(): void
|
public function testPrePersistWithoutUser(): void
|
||||||
{
|
{
|
||||||
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning(null));
|
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning(null), new MockClock());
|
||||||
$entity = new FullAuditableFixture();
|
$entity = new FullAuditableFixture();
|
||||||
|
|
||||||
$subscriber->prePersist($this->prePersistArgs($entity));
|
$subscriber->prePersist($this->prePersistArgs($entity));
|
||||||
@@ -59,8 +60,13 @@ final class TimestampableBlamableSubscriberTest extends TestCase
|
|||||||
|
|
||||||
public function testPreUpdate(): void
|
public function testPreUpdate(): void
|
||||||
{
|
{
|
||||||
$user = $this->createStub(UserInterface::class);
|
$user = $this->createStub(UserInterface::class);
|
||||||
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user));
|
// Horloge figee 1s apres le createdAt simule : updatedAt doit avancer
|
||||||
|
// de facon deterministe, sans dependre de l'heure reelle.
|
||||||
|
$subscriber = new TimestampableBlamableSubscriber(
|
||||||
|
$this->securityReturning($user),
|
||||||
|
new MockClock(new DateTimeImmutable('2020-01-01 10:00:01')),
|
||||||
|
);
|
||||||
|
|
||||||
// On simule une entite deja persistee : createdAt fige dans le passe,
|
// On simule une entite deja persistee : createdAt fige dans le passe,
|
||||||
// createdBy positionne par une creation anterieure.
|
// createdBy positionne par une creation anterieure.
|
||||||
@@ -80,7 +86,7 @@ final class TimestampableBlamableSubscriberTest extends TestCase
|
|||||||
public function testPartialEntityTimestampableOnly(): void
|
public function testPartialEntityTimestampableOnly(): void
|
||||||
{
|
{
|
||||||
$user = $this->createStub(UserInterface::class);
|
$user = $this->createStub(UserInterface::class);
|
||||||
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user));
|
$subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user), new MockClock());
|
||||||
$entity = new TimestampableOnlyFixture();
|
$entity = new TimestampableOnlyFixture();
|
||||||
|
|
||||||
// Entite Timestampable mais NON Blamable : seules les dates sont posees,
|
// Entite Timestampable mais NON Blamable : seules les dates sont posees,
|
||||||
|
|||||||
Reference in New Issue
Block a user