feat(core) : add idempotent app:seed-rbac command (roles + matrix + demo users)
- RbacSeeder : source unique des 4 roles metier (bureau/compta/commerciale/usine), de la matrice RBAC § 2.7 (role -> permissions) et des comptes demo. Operations idempotentes et non destructives (ensureRoles / attachMatrix / ensureDemoUsers). - app:seed-rbac : commande applicative presente en build prod (contrairement aux fixtures require-dev). Sans option : roles + matrice. --with-demo-users + --password / RBAC_DEMO_PASSWORD : un compte demo par role. Garde-fou : exit non-zero + invite a lancer app:sync-permissions si les codes manquent. - RbacDemoFixtures (dev/test) : appelle le meme seeder (DRY). La matrice est attachee post-sync par app:seed-rbac (la table permission est purgee au load). - makefile : etape seed-rbac apres sync-permissions (db-reset + test-db-setup). - Doc deploiement (README) + credentials des comptes demo (CLAUDE.md / README).
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## Contexte
|
## Contexte
|
||||||
CRM/ERP en architecture **modular monolith DDD**. Le backend est la source de verite unique (modules actifs, sidebar). Le frontend scanne `frontend/modules/*/` comme layers Nuxt et consomme l'API pour la navigation. Multi-tenant : chaque module est activable/desactivable.
|
CRM/ERP en architecture **modular monolith DDD**. Le backend est la source de verite unique (modules actifs, sidebar). Le frontend scanne `frontend/modules/*/` comme layers Nuxt et consomme l'API pour la navigation. Multi-tenant : chaque module est activable/desactivable.
|
||||||
|
|
||||||
Doc humaine : @README.md — Spec audit : @doc/audit-log.md
|
Doc humaine : `README.md` — Spec audit : `doc/audit-log.md` (à lire à la demande, non chargés en permanence).
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- Backend : PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM, PostgreSQL 16 (port 5437)
|
- Backend : PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM, PostgreSQL 16 (port 5437)
|
||||||
@@ -37,7 +37,7 @@ Doc humaine : @README.md — Spec audit : @doc/audit-log.md
|
|||||||
@.claude/rules/git.md
|
@.claude/rules/git.md
|
||||||
@.claude/rules/workflow.md
|
@.claude/rules/workflow.md
|
||||||
|
|
||||||
## Commandes (liste complete dans @README.md)
|
## Commandes (liste complete dans `README.md`)
|
||||||
|
|
||||||
- Demarrer : `make start`
|
- Demarrer : `make start`
|
||||||
- Dev front (hot reload) : `make dev-nuxt` (port 3004)
|
- Dev front (hot reload) : `make dev-nuxt` (port 3004)
|
||||||
@@ -70,3 +70,5 @@ Editer uniquement `config/modules.php` (commenter la ligne). Cascade automatique
|
|||||||
## Credentials (dev)
|
## Credentials (dev)
|
||||||
|
|
||||||
`admin` / `admin` (ROLE_ADMIN) ; `alice` / `alice`, `bob` / `bob` (ROLE_USER).
|
`admin` / `admin` (ROLE_ADMIN) ; `alice` / `alice`, `bob` / `bob` (ROLE_USER).
|
||||||
|
|
||||||
|
Comptes demo des roles metier (seedes par `RbacDemoFixtures` / `app:seed-rbac --with-demo-users`, mot de passe `demo`) : `bureau` / `demo`, `compta` / `demo`, `commerciale` / `demo`, `usine` / `demo`. Matrice RBAC § 2.7 (M1 Clients) attachee aux roles correspondants.
|
||||||
|
|||||||
@@ -169,13 +169,41 @@ Secrets requis dans Gitea :
|
|||||||
- `RELEASE_TOKEN` — PAT avec droits `write:repository`
|
- `RELEASE_TOKEN` — PAT avec droits `write:repository`
|
||||||
- `REGISTRY_TOKEN` — token pour le registry Docker
|
- `REGISTRY_TOKEN` — token pour le registry Docker
|
||||||
|
|
||||||
|
## Déploiement — seed RBAC (recette / prod)
|
||||||
|
|
||||||
|
Le RBAC métier (rôles `bureau` / `compta` / `commerciale` / `usine` + matrice § 2.7)
|
||||||
|
est seedé par une **commande applicative idempotente** (présente dans le build prod,
|
||||||
|
contrairement aux fixtures Doctrine en `require-dev`). À jouer dans l'étape de release,
|
||||||
|
**après** les migrations et la synchronisation des permissions :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
php bin/console app:sync-permissions # pose les permissions commercial.clients.*
|
||||||
|
php bin/console app:seed-rbac # PROD : rôles + matrice § 2.7 (sans comptes démo)
|
||||||
|
```
|
||||||
|
|
||||||
|
En **recette / staging**, ajouter le flag pour disposer de logins de test (mot de passe
|
||||||
|
fourni explicitement, jamais en dur) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:seed-rbac --with-demo-users --password='<mot-de-passe>'
|
||||||
|
# ou via la variable d'env RBAC_DEMO_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
La commande est rejouable sans effet de bord (aucun doublon de rôle, de lien ou de compte).
|
||||||
|
En dev, `make db-reset` produit le même résultat (rôles + matrice + comptes démo).
|
||||||
|
|
||||||
## Credentials (dev)
|
## Credentials (dev)
|
||||||
|
|
||||||
| Username | Password | Role |
|
| Username | Password | Role | RBAC métier |
|
||||||
|----------|----------|------|
|
|----------|----------|------|-------------|
|
||||||
| admin | admin | ROLE_ADMIN |
|
| admin | admin | ROLE_ADMIN | bypass (is_admin) |
|
||||||
| alice | alice | ROLE_USER |
|
| alice | alice | ROLE_USER | — |
|
||||||
| bob | bob | ROLE_USER |
|
| bob | bob | ROLE_USER | — |
|
||||||
|
| bureau | demo | ROLE_USER | clients : view + manage |
|
||||||
|
| compta | demo | ROLE_USER | clients : view + accounting.view/manage |
|
||||||
|
| commerciale | demo | ROLE_USER | clients : view + manage (Information obligatoire — RG-1.04) |
|
||||||
|
| usine | demo | ROLE_USER | aucun accès clients |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -198,8 +198,11 @@ migration-migrate:
|
|||||||
# doctrine:fixtures:load essaie de DELETE toutes les tables connues
|
# doctrine:fixtures:load essaie de DELETE toutes les tables connues
|
||||||
# via les mappings — si fake_site_aware_entity est mappe mais absent
|
# via les mappings — si fake_site_aware_entity est mappe mais absent
|
||||||
# en DB, le purger crash.
|
# en DB, le purger crash.
|
||||||
# 3. fixtures -> sync-permissions : fixtures:load purge la table permission,
|
# 3. fixtures -> sync-permissions -> seed-rbac : fixtures:load purge la table
|
||||||
# donc sync doit passer apres.
|
# permission, donc sync doit passer apres. seed-rbac (matrice RBAC § 2.7)
|
||||||
|
# passe ensuite, car attachMatrix() exige les permissions en base. Les
|
||||||
|
# comptes demo sont crees par RbacDemoFixtures au load (sans la matrice,
|
||||||
|
# attachee ici). Cf. ERP-74.
|
||||||
# 4. recreation des index partiels uniques : schema:update drop les index
|
# 4. recreation des index partiels uniques : schema:update drop les index
|
||||||
# orphelins du mapping ORM. Les index partiels (LOWER + WHERE) ne sont pas
|
# orphelins du mapping ORM. Les index partiels (LOWER + WHERE) ne sont pas
|
||||||
# exprimables via les attributs Doctrine ORM (fonctionnel + partiel), donc
|
# exprimables via les attributs Doctrine ORM (fonctionnel + partiel), donc
|
||||||
@@ -220,6 +223,7 @@ test-db-setup:
|
|||||||
$(SYMFONY_CONSOLE) --env=test --no-interaction app:apply-column-comments
|
$(SYMFONY_CONSOLE) --env=test --no-interaction app:apply-column-comments
|
||||||
$(SYMFONY_CONSOLE) --env=test --no-interaction doctrine:fixtures:load
|
$(SYMFONY_CONSOLE) --env=test --no-interaction doctrine:fixtures:load
|
||||||
$(SYMFONY_CONSOLE) --env=test --no-interaction app:sync-permissions
|
$(SYMFONY_CONSOLE) --env=test --no-interaction app:sync-permissions
|
||||||
|
$(SYMFONY_CONSOLE) --env=test --no-interaction app:seed-rbac
|
||||||
$(SYMFONY_CONSOLE) --env=test dbal:run-sql "CREATE UNIQUE INDEX IF NOT EXISTS uq_category_name_type_active ON category (LOWER(name), category_type_id) WHERE deleted_at IS NULL"
|
$(SYMFONY_CONSOLE) --env=test dbal:run-sql "CREATE UNIQUE INDEX IF NOT EXISTS uq_category_name_type_active ON category (LOWER(name), category_type_id) WHERE deleted_at IS NULL"
|
||||||
$(SYMFONY_CONSOLE) --env=test dbal:run-sql "CREATE UNIQUE INDEX IF NOT EXISTS uq_client_company_name_active ON client (LOWER(company_name)) WHERE is_archived = FALSE AND deleted_at IS NULL"
|
$(SYMFONY_CONSOLE) --env=test dbal:run-sql "CREATE UNIQUE INDEX IF NOT EXISTS uq_client_company_name_active ON client (LOWER(company_name)) WHERE is_archived = FALSE AND deleted_at IS NULL"
|
||||||
|
|
||||||
@@ -231,6 +235,15 @@ fixtures:
|
|||||||
sync-permissions:
|
sync-permissions:
|
||||||
$(SYMFONY_CONSOLE) --no-interaction app:sync-permissions
|
$(SYMFONY_CONSOLE) --no-interaction app:sync-permissions
|
||||||
|
|
||||||
|
# Seed RBAC metier : roles (bureau/compta/commerciale/usine) + matrice § 2.7
|
||||||
|
# (+ comptes demo en dev). Idempotent et NON destructif. A lancer APRES
|
||||||
|
# sync-permissions (attachMatrix exige les permissions en base). Les comptes
|
||||||
|
# demo dev sont deja crees par RbacDemoFixtures (make fixtures) ; ici on attache
|
||||||
|
# la matrice (les permissions etaient purgees au moment du load fixtures).
|
||||||
|
# En recette/prod, c'est cette commande (avec/sans --with-demo-users) qui seede.
|
||||||
|
seed-rbac:
|
||||||
|
$(SYMFONY_CONSOLE) --no-interaction app:seed-rbac
|
||||||
|
|
||||||
# Attention, supprime votre bdd local
|
# Attention, supprime votre bdd local
|
||||||
db-reset:
|
db-reset:
|
||||||
$(DOCKER_COMPOSE) down -v
|
$(DOCKER_COMPOSE) down -v
|
||||||
@@ -240,6 +253,7 @@ db-reset:
|
|||||||
$(MAKE) migration-migrate
|
$(MAKE) migration-migrate
|
||||||
$(MAKE) fixtures
|
$(MAKE) fixtures
|
||||||
$(MAKE) sync-permissions
|
$(MAKE) sync-permissions
|
||||||
|
$(MAKE) seed-rbac
|
||||||
$(MAKE) test-db-setup
|
$(MAKE) test-db-setup
|
||||||
|
|
||||||
# Restart la bdd
|
# Restart la bdd
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Core\Application\Rbac;
|
||||||
|
|
||||||
|
use App\Module\Core\Domain\Entity\Role;
|
||||||
|
use App\Module\Core\Domain\Entity\User;
|
||||||
|
use App\Module\Core\Domain\Exception\RbacSeedException;
|
||||||
|
use App\Module\Core\Domain\Repository\PermissionRepositoryInterface;
|
||||||
|
use App\Module\Core\Domain\Repository\RoleRepositoryInterface;
|
||||||
|
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
|
||||||
|
use App\Shared\Domain\Contract\SiteProviderInterface;
|
||||||
|
use App\Shared\Domain\Security\BusinessRoles;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source UNIQUE (anti-drift) du RBAC metier MALIO : les 4 roles
|
||||||
|
* (bureau / compta / commerciale / usine), la matrice § 2.7 (role -> permissions)
|
||||||
|
* et les comptes demo par role. Aucun de ces litteraux ne doit etre duplique
|
||||||
|
* ailleurs (ni SQL en dur, ni autre fixture).
|
||||||
|
*
|
||||||
|
* Consomme par :
|
||||||
|
* - la commande applicative `app:seed-rbac` (presente dans le build prod, donc
|
||||||
|
* rejouable en recette/prod, contrairement aux fixtures `require-dev`) ;
|
||||||
|
* - la fixture Core dev/test (DRY : meme seeder).
|
||||||
|
*
|
||||||
|
* Toutes les operations sont idempotentes et non destructives :
|
||||||
|
* - ensureRoles() : cree un role par lookup de code (skip si present) ;
|
||||||
|
* - attachMatrix() : attache les permissions § 2.7 via la M2M role_permission,
|
||||||
|
* sans re-attacher un lien existant ; STOP explicite si un code manque ;
|
||||||
|
* - ensureDemoUsers() : cree un user par role (lookup par username, skip si
|
||||||
|
* present), rattache au role + a >= 1 site.
|
||||||
|
*/
|
||||||
|
final class RbacSeeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Codes des roles metier (snake_case, regex Role respectee). `commerciale`
|
||||||
|
* reference la constante Shared deja consommee par le ClientProcessor
|
||||||
|
* (RG-1.04) pour eviter tout drift : un seul litteral pour ce code.
|
||||||
|
*/
|
||||||
|
public const string ROLE_BUREAU = 'bureau';
|
||||||
|
public const string ROLE_COMPTA = 'compta';
|
||||||
|
public const string ROLE_COMMERCIALE = BusinessRoles::COMMERCIALE;
|
||||||
|
public const string ROLE_USINE = 'usine';
|
||||||
|
|
||||||
|
/** Site de rattachement par defaut des comptes demo (cf. SitesFixtures). */
|
||||||
|
private const string DEFAULT_SITE_NAME = 'Chatellerault';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition unique des 4 roles + matrice § 2.7. La cle est le code du role,
|
||||||
|
* `label` le libelle FR affichable, `permissions` la liste des codes RBAC a
|
||||||
|
* attacher (vide pour usine : aucun acces ; admin n'apparait pas car il
|
||||||
|
* bypass tout via isAdmin ; `commercial.clients.archive` n'est attache a
|
||||||
|
* aucun role metier — admin seul).
|
||||||
|
*
|
||||||
|
* @var array<string, array{label: string, permissions: list<string>}>
|
||||||
|
*/
|
||||||
|
private const array MATRIX = [
|
||||||
|
self::ROLE_BUREAU => [
|
||||||
|
'label' => 'Bureau',
|
||||||
|
'permissions' => [
|
||||||
|
'commercial.clients.view',
|
||||||
|
'commercial.clients.manage',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
self::ROLE_COMPTA => [
|
||||||
|
'label' => 'Comptabilité',
|
||||||
|
'permissions' => [
|
||||||
|
'commercial.clients.view',
|
||||||
|
'commercial.clients.accounting.view',
|
||||||
|
'commercial.clients.accounting.manage',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
self::ROLE_COMMERCIALE => [
|
||||||
|
'label' => 'Commerciale',
|
||||||
|
'permissions' => [
|
||||||
|
'commercial.clients.view',
|
||||||
|
'commercial.clients.manage',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
self::ROLE_USINE => [
|
||||||
|
'label' => 'Usine',
|
||||||
|
'permissions' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly RoleRepositoryInterface $roleRepository,
|
||||||
|
private readonly PermissionRepositoryInterface $permissionRepository,
|
||||||
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
|
private readonly SiteProviderInterface $siteProvider,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cree chaque role metier absent (lookup par code). Idempotent.
|
||||||
|
*
|
||||||
|
* @return list<string> codes des roles effectivement crees (vide au rejeu)
|
||||||
|
*/
|
||||||
|
public function ensureRoles(): array
|
||||||
|
{
|
||||||
|
$created = [];
|
||||||
|
|
||||||
|
foreach (self::MATRIX as $code => $definition) {
|
||||||
|
if (null !== $this->roleRepository->findByCode($code)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSystem=false : ce sont des roles metier, supprimables par un
|
||||||
|
// admin (contrairement aux roles systeme admin/user).
|
||||||
|
$this->roleRepository->save(new Role($code, $definition['label'], isSystem: false));
|
||||||
|
$created[] = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attache la matrice § 2.7 a chaque role via la M2M role_permission. Lookup
|
||||||
|
* de la permission par code ; un code absent leve une RbacSeedException
|
||||||
|
* (garde-fou : `app:sync-permissions` doit avoir tourne). Idempotent : un
|
||||||
|
* lien deja present n'est pas recree.
|
||||||
|
*
|
||||||
|
* @return int nombre de liens role->permission effectivement ajoutes (0 au rejeu)
|
||||||
|
*
|
||||||
|
* @throws RbacSeedException si un role ou une permission de la matrice manque
|
||||||
|
*/
|
||||||
|
public function attachMatrix(): int
|
||||||
|
{
|
||||||
|
$added = 0;
|
||||||
|
|
||||||
|
foreach (self::MATRIX as $code => $definition) {
|
||||||
|
$role = $this->roleRepository->findByCode($code);
|
||||||
|
if (null === $role) {
|
||||||
|
throw RbacSeedException::missingRole($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$touched = false;
|
||||||
|
foreach ($definition['permissions'] as $permissionCode) {
|
||||||
|
$permission = $this->permissionRepository->findByCode($permissionCode);
|
||||||
|
if (null === $permission) {
|
||||||
|
throw RbacSeedException::missingPermission($permissionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$role->getPermissions()->contains($permission)) {
|
||||||
|
$role->addPermission($permission);
|
||||||
|
$touched = true;
|
||||||
|
++$added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un seul flush par role, et seulement si un lien a change.
|
||||||
|
if ($touched) {
|
||||||
|
$this->roleRepository->save($role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $added;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cree un compte demo par role metier (username = code du role), non-admin,
|
||||||
|
* mot de passe hashe, rattache a son role et a >= 1 site. Lookup par
|
||||||
|
* username : idempotent (un compte existant est laisse intact, mot de passe
|
||||||
|
* inchange).
|
||||||
|
*
|
||||||
|
* @return list<string> usernames effectivement crees (vide au rejeu)
|
||||||
|
*
|
||||||
|
* @throws RbacSeedException si un role metier attendu est absent (ensureRoles non joue)
|
||||||
|
*/
|
||||||
|
public function ensureDemoUsers(string $password): array
|
||||||
|
{
|
||||||
|
// Rattachement a un site par defaut s'il existe (les flux login / me en
|
||||||
|
// ont besoin ; le repertoire clients n'est pas site-scope mais on reste
|
||||||
|
// coherent avec les fixtures admin/alice/bob).
|
||||||
|
$defaultSite = $this->siteProvider->findByName(self::DEFAULT_SITE_NAME);
|
||||||
|
$created = [];
|
||||||
|
|
||||||
|
foreach (array_keys(self::MATRIX) as $code) {
|
||||||
|
$username = $code;
|
||||||
|
if (null !== $this->userRepository->findByUsername($username)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = $this->roleRepository->findByCode($code);
|
||||||
|
if (null === $role) {
|
||||||
|
throw RbacSeedException::missingRole($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername($username);
|
||||||
|
$user->setIsAdmin(false);
|
||||||
|
$user->setPassword($this->passwordHasher->hashPassword($user, $password));
|
||||||
|
$user->addRbacRole($role);
|
||||||
|
|
||||||
|
if (null !== $defaultSite) {
|
||||||
|
$user->addSite($defaultSite);
|
||||||
|
$user->setCurrentSite($defaultSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->userRepository->save($user);
|
||||||
|
$created[] = $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des codes des roles metier definis (pour reporting / tests).
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function roleCodes(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::MATRIX);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Core\Domain\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erreur de seed RBAC (service RbacSeeder / commande app:seed-rbac).
|
||||||
|
*
|
||||||
|
* Deux causes possibles, toutes deux fatales et explicites :
|
||||||
|
* - role metier attendu introuvable (ensureRoles() n'a pas tourne avant
|
||||||
|
* attachMatrix() ou ensureDemoUsers()) ;
|
||||||
|
* - code de permission de la matrice § 2.7 absent du catalogue : signe que
|
||||||
|
* `app:sync-permissions` n'a pas ete joue. Le message embarque alors
|
||||||
|
* l'invite a lancer la synchronisation, exploitee telle quelle par la
|
||||||
|
* commande.
|
||||||
|
*/
|
||||||
|
final class RbacSeedException extends RuntimeException
|
||||||
|
{
|
||||||
|
public static function missingRole(string $roleCode): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'Role metier "%s" introuvable. Appelle RbacSeeder::ensureRoles() avant attachMatrix()/ensureDemoUsers().',
|
||||||
|
$roleCode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function missingPermission(string $permissionCode): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'Permission "%s" (matrice § 2.7) absente du catalogue. '
|
||||||
|
.'Lance d\'abord `bin/console app:sync-permissions` pour la poser en base, puis relance le seed RBAC.',
|
||||||
|
$permissionCode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Core\Infrastructure\Console;
|
||||||
|
|
||||||
|
use App\Module\Core\Application\Rbac\RbacSeeder;
|
||||||
|
use App\Module\Core\Domain\Exception\RbacSeedException;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed RBAC metier idempotent et NON destructif (cf. ERP-74 / spec-back M1
|
||||||
|
* § 2.7). Contrairement aux fixtures Doctrine (`require-dev`, absentes du build
|
||||||
|
* prod `--no-dev`), cette commande applicative est presente dans l'image prod :
|
||||||
|
* elle est donc rejouable en recette/staging/prod.
|
||||||
|
*
|
||||||
|
* Etape de release : a lancer APRES `doctrine:migrations:migrate` et
|
||||||
|
* `app:sync-permissions`.
|
||||||
|
* - En prod : `app:seed-rbac` (roles + matrice § 2.7, sans comptes demo).
|
||||||
|
* - En recette : `app:seed-rbac --with-demo-users --password=<...>` pour
|
||||||
|
* disposer de logins de test.
|
||||||
|
*
|
||||||
|
* Toute la logique (litteraux des roles, matrice, comptes demo) vit dans
|
||||||
|
* RbacSeeder — cette commande n'en est que l'enveloppe CLI.
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:seed-rbac',
|
||||||
|
description: 'Seede les roles metier RBAC + la matrice § 2.7 (idempotent, non destructif).',
|
||||||
|
)]
|
||||||
|
final class SeedRbacCommand extends Command
|
||||||
|
{
|
||||||
|
/** Variable d'environnement de repli pour le mot de passe des comptes demo. */
|
||||||
|
private const string PASSWORD_ENV = 'RBAC_DEMO_PASSWORD';
|
||||||
|
|
||||||
|
public function __construct(private readonly RbacSeeder $seeder)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption(
|
||||||
|
'with-demo-users',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Cree aussi un compte demo par role metier (recette/dev — JAMAIS en prod).',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'password',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Mot de passe des comptes demo (defaut : variable d\'env '.self::PASSWORD_ENV.'). Requis avec --with-demo-users.',
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
// 1. Roles metier + matrice § 2.7. attachMatrix() exige que les
|
||||||
|
// permissions soient en base : sinon RbacSeedException porteuse de
|
||||||
|
// l'invite a lancer `app:sync-permissions`.
|
||||||
|
try {
|
||||||
|
$createdRoles = $this->seeder->ensureRoles();
|
||||||
|
$addedLinks = $this->seeder->attachMatrix();
|
||||||
|
} catch (RbacSeedException $e) {
|
||||||
|
$io->error($e->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
'Roles metier : %d cree(s), matrice § 2.7 : %d lien(s) ajoute(s).',
|
||||||
|
count($createdRoles),
|
||||||
|
$addedLinks,
|
||||||
|
));
|
||||||
|
|
||||||
|
// 2. Comptes demo (optionnel, jamais en prod).
|
||||||
|
if ((bool) $input->getOption('with-demo-users')) {
|
||||||
|
$password = $this->resolveDemoPassword($input);
|
||||||
|
if (null === $password) {
|
||||||
|
$io->error(sprintf(
|
||||||
|
'--with-demo-users exige un mot de passe : passe --password=<...> ou definis la variable d\'env %s. '
|
||||||
|
.'(Aucun mot de passe en dur cote serveur.)',
|
||||||
|
self::PASSWORD_ENV,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$createdUsers = $this->seeder->ensureDemoUsers($password);
|
||||||
|
} catch (RbacSeedException $e) {
|
||||||
|
$io->error($e->getMessage());
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
'Comptes demo : %d cree(s)%s.',
|
||||||
|
count($createdUsers),
|
||||||
|
[] === $createdUsers ? '' : ' ['.implode(', ', $createdUsers).']',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success('Seed RBAC metier termine (idempotent).');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resout le mot de passe demo : option `--password` prioritaire, sinon
|
||||||
|
* variable d'environnement. Renvoie null si aucun n'est fourni (la commande
|
||||||
|
* refuse alors --with-demo-users plutot que d'inventer un mot de passe).
|
||||||
|
*/
|
||||||
|
private function resolveDemoPassword(InputInterface $input): ?string
|
||||||
|
{
|
||||||
|
/** @var null|string $option */
|
||||||
|
$option = $input->getOption('password');
|
||||||
|
if (null !== $option && '' !== $option) {
|
||||||
|
return $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = $_SERVER[self::PASSWORD_ENV] ?? $_ENV[self::PASSWORD_ENV] ?? getenv(self::PASSWORD_ENV);
|
||||||
|
if (is_string($env) && '' !== $env) {
|
||||||
|
return $env;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Core\Infrastructure\DataFixtures;
|
||||||
|
|
||||||
|
use App\Module\Core\Application\Rbac\RbacSeeder;
|
||||||
|
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixture dev/test des roles metier MALIO (bureau / compta / commerciale /
|
||||||
|
* usine) + comptes demo associes. DRY : delegue au MEME service RbacSeeder que
|
||||||
|
* la commande `app:seed-rbac`, de sorte que `make db-reset` reproduise l'etat
|
||||||
|
* de recette.
|
||||||
|
*
|
||||||
|
* Depend de SitesFixtures : les comptes demo sont rattaches au site par defaut
|
||||||
|
* (cf. RbacSeeder::DEFAULT_SITE_NAME).
|
||||||
|
*
|
||||||
|
* ⚠ N'attache PAS la matrice § 2.7 ici : `doctrine:fixtures:load` PURGE la table
|
||||||
|
* `permission` avant de charger, donc les codes `commercial.clients.*` ne sont
|
||||||
|
* pas encore en base au moment du load (cf. ordre du makefile : fixtures PUIS
|
||||||
|
* `app:sync-permissions`). La matrice est attachee juste apres, par l'etape
|
||||||
|
* `app:seed-rbac` du makefile (db-reset / test-db-setup), via le meme seeder.
|
||||||
|
* Resultat final identique a la recette : roles + matrice + comptes demo.
|
||||||
|
*/
|
||||||
|
final class RbacDemoFixtures extends Fixture implements DependentFixtureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mot de passe DEV/TEST connu des comptes demo (bureau / compta /
|
||||||
|
* commerciale / usine). Reference par les tests fonctionnels de matrice
|
||||||
|
* RBAC. Sans rapport avec la prod : en recette/prod le mot de passe est
|
||||||
|
* fourni explicitement a `app:seed-rbac --with-demo-users --password=...`.
|
||||||
|
*/
|
||||||
|
public const string DEMO_PASSWORD = 'demo';
|
||||||
|
|
||||||
|
public function __construct(private readonly RbacSeeder $seeder) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, class-string>
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return [SitesFixtures::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
// Idempotent : ensureRoles puis ensureDemoUsers (lookup par code /
|
||||||
|
// username). La matrice est volontairement deferree (cf. docblock).
|
||||||
|
$this->seeder->ensureRoles();
|
||||||
|
$this->seeder->ensureDemoUsers(self::DEMO_PASSWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user