fix(rbac) : usine peut lire les listes client/fournisseur pour le select de contrepartie pesée (ERP-209)
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.
This commit is contained in:
@@ -77,6 +77,9 @@ export const personas: Record<PersonaKey, Persona> = {
|
||||
// (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<PersonaKey, Persona> = {
|
||||
// (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',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user