Skip to content

Commit a2ef6a3

Browse files
committed
Add a debug:roles command
1 parent 495b2ce commit a2ef6a3

File tree

5 files changed

+427
-3
lines changed

5 files changed

+427
-3
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Command;
13+
14+
use Symfony\Bundle\SecurityBundle\Debug\DebugRoleHierarchy;
15+
use Symfony\Component\Console\Attribute\AsCommand;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
22+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
23+
24+
#[AsCommand(name: 'debug:roles', description: 'Display the role hierarchy configuration.')]
25+
final class DebugRolesCommand extends Command
26+
{
27+
private DebugRoleHierarchy $roleHierarchyDebug;
28+
29+
public function __construct(RoleHierarchyInterface $roleHierarchy)
30+
{
31+
$this->roleHierarchyDebug = new DebugRoleHierarchy($roleHierarchy);
32+
33+
parent::__construct();
34+
}
35+
36+
protected function configure(): void
37+
{
38+
$this->setHelp(<<<EOF
39+
This <info>%command.name%</info> command display the current role hierarchy.
40+
41+
<info>php %command.full_name%</info>
42+
43+
You can pass one or multiple role names to display the effective roles.
44+
45+
<info>php %command.full_name% ROLE_USER</info>
46+
47+
To get a tree view of the inheritance, use the <info>tree</info> option:
48+
49+
<info>php %command.full_name% --tree</info>
50+
<info>php %command.full_name% ROLE_USER --tree</info>
51+
52+
EOF
53+
)
54+
->setDefinition([
55+
new InputArgument('roles', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The role(s) to resolve'),
56+
new InputOption('tree', 't', InputOption::VALUE_NONE, 'Show the hierarchy in a tree view'),
57+
]);
58+
}
59+
60+
protected function execute(InputInterface $input, OutputInterface $output): int
61+
{
62+
$io = new SymfonyStyle($input, $output);
63+
64+
$roles = $input->getArgument('roles');
65+
66+
if (empty($roles)) {
67+
// Full configuration output
68+
$io->title('Current role hierarchy configuration:');
69+
70+
if ($input->getOption('tree')) {
71+
$this->outputTree($io, $this->roleHierarchyDebug->getHierarchy());
72+
} else {
73+
$this->outputMap($io, $this->roleHierarchyDebug->getMap());
74+
}
75+
76+
$io->comment('To show reachable roles for a given role, re-run this command with role names. (e.g. <comment>debug:roles ROLE_USER</comment>)');
77+
78+
return self::SUCCESS;
79+
}
80+
81+
// Matching roles output
82+
$io->title(sprintf('Effective roles for %s:', implode(', ', array_map(fn ($v) => sprintf('<info>%s</info>', $v), $roles))));
83+
84+
if ($input->getOption('tree')) {
85+
$this->outputTree($io, $this->roleHierarchyDebug->getHierarchy($roles));
86+
} else {
87+
$io->listing($this->roleHierarchyDebug->getReachableRoleNames($roles));
88+
}
89+
90+
return self::SUCCESS;
91+
}
92+
93+
private function outputMap(OutputInterface $output, array $map): void
94+
{
95+
foreach ($map as $main => $roles) {
96+
if ($this->roleHierarchyDebug->isPlaceholder($main)) {
97+
$main = $this->stylePlaceholder($main);
98+
}
99+
100+
$output->writeln(sprintf('%s:', $main));
101+
foreach ($roles as $r) {
102+
$output->writeln(sprintf(' - %s', $r));
103+
}
104+
$output->writeln('');
105+
}
106+
}
107+
108+
private function outputTree(OutputInterface $output, array $tree): void
109+
{
110+
foreach ($tree as $role => $hierarchy) {
111+
$output->writeln($this->generateTree($role, $hierarchy));
112+
$output->writeln('');
113+
}
114+
}
115+
116+
/**
117+
* Generates a tree representation, line by line, in the tree unix style.
118+
*
119+
* Example output:
120+
*
121+
* ROLE_A
122+
* └── ROLE_B
123+
*
124+
* ROLE_C
125+
* ├── ROLE_A
126+
* │ └── ROLE_B
127+
* └── ROLE_D
128+
*/
129+
private function generateTree(string $name, array $tree, string $indent = '', bool $last = true, bool $root = true): \Generator
130+
{
131+
if ($this->roleHierarchyDebug->isPlaceholder($name)) {
132+
$name = $this->stylePlaceholder($name);
133+
}
134+
135+
if ($root) {
136+
// Yield root node as it is
137+
yield $name;
138+
} else {
139+
// Generate line in the tree:
140+
// Line: [indent]├── [name]
141+
// Last line: [indent]└── [name]
142+
yield sprintf('%s%s%s %s', $indent, $last ? "\u{2514}" : "\u{251c}", str_repeat("\u{2500}", 2), $name);
143+
144+
// Update indent for next nested:
145+
// Append "| " for a nested tree
146+
// Append " " for last nested tree
147+
$indent .= ($last ? ' ' : "\u{2502}").str_repeat(' ', 3);
148+
}
149+
150+
$i = 0;
151+
$count = \count($tree);
152+
foreach ($tree as $key => $value) {
153+
yield from $this->generateTree($key, $value, $indent, $i === $count - 1, false);
154+
++$i;
155+
}
156+
}
157+
158+
private function stylePlaceholder(string $role): string
159+
{
160+
return sprintf('<info>%s</info>', $role);
161+
}
162+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Debug;
13+
14+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
15+
16+
/**
17+
* Wraps a RoleHierarchy to access computed map, hierarchy tree and placeholders.
18+
*
19+
* @author Nicolas Rigaud <squrious@protonmail.com>
20+
*
21+
* @internal
22+
*/
23+
final class DebugRoleHierarchy
24+
{
25+
private array $map = [];
26+
private array $hierarchy = [];
27+
private array $placeholders = [];
28+
private ?\Closure $placeholderMatcher = null;
29+
30+
public function __construct(private readonly RoleHierarchyInterface $decorated)
31+
{
32+
if (
33+
property_exists($this->decorated, 'map')
34+
&& property_exists($this->decorated, 'hierarchy')
35+
&& property_exists($this->decorated, 'rolePlaceholdersPatterns')
36+
&& method_exists($this->decorated, 'getMatchingPlaceholders')
37+
) {
38+
$this->map = (new \ReflectionProperty($this->decorated, 'map'))->getValue($this->decorated);
39+
$this->hierarchy = (new \ReflectionProperty($this->decorated, 'hierarchy'))->getValue($this->decorated);
40+
$this->placeholders = array_keys((new \ReflectionProperty($this->decorated, 'rolePlaceholdersPatterns'))->getValue($this->decorated));
41+
$this->placeholderMatcher = fn (array $roles) => (new \ReflectionMethod($this->decorated, 'getMatchingPlaceholders'))->invoke($this->decorated, $roles);
42+
}
43+
}
44+
45+
/**
46+
* Get reachable role names from the underlying RoleHierarchy.
47+
*
48+
* @param string[] $roles
49+
*
50+
* @return string[]
51+
*/
52+
public function getReachableRoleNames(array $roles): array
53+
{
54+
return $this->decorated->getReachableRoleNames($roles);
55+
}
56+
57+
/**
58+
* Get the hierarchy tree.
59+
*
60+
* Example output:
61+
*
62+
* [
63+
* 'ROLE_A' => [
64+
* 'ROLE_B' => [],
65+
* 'ROLE_C' => [
66+
* 'ROLE_D' => []
67+
* ]
68+
* ],
69+
* 'ROLE_C' => [
70+
* 'ROLE_D' => []
71+
* ]
72+
* ]
73+
*
74+
* @param string[] $roles Optionally restrict the tree to these roles
75+
*
76+
* @return array<string,array<string,array>>
77+
*/
78+
public function getHierarchy(array $roles = []): array
79+
{
80+
$hierarchy = [];
81+
82+
foreach ($roles ?: array_keys($this->hierarchy) as $role) {
83+
$hierarchy += $this->buildHierarchy([$role]);
84+
}
85+
86+
return $hierarchy;
87+
}
88+
89+
/**
90+
* Get the computed role map from the underlying RoleHierarchy.
91+
*
92+
* @return array<string,string[]>
93+
*/
94+
public function getMap(): array
95+
{
96+
return $this->map;
97+
}
98+
99+
/**
100+
* Get registered placeholders in the underlying RoleHierarchy.
101+
*
102+
* @return string[]
103+
*/
104+
public function getPlaceholders(): array
105+
{
106+
return $this->placeholders;
107+
}
108+
109+
/**
110+
* Return whether a given role is processed as a placeholder by the underlying RoleHierarchy.
111+
*/
112+
public function isPlaceholder(string $role): bool
113+
{
114+
return \in_array($role, $this->getPlaceholders());
115+
}
116+
117+
private function buildHierarchy(array $roles, array &$visited = []): array
118+
{
119+
$tree = [];
120+
foreach ($roles as $role) {
121+
$visited[] = $role;
122+
123+
$tree[$role] = [];
124+
125+
// Get placeholders matches
126+
$placeholders = array_diff($this->placeholderMatcher?->__invoke([$role]) ?? [], $visited) ?? [];
127+
array_push($visited, ...$placeholders);
128+
$tree[$role] += $this->buildHierarchy($placeholders, $visited);
129+
130+
// Get regular inherited roles
131+
$inherited = array_diff($this->hierarchy[$role] ?? [], $visited);
132+
array_push($visited, ...$inherited);
133+
$tree[$role] += $this->buildHierarchy($inherited, $visited);
134+
}
135+
136+
return $tree;
137+
}
138+
}

src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand;
15+
use Symfony\Bundle\SecurityBundle\Command\DebugRolesCommand;
1516

1617
return static function (ContainerConfigurator $container) {
1718
$container->services()
@@ -24,5 +25,10 @@
2425
false,
2526
])
2627
->tag('console.command', ['command' => 'debug:firewall'])
28+
->set('security.command.debug_role_hierarchy', DebugRolesCommand::class)
29+
->args([
30+
service('security.role_hierarchy'),
31+
])
32+
->tag('console.command', ['command' => 'debug:roles'])
2733
;
2834
};

0 commit comments

Comments
 (0)