0e3299300f
Auto Tag Develop / tag (push) Successful in 11s
## Objectif
Seeder le RBAC métier de façon **rejouable et disponible en recette/prod** (commande applicative, pas fixture `require-dev`), durcir RG-1.04, et écrire le test de matrice (rôles enfin existants).
## A. `RbacSeeder` (Core) — source unique anti-drift
4 rôles (`bureau`/`compta`/`commerciale`/`usine`, isSystem=false), matrice § 2.7 (rôle → permissions) et comptes démo, définis en **un seul endroit**. Méthodes idempotentes `ensureRoles` / `attachMatrix` / `ensureDemoUsers`. `commerciale` référence `BusinessRoles::COMMERCIALE` (déjà consommé par RG-1.04).
## B. Commande `app:seed-rbac` (présente en build prod, idempotente, non destructive)
- Sans option : rôles + matrice § 2.7.
- `--with-demo-users` + `--password=<…>` ou env `RBAC_DEMO_PASSWORD` : 1 compte démo/rôle. **Aucun mot de passe en dur** côté serveur.
- Garde-fou : exit non-zéro + invite à lancer `app:sync-permissions` si les codes `commercial.clients.*` manquent.
## C. Fixture dev/test `RbacDemoFixtures` (DRY)
Appelle le **même seeder** (`ensureRoles` + `ensureDemoUsers`). La matrice est attachée juste après par l'étape `app:seed-rbac` du makefile (la table `permission` est purgée au moment du `fixtures:load`, donc `attachMatrix` ne peut pas tourner pendant le load). `make db-reset` / `test-db-setup` reproduisent l'état de recette.
## Déploiement (documenté README)
Après `migration-migrate` + `app:sync-permissions` : `app:seed-rbac` (prod) ; `app:seed-rbac --with-demo-users --password=…` (recette).
## D. Durcissement RG-1.04
Pour une Commerciale, complétude de l'onglet Information exigée sur **POST + tout PATCH** (suppression de la condition d'intersection). Conséquence : POST Commerciale → 422 (le POST n'expose pas le groupe Information), Admin → 201. Spec § 7 amendée.
## Compta ↔ onglet Comptabilité (§ 2.7)
Pour que `compta PATCH accounting → 200` (exigé par la matrice), la security du `Patch /clients/{id}` est élargie à `manage` **OU** `accounting.manage`, et un nouveau **`guardManage`** (mode strict RG-1.28) interdit à un porteur non-`manage` de modifier les onglets principal/Information (→ 403). Approche validée : élargir la security + guard in-processor (pas de nouvel endpoint).
## E. `ClientRBACMatrixTest`
Matrice § 2.7 complète via les comptes démo seedés (`app:seed-rbac --with-demo-users`) : bureau / compta / commerciale / usine (200/403 par verbe et par onglet) + RG-1.04 (POST Commerciale 422 / Admin 201).
## Tests
`make php-cs-fixer-allow-risky` OK ; `make test` **429 tests verts**. Idempotence vérifiée (rejeu de la commande : 0 rôle / 0 lien / 0 user). `test-db-setup` exécute la nouvelle étape `app:seed-rbac` sans erreur.
Cible : `develop`. Squash merge.
---------
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #40
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
139 lines
4.8 KiB
PHP
139 lines
4.8 KiB
PHP
<?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;
|
|
}
|
|
}
|