From ac662e701b2e710cd230e7ebf68a204c802d2f50 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 19 Jun 2026 17:00:14 +0200 Subject: [PATCH] feat(core) : aggregate module permissions and add sync-permissions command --- src/Module/Core/CoreModule.php | 10 ++- .../Console/SyncPermissionsCommand.php | 84 +++++++++++++++++++ src/Shared/Domain/Module/ModuleRegistry.php | 27 ++++++ .../Core/SyncPermissionsCommandTest.php | 29 +++++++ .../Module/ModuleRegistryPermissionsTest.php | 29 +++++++ 5 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php create mode 100644 tests/Functional/Module/Core/SyncPermissionsCommandTest.php create mode 100644 tests/Unit/Shared/Module/ModuleRegistryPermissionsTest.php diff --git a/src/Module/Core/CoreModule.php b/src/Module/Core/CoreModule.php index fde5724..587b3db 100644 --- a/src/Module/Core/CoreModule.php +++ b/src/Module/Core/CoreModule.php @@ -24,16 +24,18 @@ final class CoreModule implements ModuleInterface } /** - * Permissions posées pour le RBAC fin (1.2). Inertes tant que 1.2 n'est pas livré. + * Permissions RBAC fin du Module Core (1.2). * * @return list */ public static function permissions(): array { return [ - ['code' => 'core.user.read', 'label' => 'Consulter les utilisateurs'], - ['code' => 'core.user.manage', 'label' => 'Gérer les utilisateurs'], - ['code' => 'core.notification.read', 'label' => 'Consulter ses notifications'], + ['code' => 'core.users.view', 'label' => 'Voir les utilisateurs'], + ['code' => 'core.users.manage', 'label' => 'Gérer les utilisateurs (créer, éditer, supprimer)'], + ['code' => 'core.roles.view', 'label' => 'Voir les rôles RBAC'], + ['code' => 'core.roles.manage', 'label' => 'Gérer les rôles et permissions'], + ['code' => 'core.permissions.view', 'label' => 'Consulter le catalogue des permissions RBAC'], ]; } } diff --git a/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php b/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php new file mode 100644 index 0000000..6dd35bd --- /dev/null +++ b/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php @@ -0,0 +1,84 @@ + $moduleClasses */ + $moduleClasses = require $this->projectDir.'/config/modules.php'; + + // Phase 1 : permissions désirées (code => {code,label,module}). + $desired = []; + foreach (ModuleRegistry::permissions($moduleClasses) as $perm) { + $desired[$perm['code']] = $perm; + } + + // Phase 2 : upsert. + $existing = []; + foreach ($this->permissions->findAll() as $permission) { + $existing[$permission->getCode()] = $permission; + } + + $added = $updated = $revived = 0; + foreach ($desired as $code => $perm) { + $entity = $existing[$code] ?? null; + if (null === $entity) { + $this->permissions->save(new Permission($perm['code'], $perm['label'], $perm['module'])); + ++$added; + + continue; + } + if ($entity->isOrphan()) { + $entity->revive($perm['label'], $perm['module']); + ++$revived; + } elseif ($entity->getLabel() !== $perm['label'] || $entity->getModule() !== $perm['module']) { + $entity->updateMetadata($perm['label'], $perm['module']); + ++$updated; + } + } + + // Phase 3 : orphelines (existantes absentes des désirées). + $orphaned = 0; + foreach ($existing as $code => $entity) { + if (!isset($desired[$code]) && !$entity->isOrphan()) { + $entity->markOrphan(); + ++$orphaned; + } + } + + $this->em->flush(); + + $io->success(sprintf('Permissions synchronisées : %d ajoutées, %d mises à jour, %d réactivées, %d orphelines. Total désirées : %d.', $added, $updated, $revived, $orphaned, count($desired))); + + return Command::SUCCESS; + } +} diff --git a/src/Shared/Domain/Module/ModuleRegistry.php b/src/Shared/Domain/Module/ModuleRegistry.php index 894b6af..6c212bb 100644 --- a/src/Shared/Domain/Module/ModuleRegistry.php +++ b/src/Shared/Domain/Module/ModuleRegistry.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Shared\Domain\Module; +use InvalidArgumentException; + final class ModuleRegistry { /** @@ -22,4 +24,29 @@ final class ModuleRegistry return $ids; } + + /** + * @param list $moduleClasses + * + * @return list + */ + public static function permissions(array $moduleClasses): array + { + $out = []; + foreach ($moduleClasses as $moduleClass) { + if (!is_a($moduleClass, ModuleInterface::class, true)) { + continue; + } + $moduleId = $moduleClass::id(); + foreach ($moduleClass::permissions() as $perm) { + $code = $perm['code']; + if (!str_starts_with($code, $moduleId.'.')) { + throw new InvalidArgumentException(sprintf('Permission "%s" du module "%s" doit être préfixée par "%s.".', $code, $moduleId, $moduleId)); + } + $out[] = ['code' => $code, 'label' => $perm['label'], 'module' => $moduleId]; + } + } + + return $out; + } } diff --git a/tests/Functional/Module/Core/SyncPermissionsCommandTest.php b/tests/Functional/Module/Core/SyncPermissionsCommandTest.php new file mode 100644 index 0000000..f39d44c --- /dev/null +++ b/tests/Functional/Module/Core/SyncPermissionsCommandTest.php @@ -0,0 +1,29 @@ +find('app:sync-permissions')); + $tester->execute([]); + $tester->assertCommandIsSuccessful(); + + $repo = self::getContainer()->get(PermissionRepositoryInterface::class); + self::assertNotNull($repo->findByCode('core.users.manage')); + self::assertContains('core.roles.manage', $repo->findAllCodes()); + } +} diff --git a/tests/Unit/Shared/Module/ModuleRegistryPermissionsTest.php b/tests/Unit/Shared/Module/ModuleRegistryPermissionsTest.php new file mode 100644 index 0000000..a76de2d --- /dev/null +++ b/tests/Unit/Shared/Module/ModuleRegistryPermissionsTest.php @@ -0,0 +1,29 @@ +