mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-12 01:34:58 +00:00
Merge branch 'add-permission-list-command' into 'master'
Add `RoleDumper` and `DumpListPermissionsCommand` to generate a markdown list of permissions See merge request Chill-Projet/chill-bundles!874
This commit is contained in:
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
6
.changes/unreleased/Feature-20250904-181032.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Add a command to generate a list of permissions
|
||||||
|
time: 2025-09-04T18:10:32.334524026+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Command;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'chill:main:dump-list-permissions', description: 'Print a markdown reference of permissions (roles) grouped by title with dependencies).')]
|
||||||
|
final class DumpListPermissionsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private readonly RoleDumper $roleDumper)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$markdown = $this->roleDumper->dumpAsMarkdown();
|
||||||
|
$output->writeln($markdown);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
@@ -20,7 +20,7 @@ use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
|||||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter?: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||||
*/
|
*/
|
||||||
class ExportConfigNormalizer
|
class ExportConfigNormalizer
|
||||||
{
|
{
|
||||||
|
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class RoleDumper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RoleProvider $roleProvider,
|
||||||
|
private RoleHierarchyInterface $roleHierarchy,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function dumpAsMarkdown(): string
|
||||||
|
{
|
||||||
|
$roles = $this->roleProvider->getRoles();
|
||||||
|
$rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes();
|
||||||
|
|
||||||
|
// Group roles by title
|
||||||
|
$groups = [];
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$title = $this->roleProvider->getRoleTitle($role);
|
||||||
|
$title ??= 'Other';
|
||||||
|
$groups[$title][] = $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort groups by title
|
||||||
|
ksort($groups, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
foreach ($groups as $title => $roleList) {
|
||||||
|
// Sort roles by translated label for deterministic output
|
||||||
|
usort($roleList, function (string $a, string $b): int {
|
||||||
|
$ta = $this->translator->trans($a);
|
||||||
|
$tb = $this->translator->trans($b);
|
||||||
|
|
||||||
|
return strcasecmp($ta, $tb);
|
||||||
|
});
|
||||||
|
|
||||||
|
$translatedTitle = $this->translator->trans($title);
|
||||||
|
$lines[] = '## '.$translatedTitle;
|
||||||
|
|
||||||
|
foreach ($roleList as $role) {
|
||||||
|
// Translate primary role
|
||||||
|
$translatedRole = $this->translator->trans($role);
|
||||||
|
|
||||||
|
// Scope marker: (S) if needs scope, (~~S~~) if no scope required
|
||||||
|
$needsScope = !in_array($role, $rolesWithoutScopes, true);
|
||||||
|
$scopeMarker = $needsScope ? '(S)' : '(~~S~~)';
|
||||||
|
|
||||||
|
// Compute dependent roles from hierarchy (exclude itself)
|
||||||
|
$reachable = $this->roleHierarchy->getReachableRoleNames([$role]);
|
||||||
|
$dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role));
|
||||||
|
|
||||||
|
// Translate dependents and sort deterministically
|
||||||
|
$translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents);
|
||||||
|
sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
if (count($translatedDependents) > 0) {
|
||||||
|
$lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents));
|
||||||
|
} else {
|
||||||
|
$lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a blank line between groups
|
||||||
|
$lines[] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim possible trailing blank line
|
||||||
|
$markdown = rtrim(implode("\n", $lines));
|
||||||
|
|
||||||
|
return $markdown."\n"; // End with newline for POSIX friendliness
|
||||||
|
}
|
||||||
|
}
|
@@ -52,12 +52,8 @@ class RoleProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title for each role.
|
* Get the title for each role.
|
||||||
*
|
|
||||||
* @param string $role
|
|
||||||
*
|
|
||||||
* @return string the title of the role
|
|
||||||
*/
|
*/
|
||||||
public function getRoleTitle($role)
|
public function getRoleTitle(string $role): ?string
|
||||||
{
|
{
|
||||||
$this->initializeRolesTitlesCache();
|
$this->initializeRolesTitlesCache();
|
||||||
|
|
||||||
@@ -73,7 +69,7 @@ class RoleProvider
|
|||||||
/**
|
/**
|
||||||
* initialize the array for caching role and titles.
|
* initialize the array for caching role and titles.
|
||||||
*/
|
*/
|
||||||
private function initializeRolesTitlesCache()
|
private function initializeRolesTitlesCache(): void
|
||||||
{
|
{
|
||||||
// break if already initialized
|
// break if already initialized
|
||||||
if (null !== $this->rolesTitlesCache) {
|
if (null !== $this->rolesTitlesCache) {
|
||||||
|
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Security;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Chill\MainBundle\Security\RoleProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RoleDumperTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDumpAsMarkdownGroupsByTitleTranslatesAndListsDependencies(): void
|
||||||
|
{
|
||||||
|
// Fake provider with two groups
|
||||||
|
$provider = new class () implements ProvideRoleHierarchyInterface {
|
||||||
|
public const R_PERSON_SEE = 'CHILL_PERSON_SEE';
|
||||||
|
public const R_PERSON_UPDATE = 'CHILL_PERSON_UPDATE';
|
||||||
|
public const R_REPORT_SEE = 'CHILL_REPORT_SEE';
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
return [self::R_PERSON_SEE, self::R_PERSON_UPDATE, self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithoutScope(): array
|
||||||
|
{
|
||||||
|
// In this test, assume REPORT_SEE does not need scope, others do
|
||||||
|
return [self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithHierarchy(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Person' => [self::R_PERSON_SEE, self::R_PERSON_UPDATE],
|
||||||
|
'Report' => [self::R_REPORT_SEE],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$roleProvider = new RoleProvider([$provider]);
|
||||||
|
|
||||||
|
// Fake role hierarchy: UPDATE implies SEE; others none
|
||||||
|
$roleHierarchy = new class () implements RoleHierarchyInterface {
|
||||||
|
public function getReachableRoleNames(array $roles): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
foreach ($roles as $r) {
|
||||||
|
$output[] = $r;
|
||||||
|
if ('CHILL_PERSON_UPDATE' === $r) {
|
||||||
|
$output[] = 'CHILL_PERSON_SEE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($output));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fake translator that clearly shows translation applied
|
||||||
|
$translator = new class () implements TranslatorInterface {
|
||||||
|
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return 'T('.$id.')';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$dumper = new RoleDumper($roleProvider, $roleHierarchy, $translator);
|
||||||
|
$md = $dumper->dumpAsMarkdown();
|
||||||
|
|
||||||
|
$expected = "## T(Person)\n"
|
||||||
|
."- **T(CHILL_PERSON_SEE)** (S)\n"
|
||||||
|
."- **T(CHILL_PERSON_UPDATE)** (S): T(CHILL_PERSON_SEE)\n\n"
|
||||||
|
."## T(Report)\n"
|
||||||
|
."- **T(CHILL_REPORT_SEE)** (~~S~~)\n";
|
||||||
|
|
||||||
|
self::assertSame($expected, $md);
|
||||||
|
}
|
||||||
|
}
|
@@ -80,3 +80,7 @@ services:
|
|||||||
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
||||||
tags:
|
tags:
|
||||||
- {name: console.command}
|
- {name: console.command}
|
||||||
|
|
||||||
|
Chill\MainBundle\Command\DumpListPermissionsCommand:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
Reference in New Issue
Block a user