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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user