diff --git a/src/Module/Core/Domain/Entity/Permission.php b/src/Module/Core/Domain/Entity/Permission.php new file mode 100644 index 0000000..f23759a --- /dev/null +++ b/src/Module/Core/Domain/Entity/Permission.php @@ -0,0 +1,105 @@ + false])] + private bool $orphan = false; + + public function __construct(string $code, string $label, string $module) + { + $this->code = $code; + $this->label = $label; + $this->module = $module; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCode(): string + { + return $this->code; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getModule(): string + { + return $this->module; + } + + public function isOrphan(): bool + { + return $this->orphan; + } + + /** + * Marque la permission comme orpheline : son code n'est plus declare par + * aucun module. Elle reste en base pour preserver les assignations et + * permettre une reactivation ulterieure, mais doit etre ignoree par les + * verifications d'autorisation. + */ + public function markOrphan(): self + { + $this->orphan = true; + + return $this; + } + + /** + * Reactive une permission precedemment orpheline : son code reapparait + * dans le code source d'un module. On en profite pour rafraichir les + * metadonnees (libelle et module d'appartenance). + */ + public function revive(string $label, string $module): self + { + $this->orphan = false; + $this->label = $label; + $this->module = $module; + + return $this; + } + + /** + * Met a jour les metadonnees d'une permission active sans toucher a son + * statut d'orphelin. Utilise par la commande de synchronisation lorsque + * seul le libelle ou le module proprietaire a change cote code. + */ + public function updateMetadata(string $label, string $module): self + { + $this->label = $label; + $this->module = $module; + + return $this; + } +} diff --git a/src/Module/Core/Domain/Entity/Role.php b/src/Module/Core/Domain/Entity/Role.php new file mode 100644 index 0000000..8db87df --- /dev/null +++ b/src/Module/Core/Domain/Entity/Role.php @@ -0,0 +1,144 @@ + false])] + private bool $isSystem = false; + + /** @var Collection */ + #[ORM\ManyToMany(targetEntity: Permission::class, fetch: 'EAGER')] + #[ORM\JoinTable(name: 'role_permission')] + private Collection $permissions; + + public function __construct(string $code, string $label, bool $isSystem = false, ?string $description = null) + { + $this->code = $code; + $this->label = $label; + $this->isSystem = $isSystem; + $this->description = $description; + $this->permissions = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCode(): string + { + return $this->code; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function isSystem(): bool + { + return $this->isSystem; + } + + /** @return Collection */ + public function getPermissions(): Collection + { + return $this->permissions; + } + + /** + * Met a jour le libelle affichable du role. Le code reste immuable pour + * garantir la stabilite des references cote fixtures et migrations. + */ + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * Met a jour la description libre du role (champ documentaire). + */ + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + /** + * Ajoute une permission au role. Idempotent : ajouter deux fois la meme + * permission n'entraine pas de doublon dans la collection. + */ + public function addPermission(Permission $permission): self + { + if (!$this->permissions->contains($permission)) { + $this->permissions->add($permission); + } + + return $this; + } + + /** + * Retire une permission du role. Idempotent : retirer une permission + * absente est un no-op silencieux. + */ + public function removePermission(Permission $permission): self + { + $this->permissions->removeElement($permission); + + return $this; + } + + /** + * Garde domaine : refuse la suppression d'un role marque comme systeme. + * La traduction HTTP (403) est faite au niveau application / API Platform. + */ + public function ensureDeletable(): void + { + if ($this->isSystem) { + throw SystemRoleDeletionException::forRole($this); + } + } +} diff --git a/src/Module/Core/Domain/Exception/SystemRoleDeletionException.php b/src/Module/Core/Domain/Exception/SystemRoleDeletionException.php new file mode 100644 index 0000000..aa34d01 --- /dev/null +++ b/src/Module/Core/Domain/Exception/SystemRoleDeletionException.php @@ -0,0 +1,27 @@ +getCode())); + } +} diff --git a/src/Module/Core/Domain/Security/SystemRoles.php b/src/Module/Core/Domain/Security/SystemRoles.php new file mode 100644 index 0000000..ba1d68c --- /dev/null +++ b/src/Module/Core/Domain/Security/SystemRoles.php @@ -0,0 +1,23 @@ +getId()); + self::assertSame('core.users.view', $permission->getCode()); + self::assertSame('Voir les utilisateurs', $permission->getLabel()); + self::assertSame('core', $permission->getModule()); + self::assertFalse($permission->isOrphan()); + } + + public function testMarkOrphanSetsFlag(): void + { + $permission = new Permission('core.users.view', 'Voir les utilisateurs', 'core'); + + $permission->markOrphan(); + + self::assertTrue($permission->isOrphan()); + } + + public function testReviveResetsOrphanAndUpdatesMetadata(): void + { + $permission = new Permission('core.users.view', 'Old label', 'core'); + $permission->markOrphan(); + + $permission->revive('New label', 'commercial'); + + self::assertFalse($permission->isOrphan()); + self::assertSame('New label', $permission->getLabel()); + self::assertSame('commercial', $permission->getModule()); + } + + public function testUpdateMetadataDoesNotTouchOrphan(): void + { + $permission = new Permission('core.users.view', 'Old', 'core'); + $permission->markOrphan(); + + $permission->updateMetadata('Lbl', 'core'); + + self::assertTrue($permission->isOrphan()); + self::assertSame('Lbl', $permission->getLabel()); + } +} diff --git a/tests/Module/Core/Domain/Entity/RoleTest.php b/tests/Module/Core/Domain/Entity/RoleTest.php new file mode 100644 index 0000000..3910550 --- /dev/null +++ b/tests/Module/Core/Domain/Entity/RoleTest.php @@ -0,0 +1,79 @@ +getId()); + self::assertSame('custom', $role->getCode()); + self::assertSame('Custom', $role->getLabel()); + self::assertNull($role->getDescription()); + self::assertFalse($role->isSystem()); + self::assertTrue($role->getPermissions()->isEmpty()); + } + + public function testAddPermissionAddsOnce(): void + { + $role = new Role('custom', 'Custom'); + $permission = new Permission('core.users.view', 'Voir', 'core'); + + $role->addPermission($permission); + $role->addPermission($permission); + + self::assertSame(1, $role->getPermissions()->count()); + } + + public function testRemovePermissionRemovesWhenPresent(): void + { + $role = new Role('custom', 'Custom'); + $permission = new Permission('core.users.view', 'Voir', 'core'); + + $role->addPermission($permission); + $role->removePermission($permission); + + self::assertSame(0, $role->getPermissions()->count()); + } + + public function testRemovePermissionIsNoOpWhenAbsent(): void + { + $role = new Role('custom', 'Custom'); + $permission = new Permission('core.users.view', 'Voir', 'core'); + + $role->removePermission($permission); + + self::assertSame(0, $role->getPermissions()->count()); + } + + public function testEnsureDeletableAllowsNonSystemRole(): void + { + $role = new Role('custom', 'Custom', false); + + $role->ensureDeletable(); + + $this->expectNotToPerformAssertions(); + } + + public function testEnsureDeletableThrowsForSystemRole(): void + { + $role = new Role('admin', 'Admin', true); + + $this->expectException(SystemRoleDeletionException::class); + $this->expectExceptionMessage('admin'); + + $role->ensureDeletable(); + } +} diff --git a/tests/Module/Core/Domain/Security/SystemRolesTest.php b/tests/Module/Core/Domain/Security/SystemRolesTest.php new file mode 100644 index 0000000..0a1934e --- /dev/null +++ b/tests/Module/Core/Domain/Security/SystemRolesTest.php @@ -0,0 +1,24 @@ +