From d21025067af38092033c943f8cab94ef65acd9b0 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 12:00:57 +0200 Subject: [PATCH] =?UTF-8?q?fix(rbac)=20:=20usine=20peut=20lire=20les=20lis?= =?UTF-8?q?tes=20client/fournisseur=20pour=20le=20select=20de=20contrepart?= =?UTF-8?q?ie=20pes=C3=A9e=20(ERP-209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problème : connecté en usine (usine17/82/86), les déroulants Client/Fournisseur du ticket de pesée sortent vides. Cause = 403 : le rôle usine n'a pas `commercial.clients.view`/`suppliers.view`, et le composable contrepartie (résilient au 403) laisse les selects vides. Ce n'est PAS le filtre site (ERP-208). Fix (permission dédiée « référentiel », ne fuit pas le répertoire) : - CommercialModule : nouvelles permissions `commercial.clients.read_ref` et `commercial.suppliers.read_ref` (lecture de la LISTE pour alimenter un select). - Client/Supplier GetCollection : security `view OR read_ref`. Seule la collection s'ouvre ; item Get, POST et PATCH restent gardés par `view`/`manage`. La sidebar répertoire reste gardée par `view` → usine ne voit pas le répertoire. - RbacSeeder (rôle Usine) : ajout des 2 read_ref. - Miroirs RBAC (règle ABSOLUE n°8) : personas.ts (user-full) + SeedE2ECommand alignés. - Tests : testUsineIsForbiddenEverywhere → testUsineCanReadListButNothingElse (200 sur la liste, 403 sur détail/création/édition) pour Client et Supplier ; l'assertion categories/sites confirme que read_ref ne couvre QUE clients/suppliers. Déploiement : jouer `app:sync-permissions` puis `app:seed-rbac`. Si usine17/82/86 utilisent des rôles custom (pas le rôle « usine » standard), leur ajouter les 2 permissions read_ref à la main. RETOUR ARRIÈRE MÉTIER (si l'usine ne doit PAS voir les tiers) : retirer les 2 read_ref de RbacSeeder ROLE_USINE + de CommercialModule + le `or ...read_ref` des GetCollection Client/Supplier, puis sync-permissions + seed-rbac. Restaurer aussi les tests testUsineIsForbiddenEverywhere (403 partout). Note : une fois le 403 levé, la liste reste filtrée sur le site courant (ERP-208) ; si aucun tiers n'a d'adresse sur le site usine, le select peut rester vide — point de données/sémantique distinct, à arbitrer séparément. --- frontend/tests/e2e/_fixtures/personas.ts | 5 +++++ src/Module/Commercial/CommercialModule.php | 6 ++++++ src/Module/Commercial/Domain/Entity/Client.php | 6 +++++- src/Module/Commercial/Domain/Entity/Supplier.php | 6 +++++- src/Module/Core/Application/Rbac/RbacSeeder.php | 11 +++++++++++ .../Core/Infrastructure/Console/SeedE2ECommand.php | 4 ++++ tests/Module/Commercial/Api/ClientRBACMatrixTest.php | 12 ++++++++---- .../Module/Commercial/Api/SupplierRBACMatrixTest.php | 11 ++++++++--- 8 files changed, 52 insertions(+), 9 deletions(-) diff --git a/frontend/tests/e2e/_fixtures/personas.ts b/frontend/tests/e2e/_fixtures/personas.ts index 6596efa..8417805 100644 --- a/frontend/tests/e2e/_fixtures/personas.ts +++ b/frontend/tests/e2e/_fixtures/personas.ts @@ -77,6 +77,9 @@ export const personas: Record = { // (regle ABSOLUE n°7). commercial.clients.view n'ajoute pas de lien // dans la section Administration, donc expectedAdminLinks reste inchange. 'commercial.clients.view', + // Lecture liste seule pour le select de contrepartie pesee (ERP-209). + // Redondant ici (user-full a deja `view`) mais miroir du rang RBAC. + 'commercial.clients.read_ref', 'commercial.clients.manage', 'commercial.clients.accounting.view', 'commercial.clients.accounting.manage', @@ -86,6 +89,8 @@ export const personas: Record = { // (regle ABSOLUE n°7). commercial.suppliers.view n'ajoute pas de lien // dans la section Administration, donc expectedAdminLinks reste inchange. 'commercial.suppliers.view', + // Lecture liste seule pour le select de contrepartie pesee (ERP-209). + 'commercial.suppliers.read_ref', 'commercial.suppliers.manage', 'commercial.suppliers.accounting.view', 'commercial.suppliers.accounting.manage', diff --git a/src/Module/Commercial/CommercialModule.php b/src/Module/Commercial/CommercialModule.php index 71157c9..0e7be95 100644 --- a/src/Module/Commercial/CommercialModule.php +++ b/src/Module/Commercial/CommercialModule.php @@ -35,11 +35,17 @@ final class CommercialModule { return [ ['code' => 'commercial.clients.view', 'label' => 'Voir les clients'], + // Lecture de la LISTE clients pour alimenter un select (contrepartie d'un + // ticket de pesee — role Usine, ERP-209), SANS le repertoire ni le detail. + ['code' => 'commercial.clients.read_ref', 'label' => 'Lire la liste des clients (référentiel pour les selects)'], ['code' => 'commercial.clients.manage', 'label' => 'Créer / modifier les clients (hors onglet Comptabilité)'], ['code' => 'commercial.clients.accounting.view', 'label' => 'Voir l\'onglet Comptabilité d\'un client'], ['code' => 'commercial.clients.accounting.manage', 'label' => 'Modifier l\'onglet Comptabilité d\'un client'], ['code' => 'commercial.clients.archive', 'label' => 'Archiver / restaurer un client'], ['code' => 'commercial.suppliers.view', 'label' => 'Voir les fournisseurs'], + // Lecture de la LISTE fournisseurs pour alimenter un select (contrepartie + // d'un ticket de pesee — role Usine, ERP-209), SANS le repertoire ni le detail. + ['code' => 'commercial.suppliers.read_ref', 'label' => 'Lire la liste des fournisseurs (référentiel pour les selects)'], ['code' => 'commercial.suppliers.manage', 'label' => 'Créer / modifier les fournisseurs (hors onglet Comptabilité)'], ['code' => 'commercial.suppliers.accounting.view', 'label' => 'Voir l\'onglet Comptabilité d\'un fournisseur'], ['code' => 'commercial.suppliers.accounting.manage', 'label' => 'Modifier l\'onglet Comptabilité d\'un fournisseur'], diff --git a/src/Module/Commercial/Domain/Entity/Client.php b/src/Module/Commercial/Domain/Entity/Client.php index 09e1ea6..f8c979d 100644 --- a/src/Module/Commercial/Domain/Entity/Client.php +++ b/src/Module/Commercial/Domain/Entity/Client.php @@ -63,7 +63,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ApiResource( operations: [ new GetCollection( - security: "is_granted('commercial.clients.view')", + // `read_ref` (ERP-209) : lecture de la LISTE seule, pour alimenter le + // select de contrepartie du ticket de pesee (role Usine, qui n'a pas + // `view` et donc pas le repertoire). N'ouvre que la collection — l'item, + // la creation et l'edition restent gardes par `view`/`manage`. + security: "is_granted('commercial.clients.view') or is_granted('commercial.clients.read_ref')", // La liste embarque les categories (avec leur code, groupe // category:read) et les sites agreges des adresses (groupe // site:read) pour alimenter les colonnes « Catégories » et diff --git a/src/Module/Commercial/Domain/Entity/Supplier.php b/src/Module/Commercial/Domain/Entity/Supplier.php index b32827c..84a24a0 100644 --- a/src/Module/Commercial/Domain/Entity/Supplier.php +++ b/src/Module/Commercial/Domain/Entity/Supplier.php @@ -66,7 +66,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ApiResource( operations: [ new GetCollection( - security: "is_granted('commercial.suppliers.view')", + // `read_ref` (ERP-209) : lecture de la LISTE seule, pour alimenter le + // select de contrepartie du ticket de pesee (role Usine, qui n'a pas + // `view` et donc pas le repertoire). N'ouvre que la collection — l'item, + // la creation et l'edition restent gardes par `view`/`manage`. + security: "is_granted('commercial.suppliers.view') or is_granted('commercial.suppliers.read_ref')", // La liste embarque les categories (avec leur code/name, groupe // category:read) et les sites agreges des adresses (groupe // site:read) pour alimenter les colonnes « Catégories » et diff --git a/src/Module/Core/Application/Rbac/RbacSeeder.php b/src/Module/Core/Application/Rbac/RbacSeeder.php index 0f14233..586f67a 100644 --- a/src/Module/Core/Application/Rbac/RbacSeeder.php +++ b/src/Module/Core/Application/Rbac/RbacSeeder.php @@ -148,6 +148,17 @@ final class RbacSeeder // bypass_scope ; les tickets sont filtres par SiteScopedQueryExtension). 'logistique.weighing_tickets.view', 'logistique.weighing_tickets.manage', + // Lecture des LISTES client/fournisseur pour le select de contrepartie + // du ticket de pesee (ERP-209). `read_ref` n'ouvre QUE la collection + // /clients + /suppliers (pas le repertoire sidebar, pas le detail, pas + // l'edition) -> l'Usine peut choisir un tiers sans acceder au module + // Commercial. + // /!\ RETOUR ARRIERE METIER : si l'Usine ne doit PAS voir les tiers, + // retirer ces 2 lignes + les 2 permissions read_ref de CommercialModule + // + le `or ...read_ref` des GetCollection Client/Supplier, puis + // `app:sync-permissions` + re-seed RBAC. + 'commercial.clients.read_ref', + 'commercial.suppliers.read_ref', ], ], ]; diff --git a/src/Module/Core/Infrastructure/Console/SeedE2ECommand.php b/src/Module/Core/Infrastructure/Console/SeedE2ECommand.php index 3763268..5c7ba71 100644 --- a/src/Module/Core/Infrastructure/Console/SeedE2ECommand.php +++ b/src/Module/Core/Infrastructure/Console/SeedE2ECommand.php @@ -195,6 +195,8 @@ final class SeedE2ECommand extends Command // (bureau/compta/commerciale/usine) seedes par ERP-74. // Miroir de frontend/tests/e2e/_fixtures/personas.ts. 'commercial.clients.view', + // Lecture liste seule pour le select de contrepartie pesee (ERP-209). + 'commercial.clients.read_ref', 'commercial.clients.manage', 'commercial.clients.accounting.view', 'commercial.clients.accounting.manage', @@ -203,6 +205,8 @@ final class SeedE2ECommand extends Command // logique que les clients : mappe sur le persona "tout". // Miroir de frontend/tests/e2e/_fixtures/personas.ts. 'commercial.suppliers.view', + // Lecture liste seule pour le select de contrepartie pesee (ERP-209). + 'commercial.suppliers.read_ref', 'commercial.suppliers.manage', 'commercial.suppliers.accounting.view', 'commercial.suppliers.accounting.manage', diff --git a/tests/Module/Commercial/Api/ClientRBACMatrixTest.php b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php index bd51856..446fe9e 100644 --- a/tests/Module/Commercial/Api/ClientRBACMatrixTest.php +++ b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php @@ -55,15 +55,18 @@ final class ClientRBACMatrixTest extends AbstractCommercialApiTestCase self::ensureKernelShutdown(); } - public function testUsineIsForbiddenEverywhere(): void + public function testUsineCanReadClientListButNothingElse(): void { $seed = $this->seedClient('Usine Target'); $client = $this->authAs('usine'); - // Aucune permission : 403 sur tous les verbes. + // ERP-209 : `commercial.clients.read_ref` ouvre la LISTE seule (select de + // contrepartie du ticket de pesee) -> 200 sur la collection. $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); - self::assertResponseStatusCodeSame(403); + self::assertResponseStatusCodeSame(200); + // Mais RIEN d'autre : detail, creation et edition restent gardes par + // view/manage -> 403. (Retour arriere metier : cf. RbacSeeder ROLE_USINE.) $client->request('GET', '/api/clients/'.$seed->getId(), ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(403); @@ -288,7 +291,8 @@ final class ClientRBACMatrixTest extends AbstractCommercialApiTestCase ); } - // Usine : aucune permission -> reste a 403 sur les referentiels. + // Usine : `read_ref` ne couvre QUE clients/suppliers (ERP-209), pas les + // referentiels categories/sites -> reste a 403 sur ces deux-la. $usine = $this->authAs('usine'); $usine->request('GET', '/api/categories', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(403, 'Usine ne doit pas pouvoir lister /categories'); diff --git a/tests/Module/Commercial/Api/SupplierRBACMatrixTest.php b/tests/Module/Commercial/Api/SupplierRBACMatrixTest.php index 55cc6d4..c0282a6 100644 --- a/tests/Module/Commercial/Api/SupplierRBACMatrixTest.php +++ b/tests/Module/Commercial/Api/SupplierRBACMatrixTest.php @@ -24,7 +24,8 @@ use Symfony\Component\Console\Output\NullOutput; * - bureau : suppliers.view + manage (ni accounting, ni archive) * - compta : suppliers.view + accounting.view + accounting.manage (PAS manage) * - commerciale : suppliers.view + manage (PAS accounting) - * - usine : aucune permission (403 partout) + * - usine : read_ref seul -> 200 sur la LISTE (select contrepartie pesee, + * ERP-209), 403 sur detail/creation/edition * - archive : admin seul (aucun role metier) * * @internal @@ -59,14 +60,18 @@ final class SupplierRBACMatrixTest extends AbstractSupplierApiTestCase self::ensureKernelShutdown(); } - public function testUsineIsForbiddenEverywhere(): void + public function testUsineCanReadSupplierListButNothingElse(): void { $seed = $this->seedSupplier('Usine Target'); $client = $this->authAs('usine'); + // ERP-209 : `commercial.suppliers.read_ref` ouvre la LISTE seule (select de + // contrepartie du ticket de pesee) -> 200 sur la collection. $client->request('GET', '/api/suppliers', ['headers' => ['Accept' => self::LD]]); - self::assertResponseStatusCodeSame(403); + self::assertResponseStatusCodeSame(200); + // Mais RIEN d'autre : detail, creation et edition restent gardes par + // view/manage -> 403. (Retour arriere metier : cf. RbacSeeder ROLE_USINE.) $client->request('GET', '/api/suppliers/'.$seed->getId(), ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(403);