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);