Skip to content

Commit d7209a0

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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy