Skip to content

Commit 58e48ce

Browse files
committed
Add a debug:roles command
1 parent db85067 commit 58e48ce

File tree

9 files changed

+619
-9
lines changed

9 files changed

+619
-9
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
6.4
1616
---
1717

18+
* Add the `debug:roles` command to debug role hierarchy
1819
* Deprecate `Security::ACCESS_DENIED_ERROR`, `AUTHENTICATION_ERROR` and `LAST_USERNAME` constants, use the ones on `SecurityRequestAttributes` instead
1920
* Allow an array of `pattern` in firewall configuration
2021
* Add `$badges` argument to `Security::login`
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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: 'Debug the role hierarchy configuration.')]
25+
final class DebugRolesCommand extends Command
26+
{
27+
public function __construct(private readonly RoleHierarchyInterface $roleHierarchy)
28+
{
29+
parent::__construct();
30+
}
31+
32+
protected function configure(): void
33+
{
34+
$this->setHelp(<<<EOF
35+
This <info>%command.name%</info> command display the current role hierarchy:
36+
37+
<info>php %command.full_name%</info>
38+
39+
You can pass one or multiple role names to display the effective roles:
40+
41+
<info>php %command.full_name% ROLE_USER</info>
42+
43+
To get a tree view of the inheritance, use the <info>tree</info> option:
44+
45+
<info>php %command.full_name% --tree</info>
46+
<info>php %command.full_name% ROLE_USER --tree</info>
47+
48+
<comment>Note:</comment> With a custom implementation for <info>security.role_hierarchy</info>, the <info>--tree</info> option is ignored and the <info>roles</info> argument is required.
49+
50+
EOF
51+
)
52+
->setDefinition([
53+
new InputArgument('roles', ($this->isBuiltInRoleHierarchy() ? InputArgument::OPTIONAL : InputArgument::REQUIRED) | InputArgument::IS_ARRAY, 'The role(s) to resolve'),
54+
new InputOption('tree', 't', InputOption::VALUE_NONE, 'Show the hierarchy in a tree view'),
55+
]);
56+
}
57+
58+
protected function initialize(InputInterface $input, OutputInterface $output): void
59+
{
60+
if (!$this->isBuiltInRoleHierarchy()) {
61+
$io = new SymfonyStyle($input, $output);
62+
63+
if ($input->getOption('tree')) {
64+
$io->warning('Ignoring option "--tree" because of a custom role hierarchy implementation.');
65+
$input->setOption('tree', null);
66+
}
67+
}
68+
}
69+
70+
protected function interact(InputInterface $input, OutputInterface $output): void
71+
{
72+
if (!$this->isBuiltInRoleHierarchy() && empty($input->getArgument('roles'))) {
73+
$io = new SymfonyStyle($input, $output);
74+
75+
$roles[] = $io->ask('Enter a role to debug', validator: function (?string $role) {
76+
$role = trim($role);
77+
if (empty($role)) {
78+
throw new \RuntimeException('You must enter a non empty role name.');
79+
}
80+
81+
return $role;
82+
});
83+
while ($role = trim($io->ask('Add another role? (press enter to skip)') ?? '')) {
84+
$roles[] = $role;
85+
}
86+
87+
$input->setArgument('roles', $roles);
88+
}
89+
}
90+
91+
protected function execute(InputInterface $input, OutputInterface $output): int
92+
{
93+
$io = new SymfonyStyle($input, $output);
94+
95+
$roles = $input->getArgument('roles');
96+
97+
if (empty($roles)) {
98+
// Full configuration output
99+
$io->title('Current role hierarchy configuration:');
100+
101+
if ($input->getOption('tree')) {
102+
$this->outputTree($io, $this->getBuiltInDebugHierarchy()->getHierarchy());
103+
} else {
104+
$this->outputMap($io, $this->getBuiltInDebugHierarchy()->getMap());
105+
}
106+
107+
$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>)');
108+
109+
return self::SUCCESS;
110+
}
111+
112+
// Matching roles output
113+
$io->title(sprintf('Effective roles for %s:', implode(', ', array_map(fn ($v) => sprintf('<info>%s</info>', $v), $roles))));
114+
115+
if ($input->getOption('tree')) {
116+
$this->outputTree($io, $this->getBuiltInDebugHierarchy()->getHierarchy($roles));
117+
} else {
118+
$io->listing($this->roleHierarchy->getReachableRoleNames($roles));
119+
}
120+
121+
return self::SUCCESS;
122+
}
123+
124+
private function outputMap(OutputInterface $output, array $map): void
125+
{
126+
foreach ($map as $main => $roles) {
127+
if ($this->getBuiltInDebugHierarchy()->isPlaceholder($main)) {
128+
$main = $this->stylePlaceholder($main);
129+
}
130+
131+
$output->writeln(sprintf('%s:', $main));
132+
foreach ($roles as $r) {
133+
$output->writeln(sprintf(' - %s', $r));
134+
}
135+
$output->writeln('');
136+
}
137+
}
138+
139+
private function outputTree(OutputInterface $output, array $tree): void
140+
{
141+
foreach ($tree as $role => $hierarchy) {
142+
$output->writeln($this->generateTree($role, $hierarchy));
143+
$output->writeln('');
144+
}
145+
}
146+
147+
/**
148+
* Generates a tree representation, line by line, in the tree unix style.
149+
*
150+
* Example output:
151+
*
152+
* ROLE_A
153+
* └── ROLE_B
154+
*
155+
* ROLE_C
156+
* ├── ROLE_A
157+
* │ └── ROLE_B
158+
* └── ROLE_D
159+
*/
160+
private function generateTree(string $name, array $tree, string $indent = '', bool $last = true, bool $root = true): \Generator
161+
{
162+
if ($this->getBuiltInDebugHierarchy()->isPlaceholder($name)) {
163+
$name = $this->stylePlaceholder($name);
164+
}
165+
166+
if ($root) {
167+
// Yield root node as it is
168+
yield $name;
169+
} else {
170+
// Generate line in the tree:
171+
// Line: [indent]├── [name]
172+
// Last line: [indent]└── [name]
173+
yield sprintf('%s%s%s %s', $indent, $last ? "\u{2514}" : "\u{251c}", str_repeat("\u{2500}", 2), $name);
174+
175+
// Update indent for next nested:
176+
// Append "| " for a nested tree
177+
// Append " " for last nested tree
178+
$indent .= ($last ? ' ' : "\u{2502}").str_repeat(' ', 3);
179+
}
180+
181+
$i = 0;
182+
$count = \count($tree);
183+
foreach ($tree as $key => $value) {
184+
yield from $this->generateTree($key, $value, $indent, $i === $count - 1, false);
185+
++$i;
186+
}
187+
}
188+
189+
private function stylePlaceholder(string $role): string
190+
{
191+
return sprintf('<info>%s</info>', $role);
192+
}
193+
194+
private function isBuiltInRoleHierarchy(): bool
195+
{
196+
return $this->roleHierarchy instanceof DebugRoleHierarchy;
197+
}
198+
199+
private function getBuiltInDebugHierarchy(): DebugRoleHierarchy
200+
{
201+
if (!$this->roleHierarchy instanceof DebugRoleHierarchy) {
202+
throw new \LogicException('Cannot use the built-in debug hierarchy with a custom implementation.');
203+
}
204+
205+
return $this->roleHierarchy;
206+
}
207+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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\RoleHierarchy;
15+
16+
/**
17+
* Extended Role Hierarchy to access inner configuration data.
18+
*
19+
* @author Nicolas Rigaud <squrious@protonmail.com>
20+
*
21+
* @internal
22+
*/
23+
final class DebugRoleHierarchy extends RoleHierarchy
24+
{
25+
private readonly array $debugHierarchy;
26+
27+
public function __construct(array $hierarchy)
28+
{
29+
$this->debugHierarchy = $hierarchy;
30+
31+
parent::__construct($hierarchy);
32+
}
33+
34+
/**
35+
* Get the hierarchy tree.
36+
*
37+
* Example output:
38+
*
39+
* [
40+
* 'ROLE_A' => [
41+
* 'ROLE_B' => [],
42+
* 'ROLE_C' => [
43+
* 'ROLE_D' => []
44+
* ]
45+
* ],
46+
* 'ROLE_C' => [
47+
* 'ROLE_D' => []
48+
* ]
49+
* ]
50+
*
51+
* @param string[] $roles Optionally restrict the tree to these roles
52+
*
53+
* @return array<string,array<string,array>>
54+
*/
55+
public function getHierarchy(array $roles = []): array
56+
{
57+
$hierarchy = [];
58+
59+
foreach ($roles ?: array_keys($this->debugHierarchy) as $role) {
60+
$hierarchy += $this->buildHierarchy([$role]);
61+
}
62+
63+
return $hierarchy;
64+
}
65+
66+
/**
67+
* Get the computed role map.
68+
*
69+
* @return array<string,string[]>
70+
*/
71+
public function getMap(): array
72+
{
73+
return $this->map;
74+
}
75+
76+
/**
77+
* Return whether a given role is processed as a placeholder.
78+
*/
79+
public function isPlaceholder(string $role): bool
80+
{
81+
return \in_array($role, array_keys($this->rolePlaceholdersPatterns));
82+
}
83+
84+
private function buildHierarchy(array $roles, array &$visited = []): array
85+
{
86+
$tree = [];
87+
foreach ($roles as $role) {
88+
$visited[] = $role;
89+
90+
$tree[$role] = [];
91+
92+
// Get placeholders matches
93+
$placeholders = array_diff($this->getMatchingPlaceholders([$role]), $visited) ?? [];
94+
array_push($visited, ...$placeholders);
95+
$tree[$role] += $this->buildHierarchy($placeholders, $visited);
96+
97+
// Get regular inherited roles
98+
$inherited = array_diff($this->debugHierarchy[$role] ?? [], $visited);
99+
array_push($visited, ...$inherited);
100+
$tree[$role] += $this->buildHierarchy($inherited, $visited);
101+
}
102+
103+
return $tree;
104+
}
105+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\DependencyInjection\Compiler;
13+
14+
use Symfony\Bundle\SecurityBundle\Debug\DebugRoleHierarchy;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\Security\Core\Role\RoleHierarchy;
19+
20+
class RegisterDebugRoleHierarchyPass implements CompilerPassInterface
21+
{
22+
public function process(ContainerBuilder $container): void
23+
{
24+
if (!$container->hasDefinition('security.role_hierarchy')) {
25+
$container->removeDefinition('security.command.debug_role_hierarchy');
26+
27+
return;
28+
}
29+
30+
$definition = $container->findDefinition('security.role_hierarchy');
31+
32+
if (RoleHierarchy::class === $definition->getClass()) {
33+
$hierarchy = $definition->getArgument(0);
34+
$definition = new Definition(DebugRoleHierarchy::class, [$hierarchy]);
35+
}
36+
$container->setDefinition('debug.security.role_hierarchy', $definition);
37+
}
38+
}

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('debug.security.role_hierarchy'),
31+
])
32+
->tag('console.command', ['command' => 'debug:roles'])
2733
;
2834
};

src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\CleanRememberMeVerifierPass;
1818
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\MakeFirewallsEventDispatcherTraceablePass;
1919
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass;
20+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterDebugRoleHierarchyPass;
2021
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass;
2122
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass;
2223
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
@@ -103,5 +104,7 @@ public function build(ContainerBuilder $container): void
103104

104105
// must be registered before DecoratorServicePass
105106
$container->addCompilerPass(new MakeFirewallsEventDispatcherTraceablePass(), PassConfig::TYPE_OPTIMIZE, 10);
107+
108+
$container->addCompilerPass(new RegisterDebugRoleHierarchyPass(), PassConfig::TYPE_OPTIMIZE);
106109
}
107110
}

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