fix(technique) : cloisonner par site les sous-ressources prestataire + RG-3.04 fonction (ERP-134, ERP-135)
Les operations Get/Patch/Delete des sous-ressources Contact/Adresse/RIB passaient par le provider Doctrine par defaut (non cloisonne), et le POST resolvait le parent sans controle de scope : un user cloisonne pouvait lire/editer/supprimer une sous-ressource d'un prestataire hors de son site (IBAN/BIC du RIB inclus). SiteScopedQueryExtension ne filtre que les SiteAwareInterface, que ces entites ne sont pas. - ProviderSiteScopeChecker : decision de cloisonnement centralisee (source unique), consommee par ProviderProvider (refactore), le provider decore et les processors. - ProviderSubResourceItemProvider : decore le provider par defaut sur Get/Patch/Delete des 3 sous-ressources -> 404 si parent hors perimetre. - Garde assertInScope au POST dans les 3 processors -> 404 si parent hors perimetre. ProviderOwnedInterface sur les 3 entites. RG-3.04 : alignement code <-> spec (ligne 926). La Fonction (jobTitle) rend desormais un contact valide a elle seule : ajout au validateName, au CHECK chk_provider_contact_name et normalisation (normalizeText, vide -> null). Tests : ProviderSubResourceSiteScopeTest (fuite cross-site, 7 cas) ; RG-3.04 jobTitle reecrit. Spec § 2.13 corrigee (l'heritage n'etait pas automatique). Suite back complete verte (685 tests).
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Technique\Api;
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\Test\Client;
|
||||
|
||||
/**
|
||||
* Tests du cloisonnement par site des SOUS-RESSOURCES d'un prestataire (Contacts /
|
||||
* Adresses / RIB) — § 2.13 / RG-3.17. Complement de ProviderSiteScopeTest (qui ne
|
||||
* couvrait que le Provider lui-meme).
|
||||
*
|
||||
* Sans garde dedie, un user cloisonne pouvait lire / editer / supprimer une
|
||||
* sous-ressource d'un prestataire HORS de son site (le detail Provider est garde en
|
||||
* 404, mais les sous-ressources passent par le provider Doctrine par defaut, non
|
||||
* cloisonne — et SiteScopedQueryExtension ne filtre que les SiteAwareInterface).
|
||||
* Le RIB est particulierement sensible (IBAN / BIC).
|
||||
*
|
||||
* Garde pose par ProviderSubResourceItemProvider (Get/Patch/Delete -> 404 hors
|
||||
* perimetre) + ProviderSiteScopeChecker::assertInScope dans les processors (POST
|
||||
* sur parent hors perimetre -> 404). Decision de scope partagee (source unique).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ProviderSubResourceSiteScopeTest extends AbstractProviderApiTestCase
|
||||
{
|
||||
/** Permissions completes pour exercer view + manage + accounting sur tous les chemins. */
|
||||
private const array FULL_PERMS = [
|
||||
'technique.providers.view',
|
||||
'technique.providers.manage',
|
||||
'technique.providers.accounting.view',
|
||||
'technique.providers.accounting.manage',
|
||||
];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->skipIfSitesModuleDisabled();
|
||||
}
|
||||
|
||||
public function testGetContactOutOfScopeReturns404ButInScope200(): void
|
||||
{
|
||||
$inScope = $this->seedProvider('Presta In Scope', [self::SITE_86]);
|
||||
$inContactId = $this->addContact($inScope, 'Marie', 'Martin')->getId();
|
||||
|
||||
$outScope = $this->seedProvider('Presta Out Scope', [self::SITE_17]);
|
||||
$outContactId = $this->addContact($outScope, 'Paul', 'Durand')->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$ok = $client->request('GET', '/api/provider_contacts/'.$inContactId, ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(200, $ok->getStatusCode());
|
||||
|
||||
// Hors perimetre : 404 (ne pas reveler l'existence du contact d'un autre site).
|
||||
$ko = $client->request('GET', '/api/provider_contacts/'.$outContactId, ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(404, $ko->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGetRibOutOfScopeReturns404(): void
|
||||
{
|
||||
// RIB = donnee bancaire sensible (IBAN/BIC) : le cas le plus critique.
|
||||
$outScope = $this->seedProvider('Presta Out Rib', [self::SITE_17]);
|
||||
$ribId = $this->addRib($outScope)->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$response = $client->request('GET', '/api/provider_ribs/'.$ribId, ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPatchRibOutOfScopeReturns404(): void
|
||||
{
|
||||
$outScope = $this->seedProvider('Presta Patch Rib', [self::SITE_17]);
|
||||
$ribId = $this->addRib($outScope)->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$response = $client->request('PATCH', '/api/provider_ribs/'.$ribId, [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['label' => 'Hacked'],
|
||||
]);
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDeleteContactOutOfScopeReturns404(): void
|
||||
{
|
||||
$outScope = $this->seedProvider('Presta Del Contact', [self::SITE_17]);
|
||||
$contactId = $this->addContact($outScope, 'Paul', 'Durand')->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$response = $client->request('DELETE', '/api/provider_contacts/'.$contactId);
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostContactOnOutOfScopeProviderReturns404(): void
|
||||
{
|
||||
$outScope = $this->seedProvider('Presta Post Contact', [self::SITE_17]);
|
||||
$id = $outScope->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$response = $client->request('POST', '/api/providers/'.$id.'/contacts', [
|
||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||
'json' => ['firstName' => 'Intrus'],
|
||||
]);
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostRibOnOutOfScopeProviderReturns404(): void
|
||||
{
|
||||
$outScope = $this->seedProvider('Presta Post Rib', [self::SITE_17]);
|
||||
$id = $outScope->getId();
|
||||
|
||||
$client = $this->scopedClient();
|
||||
|
||||
$response = $client->request('POST', '/api/providers/'.$id.'/ribs', [
|
||||
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
||||
'json' => [
|
||||
'label' => 'Intrus',
|
||||
'iban' => self::VALID_IBAN,
|
||||
'bic' => self::VALID_BIC,
|
||||
],
|
||||
]);
|
||||
self::assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBypassUserReachesSubResourceOnAnySite(): void
|
||||
{
|
||||
// Temoin : l'admin (bypass total) lit bien un contact hors « son » site.
|
||||
$outScope = $this->seedProvider('Presta Admin Reach', [self::SITE_17]);
|
||||
$contactId = $this->addContact($outScope, 'Marie', 'Martin')->getId();
|
||||
|
||||
$client = $this->createAdminClient();
|
||||
$response = $client->request('GET', '/api/provider_contacts/'.$contactId, ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Client authentifie comme un user NON-bypass rattache au seul site 86 (avec
|
||||
* currentSite 86) — sujet des tests de cloisonnement des sous-ressources.
|
||||
*/
|
||||
private function scopedClient(): Client
|
||||
{
|
||||
$creds = $this->createScopedUser(
|
||||
self::FULL_PERMS,
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
|
||||
return $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user